Redis缓存失效和缓存雪崩以及热点缓存key重建优化

缓存失效(击穿)

缓存失效也称缓存击穿,是由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。

示例伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//设置一个过期时间(300到600之间的一个随机数)
int expireTime = new Random().nextInt(300) + 300;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}

缓存雪崩

缓存雪崩指的是缓存层支撑不住或者宕机后,流量会全部打向存储层。
由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降),于是大量请求都会打到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

预防和解决缓存雪崩问题,可以从以下三个方面着手解决:

  1. 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。

  2. 依赖隔离组件为后端限流熔断并降级。比如使用阿里开源流控组件Sentinel或Hystrix限流降级组件

    比如服务降级,我们可以针对不同的数据采用不同的处理方式,当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。

  3. 提前演练。在项目上线前,演练缓存层宕机后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

热点缓存key重建优化

在使用“缓存+过期时间”的策略时,既可以加速数据读写,又可以保证数据定期更新,这种模式基本上可以满足绝大部分需求,但是相应的也会出现两个问题,可能对应用造成致命bug:

  • 当前key是一个热点key(例如一个热门的新闻),并发量非常大。
  • 重建缓存不能在短时间内完成,可能是一个复杂的计算,例如复杂的SQL、多次I/O,多个依赖等。

在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。要解决这个问题主要就是避免大量线程同时重建缓存。

我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。

示例伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
String get(String key) {
// 从Redis中获取数据
String value = redis.get(key);
// 如果value为空, 则开始重构缓存
if (value == null) {
// 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis, 并设置过期时间
redis.setex(key, timeout, value);
// 删除key_mutex
redis.delete(mutexKey);
}// 其他线程休息50毫秒后重试
else {
Thread.sleep(50);
get(key);
}
}
return value;
}

缓存与数据库库双写不一致

在大并发下,同时操作数据库与缓存会存在数据不一致性问题

  1. 双写不一致情况

    缓存&数据库双写不一致

  2. 读写并发不一致

    读写并发不一致

解决方案:

  1. 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

  2. 就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

  3. 如果不能容忍缓存不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁

  4. 也可以用阿里开源的canal通过监控数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

    双写不一致解决方案

总结:

以上针对的都是读多写少的情况加入缓存提高性能,如果是写多读多的情况又不能容忍缓存数据不一致,那就没有必要加缓存了,可以直接操作数据库。放入缓存的数据应该是对实时性、一致性要求不是很高的数据。切记不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统的复杂性!

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信