概述

Redis 提供三种将客户端多条命令打包发送给服务端执行的方式: Pipelining(管道)Transactions(事务)Lua Scripts(Lua 脚本)。本文不会过细的讨论三种方式的基础知识,将从这三种方式的 优势局限性原子性 方面展开讨论

Pipelining(管道)

Redis 管道是三者之中最简单的,当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端

$ printf "INCR x\r\nINCR x\r\n" | nc localhost 6379
:1
:2

可以看到,管道只是简单的将多个命令拼接在一起,命令之间用换行符(/r/n)分割,并没有在第一条命令前或最后一条命令后面添加开始/结束标志位

redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端

Pipelining 的优势

在性能方面, Pipelining 有下面两个优势:

  • 将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数,节省了 RTT
  • 避免了上下文切换,当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换

Pipelining 原子性

我们都知道, redis 执行命令的时候是单线程执行的,所以 redis 中的所有命令都具备原子性,这意味着 redis 并不会在执行某条命令的中途停止去执行另一条命令

但是 Pipelining 并不具备原子性,想象一下有两个客户端 client1client2 同时向 redis 服务端发送 Pipelining 命令,每条 Pipelining 包含 5 条 redis 命令。 redis 可以保证 client1 管道中的命令始终是顺序执行的, client2 管道中的命令也是一样,始终按照管道中传入的顺序执行命令

但是 redis 并不能保证等 client1 管道中的所有命令执行完成,再执行 client2 管道中的命令,因此,在服务端中的命令执行顺序有可能是下面这种情况

redis pipeline lua redis pipeline lua区别_客户端

这种行为显示 Pipelining 在执行的时候并不会阻塞服务端。即使 client1 向客户端发送了包含多条指令的 Pipelining ,其他客户端也不会被阻塞,因为他们发送的指令可以插入到 Pipelining 中间执行

Pipelining 局限性

只有在 Pipelining 内所有命令执行完后,服务端才会把执行结果通过数组的方式返回给客户端。在执行 Pipelining 内的命令的时候,如果某些指令执行失败, Pipelining 仍会继续执行

比如下面的例子

$ printf "SET name huangxy\r\nINCR name\r\nGET name\r\n" | nc localhost 6379
+OK
-ERR value is not an integer or out of range
$6
huangxy

Pipelining 中第二条指令执行失败, Pipelining 并不会停止,而是会继续执行,等所有命令都执行完的时候,再将结果返回给客户端,其中第二条指令返回的是错误信息

Pipelining 的这个特性会导致一个问题,就是当 Pipelining 中的指令需要读取之前指令设置 key 的时候,需要额外小心,因为 key 的值有可能会被其他客户端修改。此时 Pipelining 的执行结果往往就不是我们所预期的

Pipelining 使用场景

  • 对性能有要求
  • 需要发送多个指令到服务端
  • 不需要上个命令的返回结果作为下个命令的输入

Transactions(事务)

redis 中的事务,跟我们之前在学关系型数据库的时候所了解到的事务概念有点区别。 redis 中的事务机制主要是用来对多个命令进行排队,并在最后决定是否需要执行事务中的所有命令与否

与管道不同,事务使用特殊的命令来标记事务的开始和结束( MULTIEXECDISCARD )。服务器还可以对事务中的命令进行排队(这样客户端可以一次发送一条命令)。除此之外,一些第三方库还喜欢在客户端中对事务的命令进行缓存,然后通过在管道中发送整个事务的方式对其进行优化

事务的优点

事务提供了 WATCH 命令,使我们可以实现 CAS 功能,比如通过事务,我们可以实现跟 INCR 命令一样的功能

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

事务的原子性

redis 事务具备原子性,当一个事务正在执行时,服务端会阻塞其接收到的其他命令,只有在事务执行完成时,才会执行接下来的命令,因此事务具备原子性

事务的局限性

Pipelining 一样,只有在事务执行完成时,才会把事务中多个命令的结果一并返回给客户端,因此客户端在事务还没有执行完的时候,无法获取其命令的执行结果

如果事务中的其中一个命令发生错误,会有以下两种可能性:

  • 当发生语法错误,在执行 EXEC 命令的时候,事务将会被丢弃,不会执行
  • 当发生运行时错误(操作了错误的数据类型)时, redis 会将报错信息缓存起来,继续执行后面的命令,并在最后将所有命令的执行结果返回给客户端(报错信息也会返回)。这意味着 redis 事务中没有回滚机制

事务使用场景

  • 需要原子地执行多个命令
  • 不需要事务中间命令的执行结果来编排后面的命令

Lua 脚本

redis 从 2.6 版本开始引入对 Lua 脚本的支持,通过在服务器中嵌入 Lua 环境, redis 客户端可以直接使用 Lua 脚本,在服务端原子地执行多个 redis 命令

Lua 脚本的优势

Pipelining 和 事务不同的是,在脚本内部,我们可以在脚本中获取中间命令的返回结果,然后根据结果值做相应的处理(如 if 判断)

local key = KEYS[1]
local new = ARGV[1]

local current = redis.call('GET', key)
if (current == false) or (tonumber(new) < tonumber(current)) then
  redis.call('SET', key, new)
  return 1
else
  return 0
end

同时, redis 服务端还支持对 Lua 脚本进行缓存(使用 SCRIPT LOADEVAL 执行过的脚本服务端都会对其进行缓存),下次可以使用 EVALSHA 命令调用缓存的脚本,节省带宽

Lua 脚本的原子性

Lua 脚本跟事务一样具备原子性,当脚本执行中时,服务端接收到的命令会被阻塞

Lua 脚本的局限性

Lua 脚本在功能上没有过多的限制,但要注意的一点是,Lua 脚本在执行的时候,会阻塞其他命令的执行,所以不宜在脚本中写太耗时的处理逻辑

Lua 脚本的使用场景

  • 需要原子性地执行多个命令
  • 需要中间值来组合后面的命令
  • 需要中间值来编排后面的命令
  • 常用于扩展 redis 功能,实现符合自己业务场景的命令

参考文档