素敵だった未来に繋がった夢
雷神点评——商户查询缓存
由于商店数量较少,所以多次查询返回结果相同,且实际操作中往往查询操作占大多数,考虑使用缓存优化查询操作,避免多次访问数据库
缓存概念
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
缓存优缺点如下:
优点:
降低后端负载
提高读写效率,降低响应时间缺点:
数据一致性成本
代码维护成本
运维成本
添加Redis缓存
Redis缓存模型作用示意如下:

具体流程示意图如下:

根据ID查询商店
控制层方法
@RestController
@RequestMapping("/shop")
public class ShopController {@Autowiredprivate ShopService shopService;@GetMapping(value="/{id}")public ResultData GetShopByID(@PathVariable("id") Long ShopId) {ShopData Shop = shopService.SelectShopById(ShopId);if(Shop==null) return ResultData.error("Shop dose not exist!");else return ResultData.success(Shop);}
}
服务层方法
@Service
public class ShopServiceImpl implements ShopService {@Autowiredprivate ShopMapper shopMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic ShopData SelectShopById(Long ShopId) {// 查询Redis,查看缓存中是否存在,有则直接返回String ShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+ShopId);if(!StringUtil.isNullOrEmpty(ShopJson)) {return JSONUtil.toBean(ShopJson,ShopData.class);}// 缓存中没有,则查询数据库,并更新缓存LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);ShopData Shop = shopMapper.selectOne(lambdaWrapper);if(Shop!=null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(Shop));}return Shop;}
}
常量封装工具类:
public class RedisConstants {public static final Long CACHE_NULL_TTL = 2L;public static final String CACHE_NULL_VALUE = "";public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final String LOCK_SHOP_KEY = "lock:shop:";public static final Long LOCK_SHOP_TTL = 10L;
}
测试效果如下:
第一次查询,走数据库查询并且缓存Redis更新


第二次同样的查询,没有走数据库

缓存更新策略
Redis如果缓存过量的话就需要及时汰换,这就是缓存更新策略

业务场景:
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存读操作
缓存命中直接返回
缓存未命中则查询数据并写入缓存写操作
先写数据库再删除缓存
确保数据库操作和缓存操作的原子性
根据ID插入或更新餐厅
// 控制层方法
@PutMapping
public ResultData UpdateShop(@RequestBody ShopData NewShop) {shopService.UpdateShop(NewShop);return ResultData.success();
}// 服务层方法
@Override
@Transactional // 添加业务注解保证操作原子性
public void UpdateShop(ShopData NewShop) {shopMapper.updateById(NewShop);stringRedisTemplate.delete(CACHE_SHOP_KEY+NewShop.getId());return;
}
缓存穿透和解决方法
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力。
常见的解决方案有两种:
缓存空对象
优点:实现简单易于维护
缺点:①额外的内存消耗 ②可能造成短期的数据不一致布隆过滤器
优点:内存占用较少,没有多余Key
缺点:①实现复杂 ②存在误判可能
除此之外,还有以下方法应对缓存穿透:
增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
这里采用缓存空对象方法来应对缓存穿透

代码如下:
@Override
public ShopData SelectShopById(Long ShopId) {String ShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+ShopId);if(!StringUtil.isNullOrEmpty(ShopJson)) {return JSONUtil.toBean(ShopJson,ShopData.class);}if (ShopJson != null && ShopJson.equals(CACHE_NULL_VALUE)) {// 判断是否是缓存中的空对象return null;}LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);ShopData Shop = shopMapper.selectOne(lambdaWrapper);// 如果为空则将空对象插入Redisif(Shop==null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,CACHE_NULL_VALUE,CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(Shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);return Shop;
}
测试效果如下:


缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
互斥锁:当缓存失效时,只有一个线程能获取到分布式锁去查询数据库并重建缓存,其他未获取到锁的线程则等待一段时间后重试,或直接返回降级数据。
①查询缓存,若命中则直接返回。
②若未命中,尝试获取分布式锁(如使用 Redis 的 SETNX 命令,需设置过期时间防止死锁)。
③双重检查:获取锁成功后,再次检查缓存是否已有数据(防止其他线程在等待期间已重建完成)。若有则直接返回。
④若仍无数据,则查询数据库,将结果写入缓存,并释放锁。
⑤未获取到锁的线程可短暂休眠后重试查询缓存,或快速失败返回提示。逻辑过期:缓存中不设置物理过期时间(TTL),而是在数据内部包含一个逻辑过期时间字段。
①查询缓存时,如果缓存中不存在数据(极少见,如首次加载),则直接查库并写入缓存(此时可加锁保证唯一性)。
②如果缓存存在,查看逻辑过期时间是否到期
③若未到期,直接返回数据。
④若已到期,不立即阻塞当前请求,而是返回旧数据(或空数据),同时启动一个异步线程去查询数据库并更新缓存。
⑤后续请求会逐渐获取到新数据。
流程示意图如下:

优缺点对比如下:

互斥锁应对缓存穿透
这里基于互斥锁方法来应对缓存穿透,流程示意图如下:

使用Redis中的setnx操作来模拟互斥锁
// 加锁操作
private boolean TryLock(String Key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(Key,"1",LOCK_SHOP_TTL,TimeUnit.MINUTES);return BooleanUtil.isTrue(flag);
}
// 去锁操作
private void Unlock(String Key) {stringRedisTemplate.delete(Key);
}private ShopData SelectShopByIdWithMutex(Long ShopId) throws InterruptedException {String ShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+ShopId);if(!StringUtil.isNullOrEmpty(ShopJson)) {return JSONUtil.toBean(ShopJson,ShopData.class);}if (ShopJson != null && ShopJson.equals(CACHE_NULL_VALUE)) {return null;}// 设置加锁的键String LockKey = LOCK_SHOP_KEY+ShopId;ShopData Shop = null;try {boolean IsLock = TryLock(LockKey);if(!IsLock) {// 模拟循环等待Thread.sleep(50);return SelectShopByIdWithMutex(ShopId);}LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);Shop = shopMapper.selectOne(lambdaWrapper);Thread.sleep(200); // 模拟重建延迟if(Shop==null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,CACHE_NULL_VALUE,CACHE_NULL_TTL, TimeUnit.MINUTES);}stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(Shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (Exception e) {throw new RuntimeException(e);} finally {// 保证无论如何都能去锁Unlock(LockKey);}return Shop;
}
逻辑过期应对缓存穿透
由于查询缓存时缓存中不存在数据情况极为少见,所以这里设定缓存中已存在数据(讨论情况为内容是否为空以及时间是否过期)
实现流程图如下:

实现代码如下:
// 用于封装过期时间的RedisData
public class RedisData {private LocalDateTime expireTime;private Object data;
}
// 服务层方法
@Service
public class ShopServiceImpl implements ShopService {@Autowiredprivate ShopMapper shopMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 设置线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);// 从数据库中读取最新信息并更新过期时间private void SaveShopToRedis(Long ShopId,Long ExpireSeconds) {LambdaQueryWrapper<ShopData> lambdaWrapper = new LambdaQueryWrapper<>();lambdaWrapper.eq(ShopData::getId,ShopId);ShopData Shop = shopMapper.selectOne(lambdaWrapper);RedisData redisData = new RedisData();redisData.setData(Shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(ExpireSeconds));stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ShopId,JSONUtil.toJsonStr(redisData));}private ShopData SelectShopByIdWithLogicExpire(Long ShopId) {String ShopKey = CACHE_SHOP_KEY+ShopId;String ShopJson = stringRedisTemplate.opsForValue().get(ShopKey);// 查看是否为空,如果为空表示不存在直接返回if(StrUtil.isBlank(ShopJson)) {return null;}//从Redis缓存中获取Shop数据,由于RedisData中封装数据类型为Object,需要进行格式转化RedisData redisData = JSONUtil.toBean(ShopJson,RedisData.class);ShopData Shop = JSONUtil.toBean(JSONUtil.toJsonStr(redisData.getData()), ShopData.class);LocalDateTime ExpireTime = redisData.getExpireTime();// 查看是否过期,如果没过期直接返回if(ExpireTime.isAfter(LocalDateTime.now())) {return Shop;}// 如果过期的话那么查看是否已加锁,获取锁成功则开启异步线程开始更新缓存数据String LockKey = LOCK_SHOP_KEY + ShopId;boolean IsLock = TryLock(LockKey);if(IsLock) {CACHE_REBUILD_EXECUTOR.submit(() -> {try{this.SaveShopToRedis(ShopId,20L);} catch (Exception e) {throw new RuntimeException(e);}finally {// 无论是否成功都要去锁Unlock(LockKey);}});}return Shop;}
}
