Redis 是一个开源的高性能的 Key-Value 服务器。本篇主要介绍一下缓存的设计与优化。
缓存收益与成本
- | 说明 |
---|---|
缓存的受益 | 1、加速读写,通过缓存加速读写速度,例如 CPU L1/L2/L3 Cache、Linux page Cache 加速硬盘读写、浏览器缓存、Ehcache 缓存数据库结果; 2、降低后端负载,后端服务器通过前端缓存降低负载,业务端使用 Redis 降低后端 MySQL 负载等。 |
缓存的成本 | 1、数据不一致,缓存和数据层有时间窗口不一致,和更新策略有关; 2、代码维护成本增加,多了一层缓存逻辑; 3、运维成本增加。 |
单线程架构
单线程架构要注意什么?
- 一次只运行一条命令;单条命令都具有原子性
- 拒绝长(慢)命令,例如 keys、flushall、flushdb、slow lua scrip、mutil/exec、operate big value(collection);
缓存一致性
缓存更新策略:
1.先更新数据库,再删除缓存(推荐)
可能存在的问题
- 缓存更新失败
如何解决?
引入消息队列,删除缓存失败不断重试,缺点是,对业务代码大量的侵入。
具体流程:
- 更新数据库数据
- 缓存因为种种问题删除失败
- 将需要删除的key发送至消息队列
- 自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功
理论上存在并发问题脏数据问题(一个请求A做查询操作,一个请求B做更新操作),但很难出现。
出现并发问题脏数据问题的场景:
- 缓存刚好失效
- 请求A查询数据库,得一个旧值
- 请求B将新值写入数据库
- 请求B删除缓存
- 请求A将查到的旧值写入缓存
发生条件:步骤3的写数据库操作比步骤2的读数据库操作耗时更短(很难出现)
2.先删缓存,在更新数据库
可能存在的问题?
- 数据不一致(同时有一个请求A进行更新操作,另一个请求B进行查询操作)
如何解决?
延时双删策略:
- 先删除缓存
- 再写数据库
- 休眠1秒,再次删除缓存(具体根据业务确实时间)
理论上依然存在极短时间窗口的数据不一致。且同时存在和方案1一样的缓存更新失败问题
缓存穿透优化
缓存穿透问题,大量请求不命中?
发生缓存穿透的常见原因:
- 业务代码自身问题;
- 恶意攻击、爬虫等等。
如何发现问题?
- 业务的响应时间;
- 业务本身问题;
- 相关监控指标:总调用数、缓存层命中数、存储层命中数;
解决方案:
方案一:缓存空对象。示例代码:
public String getPassThrough(String key) {
String cacheValue = cache.get(key);
if (StringUtils.isBlank(cacheValue)) {
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置过期时间
if (StringUtils.isBlank(storageValue)) {
cache.expire(key, 300); // 300秒
}
return storageValue;
} else {
return cacheValue;
}
}
方案二:布隆过滤器拦截。通过很小的内存来实现对数据的过滤。
缓存雪崩优化
缓存雪崩:由于 cache 服务承载大量请求,当 cache 服务异常/脱机后,流量直接压向后端组件(例如 DB),造成级联故障。
缓存雪崩优化方案:
- 保证缓存高可用性,例如 Redis Cluster、Redis Sentinel、VIP;
- 依赖隔离组件为后端限流;
- 提前演练,例如压力测试。
热点key优化
优化方案:
- 避免 bigkey。命名上做规范, 比如key通常是要区分服务的 很多人的做法就是把服务名自己做为了前缀,通过公共枚举去定义key中可能存在的服务名 业务名等等。
- 热键不要用 hash_tag,因为 hash_tag 会落到一个节点上。
- 如果真有热点 key 而且业务对一致性要求不高时,可以用本地缓存 + MQ 解决。
热点key重建优化
问题:热点 key + 较长的重建时间。
获取缓存 -> 查询数据源 -> 重建缓存 -> 输出,这个步骤在高并发的情况下,由于查询数据源需要时间,所以会有很多请求会进入到 查询数据源 -> 重建缓存 这个过程。对数据源会造成很大压力,响应时间也会变慢。
优化目标:
- 减少重建缓存的次数;
- 数据尽可能一致;
两个优化方案:
- 互斥锁(mutex key),查询数据源 -> 重建缓存 这个过程加互斥锁;
- 永不过期,缓存层面不设置过期时间(没有用 expire),功能层面为每个 value 添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
两个优化方案的对比:
策略 | 优点 | 缺点 |
---|---|---|
互斥锁 | 思路简单,保证一致性 | 代码复杂度增加,存在死锁的风险 |
永不过期 | 基本杜绝热点 key 重建问题 | 不保证一致性,逻辑过期时间增加维护成本和内存成本 |
转载:https://blog.csdn.net/lwl2014100338/article/details/107876741
查看评论