飞道的博客

Redis实战案例及问题分析之redis解决商户缓存以及相应缓存问题解决方案(穿透、雪崩、击穿)

565人阅读  评论(0)

目录

添加商户缓存、查询商户缓存

 缓存更新策略

主动更新策略 

 实现商铺缓存与数据库的双写一致

根据Id修改店铺时,先修改数据库,再删除缓存

 缓存穿透

 常见解决方案:

缓存空对象

布隆过滤

 ​编辑

增加id的复杂度,避免被猜测id规律

做好数据的基础格式校验

加强用户权限管理

做好热点参数的限流

编码解决商铺查询的缓存穿透问题

缓存雪崩

 雪崩解决方案:

给不同的key的TTL添加随机值

利用redis集群提高服务的可用性

给缓存业务添加降级限流策略

给业务添加多级缓存

缓存击穿

缓存击穿解决方法

互斥锁

逻辑过期 

​编辑 利用互斥锁方式解决缓存击穿问题

基于逻辑过期方式解决缓存击穿问题


缓存就是数据交换的缓冲区(cache),是存储数据的地方,一般读写性能比较高

缓存的作用:降低后端负载、提高读写效率,降低响应时间

缓存的成本:数据一致性成本、代码维护成本、运维成本

添加商户缓存、查询商户缓存


  
  1. @Service
  2. public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
  3. /*
  4. 因为这个类继承的是mybatis-plus的类,所以这个类是由spring管理的,可以直接把StringRedisTemplate注入给它
  5. */
  6. @Resource
  7. private StringRedisTemplate stringRedisTemplate;
  8. @Override
  9. public Result queryById (Long id) {
  10. //1.从redis查询商铺缓存
  11. String shopJason = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
  12. //2.判断缓存是否命中
  13. if (StrUtil.isNotBlank(shopJason)) {
  14. //3.如果命中直接返回商铺信息
  15. Shop shop = JSONUtil.toBean(shopJason, Shop.class);
  16. return Result.ok(shop);
  17. }
  18. //4.没有命中就根据id去数据库查
  19. //为什么这里可以直接用getById方法?
  20. //因为这个类继承了ServiceImpl<UserMapper, User>,这个mybatis-plus的类,所以可以直接用这个类里面的方法
  21. Shop shop = getById(id);
  22. //5.判断商铺是否存在
  23. if (shop == null){
  24. //6.如果不存在就返回错误信息
  25. return Result.fail( "店铺不存在");
  26. }
  27. //7.如果存在就把数据写入redis
  28. String jsonStr = JSONUtil.toJsonStr(shop);
  29. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonStr);
  30. //8.返回商品信息
  31. return Result.ok(shop);
  32. }
  33. }

 缓存更新策略

主动更新策略 

 

 

 

 实现商铺缓存与数据库的双写一致

根据Id修改店铺时,先修改数据库,再删除缓存


  
  1. public Result updateShop (Shop shop) {
  2. Long id = shop.getId();
  3. if (id == null){
  4. return Result.fail( "更新失败:店铺id不能为空");
  5. }
  6. //1.操作数据库
  7. updateById(shop);
  8. //2.删除缓存
  9. stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
  10. return Result.ok();
  11. }

 缓存穿透

缓存穿透是指客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会大导数据库。

 常见解决方案:

缓存空对象

优点:实现简单,维护方便

缺点:额外的内存消耗、可能造成短期的不一致

布隆过滤

 

 优点:内存占用少,没有多余key

缺点:实现复杂,存在误判可能

增加id的复杂度,避免被猜测id规律

做好数据的基础格式校验

加强用户权限管理

做好热点参数的限流

编码解决商铺查询的缓存穿透问题


  
  1. public Result queryById (Long id) {
  2. //1.从redis查询商铺缓存
  3. String shopJason = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
  4. //2.判断缓存是否命中
  5. if (StrUtil.isNotBlank(shopJason)) {
  6. //3.如果命中直接返回商铺信息
  7. Shop shop = JSONUtil.toBean(shopJason, Shop.class);
  8. return Result.ok(shop);
  9. }
  10. //判断是否为空值,因为前面已经判断了isNotBlank存在的两种情况,一个是null,一个是"",所以这里只需要判断不等于null就是""
  11. if (shopJason != null){
  12. //返回一个错误信息
  13. return Result.fail( "店铺不存在");
  14. }
  15. //4.没有命中就根据id去数据库查
  16. //为什么这里可以直接用getById方法?
  17. //因为这个类继承了ServiceImpl<UserMapper, User>,这个mybatis-plus的类,所以可以直接用这个类里面的方法
  18. Shop shop = getById(id);
  19. //5.判断商铺是否存在
  20. if (shop == null){
  21. //6.将空值写入redis缓存
  22. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, "",CACHE_NULL_TTL, TimeUnit.MINUTES);
  23. //返回错误信息
  24. return Result.fail( "店铺不存在");
  25. }
  26. //7.如果存在就把数据写入redis
  27. String jsonStr = JSONUtil.toJsonStr(shop);
  28. //加入超时时间,做到超时剔除
  29. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
  30. //8.返回商品信息
  31. return Result.ok(shop);
  32. }

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力

 雪崩解决方案:

给不同的key的TTL添加随机值

利用redis集群提高服务的可用性

给缓存业务添加降级限流策略

给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

缓存击穿解决方法

互斥锁

逻辑过期 

逻辑过期就是不给缓存设置TTL,这样Key就不会突然失效,就不会突然有大量的请求打到数据库上造成数据库崩溃,但是如何判断数据是否过期?加上逻辑过期时间来使得缓存更新,同时key会永远被查到。

 利用互斥锁方式解决缓存击穿问题


  
  1. //互斥锁解决缓存击穿的方法queryWithMutex()
  2. public Shop queryWithMutex (Long id){
  3. //1.从redis查询商铺缓存
  4. String shopJason = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
  5. //2.判断缓存是否命中
  6. if (StrUtil.isNotBlank(shopJason)) {
  7. //3.如果命中直接返回商铺信息
  8. Shop shop = JSONUtil.toBean(shopJason, Shop.class);
  9. return shop;
  10. }
  11. //判断是否为空值,因为前面已经判断了isNotBlank存在的两种情况,一个是null,一个是"",所以这里只需要判断不等于null就是""
  12. if (shopJason != null){
  13. //返回一个错误信息
  14. return null;
  15. }
  16. //4.获取锁
  17. String lockKey = "lock:shop:"+id;
  18. Shop shop = null;
  19. try {
  20. boolean isLock = tryLock(lockKey);
  21. //4.1如果获取锁失败,休眠一段时间并重试
  22. if (!isLock) {
  23. Thread.sleep( 50);
  24. return queryWithMutex(id);
  25. }
  26. //4.2如果获取锁成功,根据id查数据库,并进行缓存重建
  27. shop = getById(id);
  28. //5.判断商铺是否存在
  29. if (shop == null){
  30. //6.将空值写入redis缓存
  31. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, "",CACHE_NULL_TTL, TimeUnit.MINUTES);
  32. //返回错误信息
  33. return null;
  34. }
  35. //7.如果存在就把数据写入redis
  36. String jsonStr = JSONUtil.toJsonStr(shop);
  37. //加入超时时间,做到超时剔除
  38. stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
  39. } catch (InterruptedException e) {
  40. throw new RuntimeException(e);
  41. } finally {
  42. //8.释放互斥锁
  43. unlock(lockKey);
  44. }
  45. //9.返回商品信息
  46. return shop;
  47. }

基于逻辑过期方式解决缓存击穿问题


  
  1. private static final ExecutorService CACHE_REBUILE_EXECUTOR = Executors.newFixedThreadPool( 10);
  2. //逻辑过期解决缓存过期的方法queryWithLogicalExpire()
  3. public Shop queryWithLogicalExpire (Long id){
  4. //1.从redis查询商铺缓存
  5. String shopJason = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
  6. //2.判断缓存是否命中
  7. if (StrUtil.isBlank(shopJason)) {
  8. //3.如果不存在直接返回null
  9. return null;
  10. }
  11. //4.如果命中,需要将shopJason反序列化为对象
  12. RedisData redisData = JSONUtil.toBean(shopJason, RedisData.class);
  13. JSONObject data = (JSONObject) redisData.getData();
  14. Shop shop = JSONUtil.toBean(data, Shop.class);
  15. LocalDateTime expireTime = redisData.getExpireTime();
  16. //5.判断是否过期
  17. if(expireTime.isAfter(LocalDateTime.now())){
  18. //5.1未过期,直接返回店铺信息
  19. return shop;
  20. }
  21. //5.2已过期,需要缓存重建
  22. //6.缓存重建
  23. //6.1获取互斥锁
  24. String lockKey = LOCK_SHOP_KEY + id;
  25. boolean tryLock = tryLock(lockKey);
  26. //6.2判断是否获取锁成功
  27. if (tryLock) {
  28. //6.3成功,开启线程,实现缓冲重建
  29. CACHE_REBUILE_EXECUTOR.submit(() ->{
  30. try {
  31. //重建缓存
  32. this.saveShop2Redis(id, 20L);
  33. } catch (Exception e) {
  34. throw new RuntimeException();
  35. } finally {
  36. //释放锁
  37. unlock(lockKey);
  38. }
  39. });
  40. }
  41. //6.4失败,直接返回(过期的)
  42. return shop;
  43. }


转载:https://blog.csdn.net/PnJgHT/article/details/125458883
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场