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


    目录

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

     缓存更新策略

    主动更新策略 

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

    根据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. }

  • 相关阅读:
    Rust7.2 More About Cargo and Crates.io
    Druid 查询超时配置的探究 → DataSource 和 JdbcTemplate 的 queryTimeout 到底谁生效?
    【Push Kit】关于推送消息没有锁屏通知的问题
    QT中,QTableWidget 使用示例详细说明
    Spring Cloud微服务架构:实现分布式系统的无缝协作
    设计模式-单例模式
    基于PHP+MySQL中小学生科学实验展示网站的设计与实现
    go协程的栈
    【java】poi-tl 1.9.1 word模板插入文本及动态复杂表格
    驱动开发:内核监控FileObject文件回调
  • 原文地址:https://blog.csdn.net/PnJgHT/article/details/125458883