当前位置: 首页 > news >正文

【黑马点评】商户查询缓存

缓存

缓存:数据交换的缓冲区,存储数据的临时地方,读写性能高。
在这里插入图片描述

  • 优点:降低后端负载、提高读写效率
  • 缺点:缓存往往需要保证数据的一致性,就需要代码去维护缓存的一致性

缓存更新策略

在这里插入图片描述

业务场景:
低一致性需求:使用内存淘汰机制
高一致性需求:主动更新,以超时剔除为兜底方案

主动更新

  1. 自己写代码,在更新数据库的同时更新缓存
  2. 缓存与数据库整合为一个服务,由服务来维护一致性,调用者调用该服务,无需关心缓存的一致性问题
  3. 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致性

缓存穿透解决方案:缓存空对象

在这里插入图片描述

/**
     * 缓存穿透:缓存空对象
     * @param id
     * @return
     */
    public Shop queryWithPassThrough(Long id) {
        // 从redis中查缓存
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
        if(StrUtil.isNotBlank(shopJson)) {
            // 存在 - 直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否是空值
        Assert.isTrue(shopJson == null , "店铺不存在");
        // 不存在 - 操作数据库 - 写入redis
        Shop shop = getById(id);
        if(shop == null) {
            stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            throw new RuntimeException("店铺不存在");
        }
        stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

缓存击穿解决方案:互斥锁(setnx)

在这里插入图片描述
一个线程在请求redis的数据时,先尝试去获取锁;只有获取到锁的线程才能去执行相应的操作,如果获取不到锁,只能阻塞等待。

为了防止服务出现故障时导致锁无法释放,所以一般设置锁的时候会加一个过期时间。

 /**
  * 获取锁
  * @param key
  * @return
  */
 private boolean tryLock(String key) {
     Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); // 10秒锁
     return BooleanUtil.isTrue(flag);
 }
 
 /**
  * 释放锁
  * @param key
  */
 private void unLock(String key) {
     stringRedisTemplate.delete(key);
 }
 
/**
* 缓存击穿(互斥锁setnx) + 缓存穿透
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
   // 从redis中查缓存
   String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
   String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
   if(StrUtil.isNotBlank(shopJson)) { // 只有是有字符串的时候才会是true
       // 存在 - 直接返回
       return JSONUtil.toBean(shopJson, Shop.class);
   }
   // 判断命中的是否是空值
   Assert.isTrue(shopJson == null , "店铺不存在");

   // 实现缓存重建
   Shop shop = null;
   // 获取互斥锁
   String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
   try {
       boolean isLock = tryLock(lockKey);
       // 获取失败 - 休眠、重试
       if(!isLock) {
           Thread.sleep(50);
           queryWithMutex(id); // 递归、再次调用
       }
       // 获取成功 - 根据id查询数据库
       // 不存在 - 操作数据库 - 写入redis
       Thread.sleep(200);
       shop = getById(id);
       if(shop == null) {
           stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); // 店铺不存在 - 设置空值
           throw new RuntimeException("店铺不存在");
       }
       stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
   } catch (InterruptedException e) {
       throw new RuntimeException(e);
   } finally {
       unLock(lockKey); // 释放锁
   }
   return shop;
}

缓存击穿解决方案:互斥锁 + 逻辑过期

在这里插入图片描述

/**
 * 缓存击穿(互斥锁setnx、逻辑过期)
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) {
    // 从redis中查缓存
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
    String redisDataJson = stringRedisTemplate.opsForValue().get(shopKey);
    /*if(StrUtil.isBlank(redisDataJson)) { // 不存在
        return null;
    }*/

    RedisData<Shop> redisData = JSONUtil.toBean(redisDataJson, new TypeReference<RedisData<Shop>>() {}, false);
    // 未过期、直接返回
    if(redisData != null && redisData.getExpireTime().isAfter(LocalDateTime.now())) {
        return redisData.getData();
    }
    // 实现缓存重建
    Shop shop = redisData.getData();
    // 获取互斥锁
    String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 获取失败 - 返回旧数据
    if(!isLock) {
        return shop;
    }
    // 获取成功 - 开启独立线程 - 实现缓存重建 - 暂时返回旧的店铺信息
    CACHE_REBUILD_EXECUTOR.submit(()->{ // 开启独立线程
        try {
            saveShopToRedis(id, 30L); // 实现缓存重建
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            unLock(lockKey); // 释放锁
        }
    });
    return shop;
}

这里封装RedisData对象的时候也有个技巧,为了不破坏原本Shop的类型,所以决定使用组合的方式,让Shop称为RedisData的成员:

@Data
@Accessors(chain = true)
public class RedisData<T> {
   private LocalDateTime expireTime;
   private T data; // 这样可以避免对原来的数据做修改
}

缓存工具封装

@Slf4j
@Component
public class CacheClient {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 设置过期时间
     * @param key
     * @param value
     * @param expireTime
     * @param timeUnit
     * @param <T>
     */
    public <T> void set(String key, T value, Long expireTime, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
    }

    /**
     * 设置逻辑过期时间
     * @param key
     * @param data
     * @param logicalExpireTime
     * @param timeUnit
     * @param <T>
     */
    public <T> void setWithLogicalExpire(String key, T data, Long logicalExpireTime, TimeUnit timeUnit) {
        RedisData<T> redisData = new RedisData<T>().setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(logicalExpireTime))).setData(data);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 缓存穿透解决方案封装:缓存空对象
     * 调用:cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
     * @param keyPrefix
     * @param id
     * @param type 返回的数据类型
     * @param dbFallback:Function<ID, R> dbFallback函数式编程——Function<参数, 返回值>
     * @param expireTime
     * @param timeUnit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireTime, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        // 从redis中查缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(json)) {
            // 存在 - 直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        Assert.isTrue(json == null , "店铺不存在");
        // 不存在 - 操作数据库 - 写入redis
        R r = dbFallback.apply(id);
        if(r == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            throw new RuntimeException("店铺不存在");
        }
        set(key, r, expireTime, timeUnit);
        return r;
    }


    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); // 10秒锁
        return BooleanUtil.isTrue(flag);
    }
    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 缓存击穿(互斥锁setnx、逻辑过期)
     * 调用:cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.SECONDS);
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param expireTime
     * @param timeUnit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireTime, TimeUnit timeUnit) {
        // 从redis中查缓存
        String key = keyPrefix + id;
        String redisDataJson = stringRedisTemplate.opsForValue().get(key);
        RedisData<R> redisData = JSONUtil.toBean(redisDataJson, new TypeReference<RedisData<R>>() {}, false);
        // 未过期、直接返回
        R rOld = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        if(redisData != null && redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            return rOld;
        }
        // 实现缓存重建
        // 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 获取失败 - 返回旧数据
        if(!isLock) {
            return rOld;
        }
        // 获取成功 - 开启独立线程 - 实现缓存重建 - 暂时返回旧的店铺信息
        CACHE_REBUILD_EXECUTOR.submit(()->{ // 开启独立线程
            try {
                // 重建缓存 - 查询数据库
                R rNew = dbFallback.apply(id);
                this.setWithLogicalExpire(key, rNew, expireTime, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(lockKey); // 释放锁
            }
        });
        return rOld;
    }
}
http://www.dtcms.com/a/112086.html

相关文章:

  • 机器学习中的自监督学习概述与实现过程
  • 每日文献(八)——Part four
  • 基于YOLOv8的热力图生成与可视化-支持自定义模型与置信度阈值的多维度分析
  • FPGA系统开发板调试过程不同芯片的移植步骤介绍
  • Leetcode 680 -- 双指针 | 贪心
  • LeetCode算法题(Go语言实现)_30
  • AcWing 6135. 奶牛体检
  • 【Java集合】LinkedList源码深度分析
  • Nature Electronics|一种透气、可拉伸的液态金属基3D电子皮肤系统(健康监测/可穿戴电子/透汗透气性电子/电子皮肤/柔性电子/集成电路)
  • 降维算法之PCA(主成分分析)
  • 函数和模式化——python
  • 文件系统-重定向
  • 架构思维:冷热分离 - 表数据量大读写缓慢的优化方案
  • 有没有可以帮助理解高数的视频或者书籍资料?
  • 【力扣hot100题】(050)岛屿数量
  • 消息队列之-Kafka
  • #Linux内存管理# 在ARM32bit Linux中,高端内存的起始地址是如计算出来的?
  • 思二勋:未来所有的业务都将生于AI、长于AI、成于AI
  • 搜索二维矩阵
  • 笔记:代码随想录算法训练营day65:dijkstra(堆优化版)精讲、Bellman_ford 算法精讲
  • Docker安装、配置Redis
  • 使用Expo框架开发APP——详细教程
  • 【JavaScript】原型链 prototype 和 this 关键字的练习(老虎机)
  • 安当TDE透明加密:构建跨地域文件服务器安全传输的“双重保险“
  • VBA中类的解读及应用第二十二讲:利用类判断任意单元格的类型-5
  • C语言:3.31
  • 【YOLO系列(V5-V12)通用数据集-火灾烟雾检测数据集】
  • 大模型学习四:‌DeepSeek Janus-Pro 多模态理解和生成模型 本地部署指南(折腾版)
  • 七均线策略思路
  • Mac VM 卸载 win10 安装win7系统