优化指南 - 使用

操作

批量操作

传统数据库也存在批量操作效率高于单次操作的情况,但是 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 后台非阻塞的标记删除
  • keyshgetallsmembers,这类返回所有结果的命令都会占用大量资源,都应该用 scanhscansscan 等命令替换
  • 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>or
everysec:每秒保存 < br>or
no:系统自己决定什么时候保存

RDB 的 save 策略配合大键有时候简直性能地狱。必要时请重写触发机制。

AOF 的日志文件会膨胀的非常厉害,所以会定期重写。如果文件变动过于剧烈,你会发现 swap 比内存更先被吃干净。

redis4.0 以后支持一个叫 aof-use-rdb-preamble 的参数,意思就是在重写 AOF 文件的时候,会把早期日志写成 RDB 格式,新增加的继续使用 AOF。这样一来可以替高重写和恢复的速度。某种意义上有了这个就不必单独开启 RDB 持久化了。

内存清理

在 redis4.0 之后,可以通过将配置里的 activedefrag 设置为 yes 开启自动清理,或者通过 memory purge 命令手动清理。

上一页
下一页