1.1 商铺查询
根据商铺ID查询
从 redis 中查询商铺,如果没有就从数据库查询然后存入 reids
/**
* 查询单个店铺
* @param id 店铺ID
* @return
*/
@Override
public Result queryShopById(Long id) {
// 1.从 redis 中查询
String cacheShopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
if(StrUtil.isNotBlank(cacheShopJson)) {
// 存在直接返回
return Result.ok(JSONUtil.toBean(cacheShopJson,Shop.class));
}
// 2.缓存没有的话从数据库查询
QueryWrapper<Shop> shopQueryWrapper = new QueryWrapper<>();
shopQueryWrapper.eq("id",id);
Shop shop = shopMapper.selectOne(shopQueryWrapper);
// 3.设置到 redis 里给后面做缓存用
stringRedisTemplate.opsForValue().
set(RedisConstants.CACHE_SHOP_KEY + id,
JSONUtil.toJsonStr(shop),
RedisConstants.CACHE_SHOP_TTL,
TimeUnit.MINUTES);
return Result.ok(shop);
}
1.2 商铺缓存与数据库的双写一致
两种方法
先更新数据库,删除缓存,写入缓存
先删除缓存,更新数据库,写入缓存
我们这里选用第一种
缓存命中直接返回,如果没有命中缓存就从数据库查询然后写入缓存
都存在着线程安全的问题
更新完之后会删除缓存,后续查询的时候就会没有命中缓存,然后就去数据库查询写入缓存
/**
* 先操作数据库在更新
* @param shop
* @return
*/
@Override
@Transactional
public Result update(Shop shop)
{
Long id = shop.getId();
if(id == null) { return Result.fail("店铺ID不能为空"); }
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
}
1.3 缓存穿透
客户端请求的数据,缓存和数据库中都不存在
方案
缓存空对象
优点 : 实现简单,维护方便
缺点 : 额外的内存消耗
布隆过滤器
优点 : 内存占用少,没有多余的key
缺点 : 实现复杂.可能存在误判
1.3.1 实现缓存空对象

key - 前缀 + ID
第一次没有命中缓存,然后去查询数据库
数据库也没有,缓存空值到 redis
后续再次访问就会命中这个缓存空值,不会再去查询到数据库
/**
* 查询单个店铺
* @param id 店铺ID
* @return
*/
@Override
public Result queryShopById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从 redis 中查询
String cacheShopJson = stringRedisTemplate.opsForValue().get(key);
/**
* <li>{@code StrUtil.isNotBlank(null) // false}</li>
* <li>{@code StrUtil.isNotBlank("") // false}</li>
* <li>{@code StrUtil.isNotBlank(" \t\n") // false}</li>
* <li>{@code StrUtil.isNotBlank("abc") // true}</li>
*/
if(StrUtil.isNotBlank(cacheShopJson))
{
// 存在直接返回
return Result.ok(JSONUtil.toBean(cacheShopJson,Shop.class));
}
// 上面的 isNotBlank 判断 null 是 false ,所以这里还要在判断一次
if (cacheShopJson != null)
{
return Result.fail("店铺信息不存在");
}
// 2.缓存没有的话从数据库查询
Shop shop = super.getById(id);
// 缓存穿透 : 如果从数据库查询也是没有,缓存 null 值
if (shop == null)
{
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
// 3.设置到 redis 里给后面做缓存用
stringRedisTemplate.opsForValue().
set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
1.4缓存雪崩
同一时间大量的key同时失效,或者redis服务器宕机,客户端大量请求到数据库
方案
给不同的key添加随机时间
redis集群提高服务的可用性
给缓存业务添加降级限流策略
1.5 缓存击穿
热点key失效了,大量的请求到达数据库
方法
互斥锁
优点 : 保持一致性
缺点 : 线程等待,有死锁的风险
设置逻辑过期时间
优点 : 线程无需等待
缺点 : 实现复杂,不保证一致性
1.5.1 实现互斥锁
使用SETNX命令创建锁key(lock:shop:id)作为锁的表示,这个命令标识该key不存在时才会创建
判断是否获取到锁,没有的话休眠0.05秒,然后递归(注意!return,是递归的出口)
如果获取到锁,从缓存中查询,从数据库查询
最后释放锁
/**
* 判断是否获得锁
* @param lockKey
* @return
*/
private boolean tryLock(String lockKey) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 解锁
* @param lockKey
*/
private void unLock(String lockKey) {
stringRedisTemplate.delete(lockKey);
}
/**
* 互斥锁
* @param id 店铺ID
* @return
*/
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从 redis 中查询
String cacheShopJson = stringRedisTemplate.opsForValue().get(key);
/**
* <li>{@code StrUtil.isNotBlank(null) // false}</li>
* <li>{@code StrUtil.isNotBlank("") // false}</li>
* <li>{@code StrUtil.isNotBlank(" \t\n") // false}</li>
* <li>{@code StrUtil.isNotBlank("abc") // true}</li>
*/
if(StrUtil.isNotBlank(cacheShopJson))
{
// 存在直接返回
return JSONUtil.toBean(cacheShopJson,Shop.class);
}
// 上面的 isNotBlank 判断 null 是 false ,所以这里还要在判断一次
if (cacheShopJson != null)
{
return null;
}
// 互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
// 获取互斥锁
boolean flag = tryLock(lockKey);
// 判断是否获取成功
if (!flag) {
// 失败则休眠 0.05秒
Thread.sleep(50);
// 递归 记得 return !
// return 是递归出口
return queryWithMutex(id);
}
// 2.缓存没有的话从数据库查询
shop = super.getById(id);
// 模拟重建的延时
Thread.sleep(200);
// 缓存穿透 : 如果从数据库查询也是没有,缓存 null 值
if (shop == null)
{
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
// 3.设置到 redis 里给后面做缓存用
stringRedisTemplate.opsForValue().
set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
return shop;
}
1.5.2 测试
使用jmeter压力测试高并发的情况下只查询了一次数据库

1.5.3 实现逻辑过期时间
逻辑
去redis查询,如果没有就返回null
命中就判断逻辑时间是否已经过期
已过期尝试获取互斥锁
判断是否成功获取到互斥锁
是,开启独立线程查询数据库,缓存到redis,释放互斥锁
否,返回旧数据
/**
* 缓存逻辑过期时间
* @param id 商铺ID
* @param expireSeconds 过期时间
*/
@Override
public void saveShop2Redis(Long id, Long expireSeconds) {
// 查询数据库
Shop shop = super.getById(id);
// 存入缓存
RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
redisData.setData(shop);
// 并没有添加TTL时间,所以过期时间是我们自己定义
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSON.toJSONString(redisData));
}
@Data
public class RedisData {
// 逻辑时间
private LocalDateTime expireTime;
// 缓存数据
private Object data;
}
/**
* 逻辑过期时间
* @param id 店铺ID
* @return
*/
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
// 从 redis 中查询
String cacheShopJson = stringRedisTemplate.opsForValue().get(key);
// 未命中
if(StrUtil.isBlank(cacheShopJson)) return null;
// 命中
RedisData data = JSONUtil.toBean(cacheShopJson, RedisData.class);
// 将 Object转换为 Shop
Shop shop = JSONUtil.toBean((JSONObject) data.getData(),Shop.class);
LocalDateTime expireTime = data.getExpireTime();
// 判断逻辑时间是否已经过期
if (expireTime.isAfter(LocalDateTime.now())) return shop;// 未过期直接返回
// 已过期
String lockKey = LOCK_SHOP_KEY + id;
// 获取互斥锁
boolean isLock = tryLock(lockKey);
// 判断是否获取锁
if (isLock) {
// 成功,创建独立线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
// 失败,返回过期的商铺信息
return shop;
}
1.5.4 测试
如果缓存里已经是过期,还没有删除
测试高并发场景在缓存重建完成之前是否得到的是旧数据
1.6 缓存工具类封装
互斥锁,设置逻辑过期时间,这种我们可以封装成一个方法,后续调用即可
缓存到 redis
设置逻辑过期时间
缓存穿透 (返回 null 对象)
缓存击穿 (设置逻辑过期时间,缓存重建,互斥锁)
1.6.1 缓存到 reids
/**
* 存入 redis
* @param key
* @param value
* @param time
* @param unit
*/
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
1.6.2 缓存逻辑过期时间
/**
* 设置逻辑过期
* @param key
* @param value
* @param time
* @param unit
*/
public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit){
RedisData data = new RedisData();
// 设置逻辑过期时间
data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
data.setData(value);
// 存入 redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data));
}
1.6.3 缓存穿透 (缓存空对象)
/**
* 缓存穿透
* @param keyPrefix key
* @param id 实体类ID
* @param type 返回的实体类类型
* @param dbFallback 具体的调用哪个类的凡是规范
* @param <R> 实体类
* @param <ID> 什么类型的ID,可能是Long,可能是Integer
* @return
*/
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit)
{
String key = keyPrefix + id;
// 从 redis 中查询
String cacheShopJson = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(cacheShopJson)) return JSONUtil.toBean(cacheShopJson, type);// 存在直接返回// 存在直接返回
if (cacheShopJson != null) return null; // 上面的 isNotBlank 判断 null 是 false ,所以这里还要在判断一次
// 缓存没有的话从数据库查询,根据传递进来的类型来查询,因为你不知道调用什么接口的getByID()
R r = dbFallback.apply(id);
// 缓存穿透 : 如果从数据库查询也是没有,缓存 null 值
if (r == null)
{
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}
// 设置到 redis 里给后面做缓存用
this.set(key,r,time,unit);
return r;
}
1.6.4 缓存击穿 (互斥锁,缓存重建,设置逻辑过期时间)
// 创建线程
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 逻辑过期时间
* @param id 店铺ID
* @return
*/
public <R,ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit)
{
String key = keyPrefix + id;
// 从 redis 中查询
String cacheShopJson = stringRedisTemplate.opsForValue().get(key);
// 未命中
if(StrUtil.isBlank(cacheShopJson)) return null;
// 命中
RedisData data = JSONUtil.toBean(cacheShopJson, RedisData.class);
// 将 Object转换为 Shop
R r = JSONUtil.toBean((JSONObject) data.getData(),type);
LocalDateTime expireTime = data.getExpireTime();
// 判断逻辑时间是否已经过期
if (expireTime.isAfter(LocalDateTime.now())) return r;// 未过期直接返回
// 已过期
String lockKey = LOCK_SHOP_KEY + id;
// 获取互斥锁
boolean isLock = tryLock(lockKey);
// 判断是否获取锁
if (isLock) {
// 成功,创建独立线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
// 先查询数据库
R r1 = dbFallback.apply(id);
// 写入缓存
this.setWithLogicExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
// 失败,返回过期的商铺信息
return r;
}
/**
* 判断是否获得锁
* @param lockKey
* @return
*/
private boolean tryLock(String lockKey) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
/**
* 解锁
* @param lockKey
*/
private void unLock(String lockKey) {
stringRedisTemplate.delete(lockKey);
}