Redis 缓存击穿

作者: 白花蛇草可乐 | 来源:发表于2019-12-18 23:47 被阅读0次

    1、缓存击穿的概念以及原因

    给缓存中的数据添加过期时间,既可以加速数据读写,又能够保证数据定期更新。

    但是在一些场景下数据过期会给系统造成重大伤害:

    • 条件1:该数据为热点内容,并发读取量非常大。

    • 条件2:重建缓存无法在短期内完成。比如重新计算/获取该数据是一个复杂的过程,涉及到复杂SQL、耗时的各种IO或者依赖很多个其他三方服务等等。

    在热点数据过期的瞬间,大量并发的请求来获取数据,获取不到以后,会引发大量的重建缓存的动作,造成存储层瞬时负载激增,系统崩溃。

    缓存击穿示意图

    2、预防缓存击穿的思路

    尽最大可能减少重建缓存的次数,将大量请求隔离在存储层之外。

    3、解决方案一:互斥锁 (mutex key)

    此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完成后,才可以从缓存获取数据。

    3-1、具体做法

    流程以及伪代码如下:

    解决缓存击穿—互斥锁
    String value = redis.get(热点数据_key);
    
    // value为空的话表明缓存已过期
    if (value == null) { 
    
        // 尝试获取分布式锁。注意锁本身要加过期时间,否则一旦出现各种不可预料的异常,锁会长期存在
        if ("OK".equals(jedis.set(互斥锁_key, uniqueId, "NX", "EX", 互斥锁_expire_secs))) {
            value = 重建缓存;
            redis.set(热点数据_key, value, 热点数据_expire_secs);
            // 锁解除(删除互斥锁)
            if (uniqueId.equals(jedis.get(互斥锁_key))) {
                jedis.del(互斥锁_key);
            }        
        } else {
            // 获取锁失败,说明已经有其他线程在重建缓存
            线程等待 & 重试;
        }
    
    } else {
        return value;
    }
    

    注意一下加锁和释放锁时的正确操作姿势。

    加锁时,Redis 2.6.12 以后的版本直接使用set命令即可:

    SET key value [EX seconds][PX milliseconds][NX|XX]

    • EX seconds:设置指定的过期时间,单位秒。
    • PX milliseconds:设置指定的过期时间,单位毫秒。
    • NX:仅当key不存在时设置值。
    • XX:仅当key存在时设置值。

    释放锁时,养成判断value的习惯,不怕一万就怕万一,万一这把锁不是你加的呢?所以判断一下锁的value是不是你设定的那个随机数,看看这把锁是不是属于你。

    实际上上面伪代码中释放锁的姿势也不是非常正确,毕竟get和del是两个操作,不像set一样具备原子性。如果追求完美的话,可以写lua脚本:

    /**
     * 释放分布式锁
     * @param key
     * @param uniqueId
     */
    public static boolean releaseLock(String key, String uniqueId) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(uniqueId)).equals(1L);
    }
    

    3-2、风险

    使用互斥锁来阻塞读的方案,思路简单实现容易,但是存在一定的隐患。

    由于它会阻塞其他的线程,会使得系统吞吐量会下降,需要结合实际的业务去考虑是否要这么做。

    另外如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,反而拖累系统的整体表现。

    4、解决方案二:只做逻辑过期

    Redis 层面,热点数据不再设置过期时间,永远有效,这样绝对不会出现缓存击穿的现象,所有的访问可以随时获取到结果。

    从业务功能层面,需要单独管理每一个热点数据需要刷新的时间,也就是原来的数据过期时间,利用批处理等手段(或者守护线程),在这个“逻辑过期”时间到了以后,去重新构建缓存中的热点数据。

    有一些解决方案中提到设定一个缓存过期时间,一个数据过期时间,实际上跟这种“永不过期”的方案思路一致,但是实现反而更复杂了,可靠性也有所降低。

    5、一个小的处理技巧

    给热点数据设定过期时间的时候,增减一个小的随机数,可以防止大量热点数据同时失效,让它们的失效时间相互错开。

    比如

    5s(过期时间) ± 0.076s(随机数)

    相关文章

      网友评论

        本文标题:Redis 缓存击穿

        本文链接:https://www.haomeiwen.com/subject/crmpnctx.html