Redis笔记(四)

缓存

​ 在项目中,许多地方会用到缓存,尤其是在一些热点模块,例如点评网站中的商铺信息、短信登录的验证码等等。加了缓存后,可以让请求访问到数据库的操作大大减小,从而减轻数据库的压力。并且缓存一般都是在内存当中,因此,其访问速度要远高与传统数据库。而Redis用得最多的地方就是做数据的缓存。下面进行代码演示

给商铺查询添加缓存

  • Controller层代码

     /**
         * 根据id查询商铺信息
         * @param id 商铺id
         * @return 商铺详情数据
         */
        @GetMapping("/{id}")
        public Result queryShopById(@PathVariable("id") Long id) throws Exception {
            Result result = shopService.queryByid(id);
            return result;
        }
    
  • Service层代码

     public Result queryByid(Long id) throws Exception {  
         
         	//先定义存入redis中的key
    		String key = CACHE_SHOP_KEY + id;
            //1.从Redis中查询商铺缓存
            String shopCash = stringRedisTemplate.opsForValue().get(key);
            //2.如果缓存中有直接返回
            if(StrUtil.isNotBlank(shopCash)){
                //2.2存在就直接返回
                Shop shop = mapper.readValue(shopCash,Shop.class);
                return Result.ok(shop);
            }
            //3.缓存中没有的话查数据库
            Shop shop = getById(id);
            //4.数据库为空,返回报错信息
            if(shop == null){
                return Result.fail("没有该商铺信息");
            }
            //5.将对象序列化为String保存到缓存
            String s = mapper.writeValueAsString(shop);
            stringRedisTemplate.opsForValue().set(key,s);
            return Result.ok(shop);
     }
    

    上面的代码就是在第一次查询数据库时,若查到有效信息,就将其存入redis中,这样一来,以后再查这个数据时,会先去redis中查询,若查到的话就直接返回给前端,没查到再去数据库找。


缓存更新策略

​ 缓存更新策略是redis为了节约缓存空间而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis中插入太多数据时,会导致缓存中的数据过多,影响性能以及后续新的需要缓存的数据。因此,redis会对部分数据进行更新,或者说是淘汰。一般来说,缓存更新有以下几种方式

内存淘汰超时剔除主动更新
说明不用自己维护,利用redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存添加过期时间,缓存到期自动剔除一般在服务层编写业务逻辑,每当数据库修改时,更新缓存
一致性一般
维护成本

​ 在不同的业务场景,应该使用不同的淘汰机制。比如:

  • 低一致性的需求:使用内存淘汰机制即可。例如店铺类型的缓存查询
  • 高一致性的需求:主动更新,并以超时剔除方式做兜底处理。例如店铺详情查询的缓存、
数据库与缓存不一致解决方案

一般来说,在需要更新数据库时为了使缓存同步。需要同时更新缓存。在更新缓存的策略上,我们一般先更新数据库,再更新缓存。而在更新缓存的策略中一般会在更新完数据库后再删除当前缓存。这样在下次查询数据库时又会建立新的缓存。这种方式要比修改缓存内容大大减小对缓存的读写操作。

根据以上两点修改代码

  • 给缓存添加超时剔除

     public Result queryByid(Long id) throws Exception {  
         
         	//先定义存入redis中的key
    		String key = CACHE_SHOP_KEY + id;
            //1.从Redis中查询商铺缓存
            String shopCash = stringRedisTemplate.opsForValue().get(key);
            //2.如果缓存中有直接返回
            if(StrUtil.isNotBlank(shopCash)){
                //2.2存在就直接返回
                Shop shop = mapper.readValue(shopCash,Shop.class);
                return Result.ok(shop);
            }
            //3.缓存中没有的话查数据库
            Shop shop = getById(id);
            //4.数据库为空,返回报错信息
            if(shop == null){
                return Result.fail("没有该商铺信息");
            }
            //5.将对象序列化为String保存到缓存
            String s = mapper.writeValueAsString(shop);
            stringRedisTemplate.opsForValue().set(key,s,CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return Result.ok(shop);
     }
    
  • 在修改数据库操作时将缓存删除

        /**
         * 修改缓存
         * @param shop
         * @return
         */
        @Override
        @Transactional
        public Result update(Shop shop) {
            Long id = shop.getId();
            if(id == null){
                return Result.fail("店铺id不能为空");
            }
            //1.更新数据库
            boolean tag = updateById(shop);
            if(!tag){
                return Result.fail("修改商铺数据失败");
            }
            //2.删除缓存
            stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
            return Result.ok();
        }
    

    以上操作数据库的方法是mybatisPlus所提供的。这里加上@Transactional是为了保证更新数据库和删除缓存操作具有原子性。这种方式只适合与单体项目,在分布式系统上,应该采用TCC等分布式事务方案。


解决缓存三大容灾

1.缓存穿透

缓存穿透是指当用户请求一个缓存和数据库中都没有的值时,这样缓存就永远不会生效,所有请求都会直接打到数据库中。常见的解决方式有两种

  • 缓存空对象

    • 优点:实现简单,方便维护
    • 缺点:额外的内存消耗,可能造成短期的不一致
  • 布隆过滤器

    • 优点:内存占用小,没有多余的key
    • 缺点:实现复杂,有误判的可能
  • 使用缓存空对象解决缓存穿透的问题

    /**
         * 解决缓存穿透
         */
        public Shop queryShop(Long id) throws Exception {
            log.info("缓存开始");
            String key = CACHE_SHOP_KEY + id;
            //1.从Redis中查询商铺缓存
            String shopCash = stringRedisTemplate.opsForValue().get(key);
            //2.如果缓存中有直接返回
            if (StrUtil.isNotBlank(shopCash)) {
                //2.2存在就直接返回
                Shop shop = mapper.readValue(shopCash, Shop.class);
                return shop;
            }
            //2.3判断命中的是否是空值
            if (shopCash != null) {
                return null;
            }
            //3.缓存中没有的话查数据库
            Shop shop = getById(id);
            //4.数据库为空,返回报错信息
            if (shop == null) {
                //4.2将空值也存在数据库中,解决缓存穿透问题
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //5.将对象序列化为String保存到缓存
            String s = mapper.writeValueAsString(shop);
            stringRedisTemplate.opsForValue().set(key, s, CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return shop;
        }
    

    在上面的代码中,当数据库查询到的信息为null时。我们在redis中也新增一个键值对将其作为空对象存到缓存中去,下次再进入查询时,若查到缓存中有值并且是空还不为null时就直接return。


2.缓存雪崩

缓存雪崩是指大量缓存在同一时间内失效,导致大量的请求直接打到数据库上,给服务器带来巨大压力。解决方案有以下几种

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

3.缓存穿透

缓存穿透是指热点key失效,无数的请求访问会在瞬间给数据库带来巨大的冲击。常见的解决方法有两种

  • 互斥锁:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响
  • 逻辑过期:线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦

利用互斥锁来解决缓存穿透的问题
利用Redis中的setnx方法,此方法是来创建key,若redis中无当前key则创建,否则无法创建。这一特性刚好符合互斥的效果
加锁方法

    /**
     * 获取互斥锁
     * @param key
     * @return
     */
    private Boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

解锁方法

    /**
     * 释放互斥锁
     * @param key
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}

    /**
     * 解决缓存击穿
     * @param id
     * @return
     * @throws Exception
     */
    public Shop queryShop2(Long id) throws Exception {
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis中查询商铺缓存
        String shopCash = stringRedisTemplate.opsForValue().get(key);
        //2.如果缓存中有直接返回
        if(StrUtil.isNotBlank(shopCash)){
            //2.2存在就直接返回
            Shop shop = mapper.readValue(shopCash,Shop.class);
            return shop;
        }
        //2.3判断命中的是否为空
        if(shopCash != null){
            return null;
        }
        //3.实现缓存重构
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            //3.1获取锁
            Boolean flag = tryLock(lockKey);
            if (!flag) {
                //如果没有拿到互斥锁,休眠一段时间
                Thread.sleep(20);
                return queryShop2(id);
            }
            //3.2缓存中没有的话查数据库
            shop = getById(id);
            //4.数据库为空,返回报错信息
            if (shop == null) {
                //4.2将空值也存在数据库中,解决缓存穿透问题
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //5.将对象序列化为String保存到缓存
            String s = mapper.writeValueAsString(shop);
            stringRedisTemplate.opsForValue().set(key, s, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放互斥锁
            unlock(lockKey);
        }
        //7.返回数据
        return shop;
    }

在上面方法中,当缓存中没有数据时,线程会去先获取锁,没有获取到的话就等待一段时间再去获取。直到拿到锁再去查询数据库并将数据添加到缓存中去。

利用逻辑过期的方式解决缓存穿透

创建一个类,用来当缓存对象

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

创建一个线程池

 //创建线程池,在利用逻辑过期中使用
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

/**
     * 利用逻辑过期解决缓存击穿
     * @param id
     * @return
     * @throws Exception
     */
    public Shop queryShop3(Long id) throws Exception {
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis中查询商铺缓存
        String shopCash = stringRedisTemplate.opsForValue().get(key);
        //2.如果缓存未命中
        if(StrUtil.isBlank(shopCash)){
            return null;
        }
        // 3.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopCash, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断缓存是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺信息
            return shop;
        }
        //已过期
        //3.实现缓存重构
        String lockKey = LOCK_SHOP_KEY + id;
        Boolean isLock = tryLock(lockKey);
        //判断获取锁是否成功
        if(isLock){
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });
        }
        //7.返回过期的数据
        return shop;
    }
/**
 * 进行缓存预热的方法
 * @param id
 * @param expireSeconds
 */
public void saveShop2Redis(Long id,Long expireSeconds){
    //1.店铺查询
    Shop shop = getById(id);
    //2.封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //3.写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

​ 在上面的代码中,当获取到缓存对象后会先取出对象中的object类型的对象并将其序列化为想要的对象。再拿时间和当前时间做比较,若没有过期就返回到前端,否则就开启一个独立的线程去做缓存重构,并将原来的数据返回给前端。这样一来,当缓存重构完成后,缓存的过期时间也一并更新,下一次再进来查询时,就可以获取到未过期的缓存了。


封装解决方法

@Slf4j
@Component
public class CacheClient {
/**
 * 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
 * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
 * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
 * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用互斥锁解决缓存击穿问题
 * 方法5:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
 */
    private final StringRedisTemplate stringRedisTemplate;
    //线程池,在使用逻辑过期解决缓存击穿问题中使用
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

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

    /**
     * 方法1,将对象存到redis中,并设置过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
        log.info("缓存到reids成功");
    }

    /**
     * 方法2,将对象存到redis中,并设置逻辑过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    /**
     * 方法三:使用缓存空值解决缓存穿透问题
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @param <R>
     * @param <ID>
     * @return
     */
    public  <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit
            ){
        String key = keyPrefix + id;
        //1.从Redis中查询商铺缓存
        String shopCash = stringRedisTemplate.opsForValue().get(key);
        //2.如果缓存中有直接返回
        if (StrUtil.isNotBlank(shopCash)) {
            log.info("使用缓存数据");
            //2.2存在就直接返回
            return JSONUtil.toBean(shopCash,type);
        }
        //2.3判断命中的是否是空值
        if (shopCash != null) {
            return null;
        }
        //3.缓存中没有的话查数据库
        R r = dbFallback.apply(id);
        //4.数据库为空,返回报错信息
        if (r  == null) {
            //4.2将空值也存在数据库中,解决缓存穿透问题
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //5.将对象序列化为String保存到缓存
       this.set(key,r,time,unit);

        return r;
    }

    /**
     * 方法4,利用互斥锁解决缓存击穿
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R,ID> R  queryWithMutex(String keyPrefix, ID id, Class<R> type,Function<ID,R> dbFallback,
                                    Long time,TimeUnit unit){
        String key = keyPrefix + id;
        //1.从Redis中查询商铺缓存
        String shopCash = stringRedisTemplate.opsForValue().get(key);
        //2.如果缓存中有直接返回
        if(StrUtil.isNotBlank(shopCash)){
            //2.2存在就直接返回
            return JSONUtil.toBean(shopCash,type);
        }
        //2.3判断命中的是否为空
        if(shopCash != null){
            return null;
        }
        //3.实现缓存重构
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            //3.1获取锁
            Boolean flag = tryLock(lockKey);
            if (!flag) {
                //如果没有拿到互斥锁,休眠一段时间
                Thread.sleep(20);
                return queryWithMutex(key,id,type,dbFallback,time,unit);
            }
            //3.2缓存中没有的话查数据库
            r  = dbFallback.apply(id);
            //4.数据库为空,返回报错信息
            if (r  == null) {
                //4.2将空值也存在数据库中,解决缓存穿透问题
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //5.将对象序列化为String保存到缓存
                this.set(key,r,time,unit);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放互斥锁
            unlock(lockKey);
        }
        //7.返回数据
        return r;
    }

    /**
     * 方法5,利用逻辑过期解决缓存击穿
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R,ID>R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,
                                          Long time,TimeUnit unit){
            String key = keyPrefix + id;
            //1.从Redis中查询商铺缓存
            String shopCash = stringRedisTemplate.opsForValue().get(key);
            //2.如果缓存未命中
            if(StrUtil.isBlank(shopCash)){
                return null;
            }
            // 3.命中,需要先把json反序列化为对象

            RedisData redisData = JSONUtil.toBean(shopCash, RedisData.class);
            R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
            LocalDateTime expireTime = redisData.getExpireTime();
            //判断缓存是否过期
            if(expireTime.isAfter(LocalDateTime.now())){
                //未过期,直接返回店铺信息
                return r;
            }
            //已过期
            //3.实现缓存重构
            log.info("缓存已过期");
            String lockKey = LOCK_SHOP_KEY + id;
            Boolean isLock = tryLock(lockKey);
            //判断获取锁是否成功
            if(isLock){
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    try {
                        //查询数据库
                        R nweR = dbFallback.apply(id);
                        //重建缓存
                        this.setWithLogicalExpire(key,nweR,time,unit);
                        log.info("缓存重建成功");
                    }catch (Exception e){
                        throw new RuntimeException(e);
                    }finally {
                        unlock(lockKey);
                    }
                });
            }
            //7.返回过期的数据
            return r;
        }

    /**
     * 获取互斥锁
     * @param key
     * @return
     */
    private Boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放互斥锁
     * @param key
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

}

​ 主要就是利用泛型的特性来做返回参数和传入参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值