【Redis】缓存(下)

经过缓存这篇文章的概述,已经对缓存有了初步的了解和认知。在本篇文章中,主要是通过代码来实现缓存的应用,以及在使用缓存过程中出现的经典问题。

简单应用

需求:根据菜品id来查询缓存

流程:① 从缓存中查询,如果菜品存在,那么直接返回;② 缓存中不存在,从数据库中查询;③ 如果存在,那么写入缓存并返回;④ 不存在就直接返回一个null。

@Service
public class DishService extends ServiceImpl<DishMapper, Dish> {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public Dish queryDishById(Long id) {
        // TODO 在Redis中查询该菜品是否存在
        String dishJson = this.stringRedisTemplate.opsForValue().get(Constants.DISH_KRY + id);
        // TODO 判断是否存在
        if(StrUtil.isNotBlank(dishJson)) { // StrUtil.isNotBlank() 表示不为null,不为空字符串,不为不可见字符
            // 存在直接返回
            return JSONUtil.toBean(dishJson, Dish.class);
        }
        // TODO 不存在查询数据库
        Dish dish = this.getById(id);
        // TODO 数据库也不存在就直接返回,看业务需求
        if(dish == null) {
            return null;
        }
        // TODO 数据库存在就写入缓存
        this.stringRedisTemplate.opsForValue().set(Constants.DISH_KRY + id, JSONUtil.toJsonStr(dish));
        return dish;
    }

}

缓存预热

设想某个服务运行很长时间之后,为了优化该服务,将Redis接入服务之中。此时,Redis服务器之中是没有任何数据的。因此服务都会跳过Redis直接打入到MySQL之中。随着时间的积累,Redis上积累足够多的数据之后,MySQL的压力才会减少。

缓存预热就是把定期生产和实时生成两种更新策略进行结合,当Redis未接入系统之前,就先统计一些热点数据,将其导入到Redis中,然后再把Reids服务给接入到整个系统之中。通过这种方式,即使Redis之中的数据不是最热的,但是也可以减少很大一部分MySQL的压力。随着时间的推移,Redis之中旧的热点数据就会被淘汰,新的热点数据就会生成。

缓存穿透

客户端发来的查询请求,先会在Redis之中进行查询。如果发现没有,就会去MySQL之中进行查询。如果MySQL之中也没有,并且像这样的请求很多,那么就会给MySQL造成不小的影响。

产生原因

1. 业务设计不合理,比如缺少必要的参数验证,导致非法的key也被删除了。

2. 开发/运维操作错误,不小心把部分数据给删除了。

3. 黑客恶意攻击。

解决方案

1. 当客户端发送的请求在Redis和MySQL都不存在时,仍然写入Redis,但是把value设定成一个非法值。

    优点是实现简单,维护方便。缺点是造成Redis额外的内存消耗(毕竟把没有用的数据给插入到了Reids之中),并且可能造成短期的不一致(例如在Redis中设置了一个非法值,但是此时MySQL更新了数据,这就导致了数据库和Redis的值不一致的情况出现。不过对于这种情况,可以给非法值设置一个过期时间,并且在更新数据库的同时把Reids之中的数据给删除,等到再次查询时再写入缓存)。

2. 引入布隆过滤器:每次查询Redis之前,都先判断一下key是否再布隆过滤器中存在。所谓的布隆过滤器,就是使用hash + bitmap的思想,因此可以使用比较小的空间开销,以及比较快的速度,来判断某个key是否存在于布隆过滤器之中。对于该业务背景来说,就是把所有的key插入布隆过滤器之中,然后进行查询时,先判断是否存在于布隆过滤器之中即可。

    优点是内存占用较少,不存在多余的key。缺点则是实现复杂,并且存在误判的可能。

简单案例

该代码案例中,使用的是第一种解决方法,把key写入到缓存中,并给value设定一个非法值。

public Dish queryDishByIdOfCachePenetration(Long id) {
        // TODO 在Redis之中查询该菜品是否存在
        String dishJson = this.stringRedisTemplate.opsForValue().get(Constants.DISH_KRY + id);
        // TODO 判断是否存在
        if(StrUtil.isNotBlank(dishJson)) {
            // 菜品存在,直接返回缓存信息
            return JSONUtil.toBean(dishJson, Dish.class);
        }
        // TODO 判断获取到的是否是非法值,从而解决缓存穿透问题
        if(dishJson != null) {
            // 由于在isNotBlank方法之中已经判断其不为null,不为空字符串,不为不可见字符进而明白其是菜品信息
            // 因此,当走到这一步并且不为null时,就可以判断,其是非法值
            return null;
        }
        // TODO 从数据库之中获取菜品
        Dish dish = this.getById(id);
        // TODO 判断数据库返回的是否是空值
        if(dish == null) {
            // 数据库之中菜品也为空时,表示该id是一个非法值,存到Redis之中,并返回
            this.stringRedisTemplate.opsForValue().set(Constants.DISH_KRY + id, "", 2, TimeUnit.MINUTES);
            return null;
        }
        // 数据库不为空时,将其写入缓存之中,返回菜品内容
        this.stringRedisTemplate.opsForValue().set(Constants.DISH_KRY + id, JSONUtil.toJsonStr(dish), 30, TimeUnit.SECONDS);
        return dish;
    }

 缓存雪崩

缓存雪崩是指在短时间内,Redis服务上的key大规模失效或Redis服务宕机,导致缓存命中率陡然下降。这就肯定会使得MySQL压力陡然上升,甚至直接宕机。

产生原因

1. Redis服务宕机了

2. Redis服务中大量的key到达过期时间,然后失效了

解决方案

1. 加强监控报警,从而增加Redis服务的可用性。

2. 给key设置过期时间时添加随机因子,从而避免同一时期过期。

3. 利用Redis集群提高服务的可用性。

4. 给缓存业务添加降级限流策略。

5. 给业务添加多级缓存。

缓存击穿

缓存击穿问题也称作热点key问题,即一个被大量请求访问且缓存重建业务较复杂的key突然失效了,导致无数的请求直接打到数据库之中,在瞬间给与了数据巨大的冲击。

解决方案

1. 基于统计的方式发现热点key,并设置永不过期。

2. 使用互斥锁的方式来进行查询。即当查询缓存未命中时,先获取互斥锁,然后再去访问数据库进行重建缓存。这样就不会造成所有的请求都直接打到数据库之中,从而避免了数据库的宕机。

    优点是没有额外的内存消耗,保证一致性,实现简单。缺点是线程需要额外等待,性能受到影响,同时存在死锁的情况。

3. 采用逻辑过期的方式来进行查询。即当查询缓存时,发现已经到了逻辑过期时间之后,这时就先获取互斥锁,然后开启新的线程去进行缓存重建并重新设置过期时间。在缓存重建的过程中,有其他请求来访问时,就返回已经过期的数据。

    优点是线程无需等待,性能较好。缺点是不保证一致性,有额外的内存消耗,实现复杂。

简单案例 

使用互斥锁来解决

    /**
     * 解决缓存击穿的案例
     * 分布式锁
     */
    public Dish queryDishByIdOfCacheBreakdownLock(Long id) throws InterruptedException {
        // TODO 设置key值
        String key = Constants.DISH_KRY + id;
        // TODO 从Redis中查询菜品
        String dishJson = this.stringRedisTemplate.opsForValue().get(key);
        // TODO 判断是否存在
        if(StrUtil.isNotBlank(dishJson)) {
            // TODO 存在就返回
            return JSONUtil.toBean(dishJson, Dish.class);
        }
        // TODO 不存在,先判断是否是非法值,解决缓存穿透问题
        if(dishJson != null) {
            // TODO 是非法值,直接返回
            return null;
        }
        Dish dish = null;
        String lockKey = Constants.LOCK_DISH_KEY + id;
        try {
            // TODO 解决缓存击穿,实现缓存重建
            // TODO 获取锁
            if(!this.tryLock(lockKey)) {
                // TODO 获取锁失败就休眠一段时间,然后递归重新获取数据
                Thread.sleep(50);
                return this.queryDishByIdOfCachePenetration(id);
            }
            // TODO 从数据库中查询
            dish = this.getById(id);
            // TODO 判断数据库有没有该菜品
            if(dish == null) {
                // TODO 数据库也不存在,解决缓存穿透问题
                this.stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
                return null;
            }
            // TODO 数据库存在,写入Redis
            this.stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(dish), 30, TimeUnit.SECONDS);
        } finally {
            // TODO 无论结果如何,释放锁,防止造成死锁问题
            this.unLock(lockKey);
        }
        return dish;
    }

    /**
     * 获取锁
     */
    private boolean tryLock(String key) {
        // 设置过期时间是防止死锁
        Boolean absent = this.stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        if(absent == null) {
            absent = false;
        }
        return absent;
    }

    /**
     * 释放锁
     */
    private void unLock(String key) {
        this.stringRedisTemplate.delete(key);
    }

使用逻辑过期来解决

    /**
     * 获取锁
     */
    private boolean tryLock(String key) {
        // 设置过期时间是防止死锁
        Boolean absent = this.stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        if(absent == null) {
            absent = false;
        }
        return absent;
    }

    /**
     * 释放锁
     */
    private void unLock(String key) {
        this.stringRedisTemplate.delete(key);
    }

    @Data
    public class RedisData {
        private Object data; // 数据
        private LocalDateTime expireTime; // 过期时间
    }

    /**
     * 解决缓存击穿的案例
     * 逻辑过期
     */
    public Dish queryDishByIdOfCacheBreakdownLogicalExpiration(Long id) {
        // TODO 设置key值
        String key = Constants.DISH_KRY + id;
        // TODO 从Redis中查询菜品
        String redisDataJson = this.stringRedisTemplate.opsForValue().get(key);
        // TODO 判断查询结果
        if(StrUtil.isBlank(redisDataJson)) {
            // TODO 没有查到缓存,直接返回空
            return null;
        }
        // TODO 将数据格式还原
        RedisData redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Dish dish = JSONUtil.toBean(data, Dish.class);
        // TODO 判断过期时间
        if(redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            // TODO 没有过期,直接返回
            return dish;
        }
        // TODO 过期,实现缓存重建,解决缓存击穿问题
        // TODO 获取锁
        String lockKey = Constants.LOCK_DISH_KEY + id;
        if(this.tryLock(lockKey)) {
            // TODO 获取锁成功,进行缓存重建
            Thread thread = new Thread(() -> {
                // TODO 从数据库中查询
                Dish dishDB = this.getById(id);
                // TODO 封装数据
                RedisData dataDB = new RedisData();
                dataDB.setData(dishDB);
                dataDB.setExpireTime(LocalDateTime.now().plusSeconds(30));
                // TODO 存入数据库中
                this.stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
                this.unLock(lockKey);
            });
            thread.start();

        }
        return dish;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王彬泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值