由于 redis 事务不满足原子性,并且每条命令都会与服务器进行网络交互,因此,对于整个交互过程而言也并没有性能上的提升,所以在实际的使用中,redis 的事务特性基本上是不会被使用到的。
但有时,我们还是需要去批量执行 redis 任务,并且对于每个命令的执行结果我们并不关心,这样的场景下是否可以提升 redis 的执行性能呢? 答案当然是有的,本文我们就来介绍一下。
如图所示,每个 redis 命令的执行都分为以下四个步骤:
这个执行过程所消耗的时间,就称为 RTT(Round trip time),也就是往返时间,当我们要去优化 redis 命令的执行时间,我们就可以在 redis 命令执行过程上进行着手。
redis 原生提供了 mget、mset、hmget、hmset 等一系列操作指令,用来提供批量获取数据或设置数据的能力。 这类操作实现了一次发送,批量排队、执行并且最终将结果一次性返回的特性。 这些操作最大的意义在于,他们不仅提供了批量执行任务的能力以及性能上的提升,最关键的是这些命令保证了指令执行的原子性。 但 redis 3.x 引入原生集群模式以后,由于 mget 与 mset 无法保证所有的 key 均散落在同一个服务器上,而一旦出现用于通信的目标服务器与实际存储对应 key 的服务器不同的情况时,redis 服务器会返回 MOVED 转向从而让客户端重新发起请求,可以参看: Redis 的 MOVED 转向与 ASK 转向
这就违背了 mget、mset 这一系列批量操作的初衷,也无法保证执行的原子性,因此,redis 集群模式下,mget 与 mset 这一系列批量操作是被禁用的。 但实际上,如果我们使用非集群模式的客户端(官方版本包中 redis-cli 不加 -c 参数),那么我们可以直接将集群中某个服务器节点当做非集群模式节点来使用,在这样的情况下,如果我们预先在客户端存储节点与 key 的关系,保证每次通信都使用全部正确的 key 去访问服务器,即自己维护 key 与 slot 的对应关系,我们仍然可以使用 mget、mset 这一系列指令,并且保证局部的原子性。 对于不使用官方集群方案的 redis 集群,如 codis、twemproxy 等集群模式,这都是一个比较实用的性能提升方式。
由于 mget、mset 的限制:
因此,通常情况下,mget、mset 往往无法满足复杂的业务需要。 事实上,redis 在早期版本已经考虑到通过减少客户端与服务端的交互来进行性能提升,这就是 pipeline 机制。
pipeline 机制让 redis 服务器可以在上一个请求尚未完成的情况下将下一个请求直接添加到队列进行等待,从而让客户端可以在一个 TCP 连接中完成多个指令的发送,并且无需等待指令执行完成,而后,客户端与服务端再通过一个 TCP 连接完成多个指令的返回。 例如,你可以在 redis 客户端中执行:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
随后,你会看到返回:
+PONG +PONG +PONG
但由于命令与返回信息缓存需要占用一定的内存,所以每次提交的命令不宜过大,如果有 1M 个 key 需要进行操作,可以进行 100 次 10K 个命令为单位的 pipeline 通信。
RTT 的降低是显而易见的,但这并不是让性能提升的唯一途径,因为如果多个命令顺次提交并执行,随着连接的反复创建,事实上,服务端每次进行 read 和 write 的上下文切换是非常耗时的,因此,如果提交的 key 过少,也并不推荐使用 pipeline。 下面是一段 Ruby 的测试代码。
require 'rubygems'
require 'redis'
def bench(descr)
start = Time.now
yield
puts "#{descr} #{Time.now-start} seconds"
end
def without_pipelining
r = Redis.new
10000.times {
r.ping
}
end
def with_pipelining
r = Redis.new
r.pipelined {
10000.times {
r.ping
}
}
end
bench("without pipelining") {
without_pipelining
}
bench("with pipelining") {
with_pipelining
}
打印出了:
without pipelining 1.185238 seconds with pipelining 0.250783 seconds
可见通过 pipeline 的方式,传输性能提升了 5 倍。
redis 的 pipeline 机制并没有改变命令的执行方式,指令只是缓存在了队列中,因此,与 mget、mset 不同,pipeline 无法保证 pipeline 内指令执行的原子性。 但与 mget、mset 相同的是,pipeline 操作依然无法在原生的集群模式下工作,如果想要在原生的 redis cluster 下使用 pipeline 操作,那么就必须改造客户端,维护 slot 映射。 同时,与 redis 事务机制一样,pipeline 操作无法因为某个指令执行失败而中断,也无法让下一个指令依赖上一个指令的结果,如果需要这些复杂的机制,那么建议使用 lua 脚本来完成相应的操作。
如果使用的是非集群模式的 redis,虽然这在现在几乎无法见到,这样的情况下,mget、mset 将会是你的首选,不仅可以提升效率,更重要的是可以保证批量任务的原子性,而如果是复杂的业务场景,需要多种指令混合操作,并且不需要在每一个指令执行完成后立即得到其执行结果,可以使用 pipeline 操作,而如果业务场景复杂到一个指令需要依赖其先前的指令的执行结果,并且一系列指令需要保证原子性,那只能通过 lua 脚本来实现了。 但在集群模式下,如果要实现上述这些机制,就需要修改客户端,实现 slot 映射的维护,这是官方不推荐的做法,具体可以参看搜狐开源的 redis client:https://cachecloud.github.io/
https://redis.io/topics/pipelining。