Redis 性能调优——缓存设计优化
Posted 一叶知秋V
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 性能调优——缓存设计优化相关的知识,希望对你有一定的参考价值。
Redis 是一个开源的高性能的 Key-Value 服务器。本篇主要介绍一下缓存的设计与优化。
1. 缓存的受益与成本
- | 说明 |
---|---|
缓存的受益 | 1、加速读写,通过缓存加速读写速度,例如 CPU L1/L2/L3 Cache、Linux page Cache 加速硬盘读写、浏览器缓存、Ehcache 缓存数据库结果; 2、降低后端负载,后端服务器通过前端缓存降低负载,业务端使用 Redis 降低后端 mysql 负载等。 |
缓存的成本 | 1、数据不一致,缓存和数据层有时间窗口不一致,和更新策略有关; 2、代码维护成本增加,多了一层缓存逻辑; 3、运维成本增加。 |
缓存的使用场景:
- 降低后端负载,对高消耗的 SQL,例如 join 结果集/分组统计结果缓存;
- 加速请求响应,利用 Redis/Memcache 优化 IO 响应时间;
- 大量写合并为批量写,例如计数器先 Redis 累加再批量写 DB。
2.单线程架构
Redis 在一个同一时间点只会执行一条命令。
大多情况下,单线程是非常慢的。Redis 单线程架构为什么这么快?
- 主要原因:纯内存;
- 非阻塞 IO,Redis 使用 Event Loop 这样的模型作为 IO 多路复用的实现,并且 Redis 自身实现了一个事件处理,将 Event Loop 连接、读写、关闭转换为自身的一个事件,不再往 IO 上浪费过多时间;
- 避免线程切换和竞态消耗;
单线程架构要注意什么?
- 一次只运行一条命令;
- 拒绝长(慢)命令,例如 keys、flushall、flushdb、slow lua scrip、mutil/exec、operate big value(collection);
2.缓存更新策略
策略 | 说明 | 一致性 | 维护成本 |
---|---|---|---|
LRU/LFU/FIFO 算法剔除 | 例如 maxmemory-policy | 最差 | 低 |
超时剔除 | 例如 expire | 较差 | 低 |
主动更新 | 开发控制生命周期 | 强 | 高 |
两条建议:
低一致性:推荐最大内存和淘汰策略;
高一致性:推荐超时剔除和主动更新结合,超时剔除是给主动更新做了一个兜底,还需要最大内存和淘汰策略二次兜底。
3.缓存粒度控制
从 MySQL 获取用户信息:select * from user where id = id
设置用户信息缓存:set user:id ‘select * from user where id = id’
缓存粒度:
- 全部属性:set user:id ‘select * from user where id = id’
- 部分重要属性:set user:id ‘select importantColumn1, …importantColumnK from user where id = id’
缓存粒度控制的三个角度:
通用性:全部属性更好;
占用空间:部分重要属性更好;
代码维护:表面上全部属性更好,增删字段不需要维护代码。
4.缓存穿透优化
缓存穿透问题,大量请求不命中?
发生缓存穿透的常见原因:
- 业务代码自身问题;
- 恶意攻击、爬虫等等。
如何发现问题?
- 业务的响应时间;
- 业务本身问题;
- 相关监控指标:总调用数、缓存层命中数、存储层命中数;
缓存穿透问题解决方案:
方案一:缓存空对象。示例代码:
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;
方案二:布隆过滤器拦截。通过很小的内存来实现对数据的过滤。
5.缓存雪崩优化
缓存雪崩:由于 cache 服务承载大量请求,当 cache 服务异常/脱机后,流量直接压向后端组件(例如 DB),造成级联故障。
缓存雪崩优化方案:
- 保证缓存高可用性,例如 Redis Cluster、Redis Sentinel、VIP;
- 依赖隔离组件为后端限流;
- 提前演练,例如压力测试。
6.无底洞问题优化
无底洞问题:增加机器性能没能提升,反而下降。问题关键点就是批量操作的链化,例如 mget 操作,时间复杂度为 O(node),随着机器的增加,mget 批量操作的时间会越长,更多的机器不代表更多的性能。
但是随着数据增长,水平扩展是必须的。
优化 IO 的几种方法:
- 命令本身优化,例如慢查询 keys、hgetall bigkey;
- 减少网络通信次数;
- 降低接入成本,例如客户端使用长连接/连接池、NIO 等 。
7.热点key优化
发现热点key:
方法一:客户端,可以使用 Guava 的 AtomicLongMap,记录 key 的调用次数:
public static final AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
String get(String key)
counterKey(key);
...
String set(String key, String value)
counterKey(key);
...
方法二:代理端
客户端和 Redis 中间加一个代理进行收集统计。
方法三:服务端
使用 monitor 解析,输出统计。
方法四:机器收集
抓取分析 Redis 所在机器的 TPC 数据。
四种方式对比:
方案 | 优点 | 缺点 |
---|---|---|
客户端 | 1、实现简单; | 1、内存泄露隐患,如果 key 量太大不建议使用; 2、维护成本高; 3、只能统计单个客户端; |
代理端 | 1、代理是客户端和服务端的桥梁,实现最方便最系统; | 1、增加代理端的开发部署成本; |
服务端 | 1、实现简单; | 1、monitor 本身的使用成本和危害,只能短时间使用; 2、只能统计单个 Redis 节点; |
机器收集 | 1、对于客户端和服务端无侵入和影响; | 1、需要专业的运维团队开发,并且增加了机器的部署成本; |
优化方案:
- 避免 bigkey;
- 热键不要用 hash_tag,因为 hash_tag 会落到一个节点上;
- 如果真有热点 key 而且业务对一致性要求不高时,可以用本地缓存 + MQ 解决。
8.热点key重建优化
问题:热点 key + 较长的重建时间。
获取缓存 -> 查询数据源 -> 重建缓存 -> 输出,这个步骤在高并发的情况下,由于查询数据源需要时间,所以会有很多请求会进入到 查询数据源 -> 重建缓存 这个过程。对数据源会造成很大压力,响应时间也会变慢。
三个优化目标:
- 减少重建缓存的次数;
- 数据尽可能一致;
- 减少潜在风险。
两个优化方案:
- 互斥锁(mutex key),查询数据源 -> 重建缓存 这个过程加互斥锁;
- 永不过期,缓存层面不设置过期时间(没有用 expire),功能层面为每个 value 添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
两个优化方案的对比:
策略 | 优点 | 缺点 |
---|---|---|
互斥锁 | 思路简单,保证一致性 | 代码复杂度增加,存在死锁的风险 |
永不过期 | 基本杜绝热点 key 重建问题 | 不保证一致性,逻辑过期时间增加维护成本和内存成本 |
9.总结
- 缓存收益:加速读写、降低后端存储负载;
- 缓存成本:缓存和存储数据不一致性、代码维护成本、运维成本;
- 推荐结合剔除、超时、主动更新三种方案共同完成;
- 穿透问题:使用缓存空对象和布隆过滤器来解决,注意它们各自的使用场景和局限性;
- 无底洞问题:分布式缓存中,有更多的机器不保证有更高的性能。有四种批量操作方式:串行命令、串行 IO、并行 IO、hash_tag;
- 雪崩问题:缓存层高可用、客户端降级、提前演练是解决雪崩问题的重要方法;
- 热点 key 重建问题:互斥锁、永不过期能够在一定程度上解决热点 key 问题,开发人员在使用时要了解它们各自的使用成本。
以上是关于Redis 性能调优——缓存设计优化的主要内容,如果未能解决你的问题,请参考以下文章