文章目录

  • 1、缓存击穿的概念以及原因
  • 2、预防缓存击穿的思路
  • 3、解决方案一:互斥锁 (mutex key)
  • 3-1、具体做法
  • 3-2、风险
  • 4、解决方案二:只做逻辑过期
  • 5、一个小的处理技巧


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

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

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

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

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

redis 缓存热点数据 redis热点缓存重建_redis

2、预防缓存击穿的思路

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

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

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

3-1、具体做法

流程以及伪代码如下:

redis 缓存热点数据 redis热点缓存重建_缓存_02

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(随机数)