redis实现

1、实现互斥锁

key-- lock:业务名称:业务主键

/**
 * 获取锁:利用redis的setnx 存在则返回false的特性 达到互斥
 * @param key
 * @return
 */
private boolean tryLock(String key){
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

/**
 * 释放锁 :删除redis 对应的键
 */
private void unLock(String key){
    stringRedisTemplate.delete(key);
}

2、逻辑过期解决缓存击穿

/**
 * 逻辑过期解决缓存击穿:热点key的缓存失效(redis未命中),大量并发的请求直接到数据库
 * 将热点key的value设置一个字段存逻辑过期的时间,在代码中进行判断是否过期,
 * 如果过期则使用互斥锁让一个线程去开另一个线程执行重建缓存的任务,在重建任务结束之前,其余并发都返回过期的数据
 * @return
 */
private Shop queryWithLogicalExpire(Long id){
    //从redis查商铺信息缓存,存在缓存则直接返回,不存在则查数据库并写回到redis中
    String redisShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //不存在缓存(非热点数据),直接返回为空
    if (StrUtil.isBlank(redisShopJson)){
        return null;
    }
   //存在缓存(命中),查看数据是否已过期
    RedisData redisData = JSONUtil.toBean(redisShopJson, RedisData.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    if (expireTime.isAfter(LocalDateTime.now())){
        //缓存未过期,返回店铺信息
        return shop;
    }
    //缓存已过期,互斥锁新开线程执行重建缓存,原线程返回过期的数据
    String lockKey =LOCK_SHOP_KEY +id;
    boolean isLock = tryLock(lockKey);
    if(isLock){
        //新线程执行重建缓存任务
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
            //过期时间设置的短,20秒,方便测试
                saveShopToRedis(id,20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                //释放锁
                unLock(lockKey);
            }
        });
    }
    //返回过期的数据
    return shop;
}

   /**
     * 保存商铺信息到redis(可做缓存预热)
     */
    public void saveShopToRedis(Long id,Long expireTimeSeconds) throws InterruptedException {
        //从数据库查询商铺信息
        Shop shop = getById(id);
        //模拟重建缓存业务复杂
        Thread.sleep(200);
        //封装缓存数据,含过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTimeSeconds));
        //这里不设置TTL 使用的是逻辑过期时间(用代码来控制)
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

/**
 * redis操作逻辑过期数据实体
 */
@Data
public class RedisData {

    /**
     * 过期时间
     */
    private LocalDateTime expireTime;

    /**
     * redis存储的业务实体
     */
    private Object data;
}

3、互斥锁解决缓存击穿

/**
 * 互斥锁解决缓存击穿:热点key的缓存失效(redis未命中),大量并发的请求直接到数据库
 * 使用redis的setnx制作互斥锁,在多线程请求中使用互斥锁由一个线程对热点key进行重建缓存,
 * 其余线程未命中先去拿锁,没拿到则等待一会后再次获取热点key(递归),直到缓存命中
 * @return
 */
private Shop queryWithMutex(Long id){
    //从redis查商铺信息缓存,存在缓存则直接返回,不存在则查数据库并写回到redis中
    String redisShopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //存在缓存
    if (StrUtil.isNotBlank(redisShopJson)){
        //字符串反序列化为实体对象
        Shop shop = JSONUtil.toBean(redisShopJson, Shop.class);
        return shop;
    }
    //解决缓存穿透:缓存中设置存的值为"",直接返回null
    if (redisShopJson != null){
        return null;
    }
    //互斥锁中重建缓存
    String lockKey =LOCK_SHOP_KEY +id;
    Shop dbShop = null;
    try {
        boolean flag = tryLock(lockKey);
        //没有拿到锁,等待一会后重新获取缓存(递归)
        if (!flag){
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        //拿到锁后,重建缓存,再次判断缓存中是否有值
        //不存在缓存,查数据库
        dbShop = getById(id);
        //模拟重建的时间的长,测试并发拿锁
        Thread.sleep(200);
        if(dbShop == null){
            //数据库数据也不存在,则在缓存中建立一个key,value为""
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(dbShop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        //释放锁
        unLock(lockKey);
    }
    return dbShop;
}

4、工具类

/**
 * @author zxk
 * @create 2023-02-26 23:39
 */
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    //线程池:缓存重建任务执行器
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //写入redis并设置ttl
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    //写入redis并设置逻辑过期时间
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData),time,unit);
    }


    //解决缓存穿透:缓存中和数据库中都没有数据,在缓存中建立一个key,value为""并设置过期时间,防止大量请求到数据库
    public  <R,ID>R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = keyPrefix + id;
        //从redis查缓存,存在缓存则直接返回,不存在则查数据库并写回到redis中
        String redisJson = stringRedisTemplate.opsForValue().get(key);
        //存在缓存
        if (StrUtil.isNotBlank(redisJson)){
            //字符串反序列化为实体对象
            return JSONUtil.toBean(redisJson, type);
        }
        //不存在,解决缓存穿透:缓存中设置存的值为"",直接返回null
        if (redisJson != null){
            //返回错误信息
            return null;
        }
        //不存在缓存,查数据库 函数式编程
        R r = dbFallback.apply(id);
        if(r == null){
            //数据库数据也不存在,则在缓存中建立一个key,value为""
            stringRedisTemplate.opsForValue().set(key, "",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //存在
        this.set(key, r, time, unit);
        return r;
    }

    /**
     * 逻辑过期解决缓存击穿:热点key的缓存失效(redis未命中),大量并发的请求直接到数据库
     * 将热点key的value设置一个字段存逻辑过期的时间,在代码中进行判断是否过期,
     * 如果过期则使用互斥锁让一个线程去开另一个线程执行重建缓存的任务,在重建任务结束之前,其余并发都返回过期的数据
     * @return
     */
    private <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String key = keyPrefix + id;
        //从redis查缓存,存在缓存则直接返回,不存在则查数据库并写回到redis中
        String redisJson = stringRedisTemplate.opsForValue().get(key);
        //不存在缓存(非热点数据),直接返回为空
        if (StrUtil.isBlank(redisJson)){
            return null;
        }
        //存在缓存(命中),查看数据是否已过期
        RedisData redisData = JSONUtil.toBean(redisJson, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        if (expireTime.isAfter(LocalDateTime.now())){
            //缓存未过期,返回店铺信息
            return r;
        }
        //缓存已过期,互斥锁新开线程执行重建缓存,原线程返回过期的数据
        String lockKey =LOCK_SHOP_KEY +id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            //新线程执行重建缓存任务
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存1.查数据库
                    R rdb = dbFallback.apply(id);
                    //重建缓存2.写入redis 带逻辑过期时间
                    this.setWithLogicalExpire(key, rdb, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //返回过期的数据
        return r;
    }

    /**
     * 获取锁:利用redis的setnx 存在则返回false的特性 达到互斥
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁 :删除redis 对应的键
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
}

 //引入CacheClient
 @Resource
 private CacheClient cacheClient;

 

posted @   dream_of_freedom  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示