优化指南 - 使用
操作
批量操作
传统数据库也存在批量操作效率高于单次操作的情况,但是 redis 由于执行效率更高,批量操作带来的提升也更夸张。举个不是很恰当的例子,还是按 redis 每秒能处理 10k 请求来算——
假设客户端和服务端不在同一机器,网络通信存在额外 1ms 延时:
操作 | 时间 |
---|---|
1000 次 get | 1000 1 + 1000 0.01 = 1010 (ms) |
10 次 100 个键值对的 mget | 10 (1 1 + 100 * 0.01) = 20 (ms) |
1 次 1000 个键值对的 mget | 1 1 + 1000 0.01 = 11 (ms) |
如果一条条去执行,这时 redis 每秒只能处理 1000 / 1.01 ≈ 990 次请求,只发挥了实际计算力的 0.99%。
Multi-action vs Pipeline vs Transaction
批量操作有 3 种实现方式:
Multi-action
也就是 m 开头的命令,比如 mget
优点:
- 效率是最高的,因为它只需要解析一条命令
缺点:
- 只能做一件事
- 一次操作的 key 太多的话会导致 redis 实例的响应能力等比下降
- 不具有原子性,存在部分成功部分失败的情况
Pipeline
管道式的操作
优点:
- 可以处理多类数据
- 可以将大量命令分解为多个包依次发送执行
- 使用灵活
缺点:
- 不保证事务,其他客户端发送的命令可能在 pipeline 执行期间被执行
- 不具有原子性,存在部分成功部分失败的情况
Transaction
事务
优点:
- 原子性,要么全执行要么不执行
- 乐观锁
缺点:
- 命令会被分批发送给服务端,最后统一执行,性能是最低的 (但还是远高于执行 n 次命令)
- 随着竞争激烈程度的上升,乐观锁会导致性能相应下降
- 在集群中,只有同属于一个哈希槽的键才能使用事务,多数客户端支持的不好
另外,因为 Pipeline 是基于 redis 自定义的 RESP 协议实现的,而 Transaction 是命令实现,所以给了我们组合使用的机会。相比直接使用事务会快上一点点,没有太大区别。
总的来说,实现批量执行的核心肯定是 Pipeline,请尽可能的使用。
放上一组官方测试结果以供参考:
# Intel(R) Xeon(R) CPU E5520 @ 2.27GHz (with pipelining)
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -P 16 -q
> SET: 552028.75 requests per second
> GET: 707463.75 requests per second
> LPUSH: 767459.75 requests per second
> LPOP: 770119.38 requests per second
# Intel(R) Xeon(R) CPU E5520 @ 2.27GHz (without pipelining)
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q
> SET: 122556.53 requests per second
> GET: 123601.76 requests per second
> LPUSH: 136752.14 requests per second
> LPOP: 132424.03 requests per second
减少阻塞
另一方面,针对每条命令,由于 redis 是单进程单线程的模式,命令是依次执行的,想象一下星巴克排队,只要有一个客人堵在那,后面的不管买多买少都只能排着…
可能造成阻塞的命令包括:
del
,这个删除是在前台阻塞式的删除,在 redis4.0 以后应该使用unlink
后台非阻塞的标记删除keys
、hgetall
、smembers
,这类返回所有结果的命令都会占用大量资源,都应该用scan
、hscan
、sscan
等命令替换sinter
/sunion
/sdiff
的结果如果会重复使用的话,用sinterstore
/sunionstore
/sdiffstore
将结果保存起来sort
,可以先取到本地再排序- 能用
mget
/mhset
的情况下就不要用get
/set
- 总之,所有时间复杂度大于 O(log n) 的操作都应该考虑有没有更低占用的实现
除了命令本身,造成阻塞的原因还有:
- CPU 饱和:cpu 占用率 100% 了
- CPU 竞争:和其他服务竞争资源
- 持久化带来的 IO 阻塞
- fork 阻塞:rdb/aof 文件重写的时候 fork 出的子进程长时间不能完成,导致的主进程阻塞
- AOF 阻塞:数据变动剧烈的时候 fsync 持续写硬盘导致的
- HugePage 阻塞:如果 linux 内核里启用了
transparent_hugepage
,会对内存和延迟带来很大影响
- 内存交换:物理内存不够用了,部分数据被写到 Linux 的虚拟内存,也就是 swap,但是内存和磁盘的读写速度起码差了 5 个量级
- 网络问题
这些就需要在使用过程中不断监测和发现了。
策略
过期回收
随着时间增长,碎片化的无用 key 的数量也会持续上升,直到最终你的内存被垃圾 Key 占满。 所以一个好习惯是给不需要持久存储 (redis 本身就不是用来持久化的) 的 Key 都加上过期时间。
命令 | 注释 |
---|---|
EXPIRE <KEY> <TTL> |
将键的生存时间设为 ttl 秒 |
PEXPIRE <KEY> <TTL> |
将键的生存时间设为 ttl 毫秒 |
EXPIREAT <KEY> <timestamp> |
将键的过期时间设为 timestamp 所指定的秒数时间戳 |
PEXPIREAT <KEY> <timestamp> |
将键的过期时间设为 timestamp 所指定的毫秒数时间戳. |
但是需要注意,过期键的内存空间默认并不会被立即回收。redis 的内存回收策略主要是这两个:
- 被动删除,读 / 写过期键时触发删除;
- 主动删除,每隔 100ms 检查 20 个带过期时间的键,如果有超过四分之一的键过期,则重复上面步骤;
另外,设置 maxmemory
最大内存,可以在达到内存阈值的时候触发强制删除机制 (配置项 maxmemory-policy
):
- noeviction,禁止强制删除,默认策略;
- volatile-ttl,从带过期时间的键中删除最接近过期的;
- volatile-lru,从带过期时间的键中删除最近最久未使用的 (
Least Recently Used
); - volatile-lfu,从带过期时间的键中删除最近最少使用的 (
Least Frequently Used
); - volatile-random,从带过期时间的键中随机删除;
- allkeys-lru,从所有键中删除最近最久未使用的;
- allkeys-lfu,从所有键中删除最近最少使用的;
- allkeys-random,从带过期时间的键中随机删除;
持久化
redis 数据落到硬盘依赖两种持久化机制:RDB 和 AOF。
RDB | AOF | |
---|---|---|
存储内容 | 数据 | 写操作日志 |
性能影响 | 小 | 大 |
恢复速度 | 高 | 低 |
存储空间 | 小 | 大 |
可读性 | 低 | 高 |
安全程度 | 较低,保存频率低 | 较高,保存频率高 |
默认开启 | 是 | 否 |
存储策略 | save 900 1 :九百秒内一次修改即保存 save 300 10 :三百秒内十次修改即保存 < br>save 60 10000 :六十秒内一万次修改即保存 < br > 允许自定义 |
always :逐条保存 < br>oreverysec :每秒保存 < br>orno :系统自己决定什么时候保存 |
RDB 的 save 策略配合大键有时候简直性能地狱。必要时请重写触发机制。
AOF 的日志文件会膨胀的非常厉害,所以会定期重写。如果文件变动过于剧烈,你会发现 swap 比内存更先被吃干净。
redis4.0 以后支持一个叫 aof-use-rdb-preamble
的参数,意思就是在重写 AOF 文件的时候,会把早期日志写成 RDB 格式,新增加的继续使用 AOF。这样一来可以替高重写和恢复的速度。某种意义上有了这个就不必单独开启 RDB 持久化了。
内存清理
在 redis4.0 之后,可以通过将配置里的 activedefrag
设置为 yes
开启自动清理,或者通过 memory purge
命令手动清理。