• [Redis-实战] 企业常用的缓存使用方案(查询、更新、击穿、穿透、雪崩) 附源码


    目录

    🍊 缓存查询策略

    🍩 缓存更新策略

    🍭 缓存穿透

    🍣 缓存雪崩

    🍕 缓存击穿

    👾 项目源码下载​​​​​​​


    🍊 缓存查询策略

    我们要查询的业务数据并不是经常改变的, 这里我们可以放到Redis缓存中, 降低对数据库的请求

    下面我们以查询店铺为例, 因为店铺列表是不经常改变的数据, 所以我们可以请求redis缓存来降低MySQL的查询压力 

    1. @Override
    2. public Result queryShopById(Long id) {
    3. //1.从Redis中查询商铺缓存
    4. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
    5. //2.判断是否存在
    6. if (StrUtil.isNotBlank(shopJson)) {
    7. //3.存在, 直接返回
    8. Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    9. return Result.ok(shop);
    10. }
    11. //4.不存在, 根据id查询数据库
    12. Shop shop = getById(id);
    13. //不存在, 返回错误提示信息
    14. if (shop == null){
    15. return Result.fail("店铺不存在!");
    16. }
    17. //存在, 写入redis中
    18. stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop));
    19. return Result.ok(shop);
    20. }

    🍩 缓存更新策略

    在常规的企业开发中,我们优先选择的缓存策略是 更新数据库的同时也会去更新缓存

    在此情况下我们也要考虑三点 : 

    1. 更新数据库后再删除缓存, 再查询的时候重新添加缓存 (这样可以保证数据查询的是最新的)

    2.在单体项目中, 将缓存与数据库操作放在同一个事务中, 这样方便回滚. 分布式项目中需要使用分布式事务

    3. 在并发场景下, 应当先操作数据库,再删除缓存

    1. @Override
    2. @Transactional
    3. public void updateShop(Shop shop) {
    4. if (shop.getId() == null) {
    5. throw new RuntimeException("ID不能为null");
    6. }
    7. //1. 先更新数据库
    8. updateById(shop);
    9. //2. 后删除缓存
    10. stringRedisTemplate.delete("cache:shop" + shop.getId());
    11. }

    🍭 缓存穿透

    缓存穿透场景 : 假设用户恶意请求的数据在Redis和MySQL中均不存在, 导致Redis中的缓存不生效从而一直去请求MySQL

    解决方案 : 因为用户传来的恶意数据在缓存和数据库中都不存在, 在从数据库中查询不到后将恶意数据缓存在Redis中

    代码实现如下, 当在数据库没有查询到后, 将空信("")息存入到Redis中,并设置过期时间为2分钟, 当用户再次查询时, 校验如果为("") 直接返回 店铺信息不存在!

    1. @Override
    2. public Result queryShopById(Long id) {
    3. //1.从Redis中查询商铺缓存
    4. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
    5. //2.判断是否存在
    6. if (StrUtil.isNotBlank(shopJson)) {
    7. //3.存在, 直接返回
    8. Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    9. return Result.ok(shop);
    10. }
    11. //判断命中的是否是空值
    12. if (Objects.equals(shopJson, "")) {
    13. //返回错误信息
    14. return Result.fail("店铺信息不存在!");
    15. }
    16. //4.不存在, 根据id查询数据库
    17. Shop shop = getById(id);
    18. //不存在, 返回错误提示信息
    19. if (shop == null){
    20. //将空值写入Redis中 并将有效期时间改为2分钟
    21. stringRedisTemplate.opsForValue().set("cache:shop" + id, "", 2L, TimeUnit.MINUTES);
    22. //返回错误信息
    23. return Result.fail("店铺不存在!");
    24. }
    25. //存在, 写入redis中 设置过期时间为30分钟
    26. stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
    27. return Result.ok(shop);
    28. }

    🍣 缓存雪崩

    Redis的缓存雪崩意思是指: 在统一同一时间内Redis中的大量的Key失效, 导致请求压力到达数据库

    解决办法 : 缓存数据的过期时间设置随机,将不同的Key的TTL设置随机值

    🍕 缓存击穿

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

    🍥 解决方案1 : 互斥锁

    锁代码实现(获取锁, 释放锁)

    1. /**
    2. * 获取锁
    3. *
    4. * @param key
    5. * @return
    6. */
    7. private boolean tryLock(String key) {
    8. //相当于 SETNX:添加一个String类型的键值对,当key不存在的时候执行
    9. Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    10. // BooleanUtil可以帮你自动拆装箱解决可能空指针问题
    11. return BooleanUtil.isTrue(flag);
    12. }
    13. /**
    14. * 释放锁
    15. *
    16. * @param key
    17. */
    18. private void unlock(String key) {
    19. stringRedisTemplate.delete(key);
    20. }

    业务代码实现

    这里的互斥锁如果获取不到锁就会进入休眠状态, 然后再去重新获取锁, 这样做能保持数据的一致性

    1. /**
    2. * 缓存击穿
    3. */
    4. public Shop queryWithmutex(Long id){
    5. //1.从Redis中查询商铺缓存
    6. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
    7. //2.判断是否存在
    8. if (StrUtil.isNotBlank(shopJson)) {
    9. //3.存在, 直接返回
    10. return JSONUtil.toBean(shopJson, Shop.class);
    11. }
    12. //判断命中的是否是空值
    13. if (Objects.equals(shopJson, "")) {
    14. //返回错误信息
    15. log.error("店铺信息不存在");
    16. return null;
    17. }
    18. //4实现缓存重建
    19. //4.1 获取互斥锁
    20. String lockKey = "lock:shop" + id;
    21. Shop shop;
    22. try {
    23. //这块功能的业务是, 当A线程操作该方法是, B线程进来判断锁是否释放, 如果没有释放则休眠重试, 为了解决数据一致性的问题
    24. boolean isLock = tryLock(lockKey);
    25. //4.2判断锁是否获取成功
    26. if (!isLock){
    27. //4.3 失败,则休眠重试
    28. Thread.sleep(50);
    29. //递归重试(这块地方有异议 不建议递归, 后期用到类似业务可以寻找其他解决办法)
    30. return queryWithmutex(id);
    31. }
    32. //根据id查询数据库
    33. shop = getById(id);
    34. if (shop == null) {
    35. //不存在 将空值写入Redis中 并将有效期时间改为2分钟 (这里是为了解决缓存穿透问题)
    36. stringRedisTemplate.opsForValue().set("cache:shop" + id, "", 2L, TimeUnit.MINUTES);
    37. //返回错误信息
    38. log.error("店铺信息不存在");
    39. return null;
    40. }
    41. //存在, 写入redis中 设置过期时间为30分钟
    42. stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
    43. }catch (InterruptedException e){
    44. throw new RuntimeException(e);
    45. }finally {
    46. //7. 释放互斥锁
    47. unlock(lockKey);
    48. }
    49. return (shop);
    50. }

    解决方案2 : 逻辑过期

    设置逻辑过期数据和过期时间

    1. /**
    2. * 向Redis中写入店铺信息并设置逻辑过期时间
    3. * @param id
    4. * @param expireSeconds
    5. */
    6. public void saveShop2Redis(Long id ,Long expireSeconds){
    7. //1.查询店铺数据
    8. Shop shop = getById(id);
    9. //2.封装逻辑过期时间
    10. RedisData redisData = new RedisData();
    11. redisData.setData(shop);
    12. //设置过期时间秒 测试时设置的时间短一些方便测试过期
    13. redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    14. //3.写入Redis
    15. stringRedisTemplate.opsForValue().set("cache:shop" + id, JSONUtil.toJsonStr(redisData));
    16. }

    手动加入逻辑过期数据

    1. @Test
    2. void test(){
    3. //模拟后台管理手动设置热点数据
    4. shopService.saveShop2Redis(1L, 10L);
    5. }

     逻辑过期业务代码

    1. /**
    2. * 逻辑过期
    3. * @param id
    4. * @return
    5. */
    6. public Shop queryWithLogicalExpire(Long id) {
    7. //1.从Redis中查询商铺缓存
    8. String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id);
    9. //2.判断是否存在
    10. if (StrUtil.isBlank(shopJson)) {
    11. //3.不存在, 直接返回
    12. return null;
    13. }
    14. //4. 命中, 需要把JSON反序列化为对象
    15. log.info("打桩数据 : {}", shopJson);
    16. RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    17. //反序列化
    18. Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    19. //获取过期时间
    20. LocalDateTime expireTime = redisData.getExpireTime();
    21. //5.判断是否过期
    22. if (expireTime.isAfter(LocalDateTime.now())) {
    23. //5.1未过期, 直接返回店铺信息
    24. return shop;
    25. }
    26. //5.2已过期,需要缓存重建
    27. //6.缓冲重建
    28. //6.1获取互斥锁
    29. String lockKey = "lock:shop" + id;
    30. boolean isLock = tryLock(lockKey);
    31. //6.2判断锁是否获取成功
    32. if (isLock) {
    33. //TODO 6.3成功, 开启独立线程, 实现缓存重建
    34. CACHE_REBUILD_EXECUTOR.submit(() -> {
    35. try {
    36. //重建缓存
    37. log.info("开始缓存重建");
    38. this.saveShop2Redis(id, 20L);
    39. } catch (Exception e) {
    40. throw new RuntimeException(e);
    41. }finally {
    42. //释放锁
    43. log.info("释放锁");
    44. unlock(lockKey);
    45. }
    46. });
    47. }
    48. //6.4 返回过期的商铺信息
    49. return shop;
    50. }

    这里实现的互斥锁, 如果没有拿到锁就会直接return返回历史数据, 在并发环境下短期内会造成数据的不一致性

    比如我修改了name属性字段

     


    👾 项目源码下载

    扫描下方公众号二维码 回复: Redis缓存实战 即可领取项目源码 👇👇👇

  • 相关阅读:
    对Docker的认识和总结
    HCIA网络课程第二周作业
    k8s笔记 实例篇
    Windows11右键菜单修改为Win10模式的方法
    828 统计子串中唯一的字符 思路草稿纸箱
    裸机与RTOS(概念、关系、区别)
    Linux 信号 alarm函数 setitimer函数
    爆肝总结,软件测试-常见并发问题+解决方案,测试进阶...
    【广州华锐互动】VR高层小区安全疏散演练系统
    青少年python系列 42.面向对象-继承
  • 原文地址:https://blog.csdn.net/qq_45481709/article/details/127943247