问题/危害
- 当前 Key 为热点 key(如:热门的娱乐资讯),并发量非常大
- 重建缓存不能有效即时完成,如:一个复杂的运算,查询等;当缓存失效,大量请求来重建缓存,造成后端负载大
优化目标
- 减少重建缓存的次数
- 数据尽可能一致
- 较少的潜在危险
解决方案
- 互斥锁(mutex key)
只允许一个线程重建缓存,其他线程等待。如下为 Java 方式实现互斥锁思路:
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;
}
- 数据永不过期,两层意思:
- 从缓存层面看,不设置过期时间
- 从功能层面看,设置过期时间,当发现超过过期时间,会使用单独线程进行重建缓存
问题:可能存在缓存数据与数据库数据不一致的情况,看是否容忍。
- 热点 key 进行拆分多 key
基于如 Redis Cluster 的哈希槽概念,拆分多 key ,尽可能的让更多的节点承担缓存层的流量负载。
对比
使用缓存时有三个目标:
- 加快用户访问速度,提高用户体验
- 降低后端负载,减少潜在风险,保证系统稳健
- 保证数据“尽可能”的及时更新
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁(分布式锁) | - 思路简单 - 保证一致性 |
- 代码复杂度大 - 存在死锁风险 - 存在线程阻塞的风险 |
“永不过期” | - 基本杜绝热点 key 情况 |
- 不保证一致性 - 逻辑过期时间增加代码维护成本和内存成本 |
key 拆分 | - 提升热点 key 性能 |
- 代码复杂度大 - 增加多 key 维护成本 |