Pipeline
客户端将多条命令打包发送,服务器顺序执行并一次性返回所有结果。可以减少网络往返延迟(RTT)以提升吞吐量。
需要注意的是,Pipeline 中的命令按顺序执行,但中间可能被其他客户端的命令打断。
典型场景:批量插入、查询或更新数据,也就是命令间没有什么依赖关系的情况。
比如下面这个例子,Pipeline 将 3 个 SET 打包发送,减少三次网络往返为一次。假设单次命令 RTT 为 1ms,3 次命令需 3ms;使用 Pipeline 仅需约 1ms。
SET key1 value1
SET key2 value2
SET key3 value3
事务
可以确保一组命令“原子”执行,确保一组命令执行过程中不被其他客户端打断。如果事务中有命令失败,整个事务不会回滚,但后续命令不会执行。
通常,它会结合 WATCH
来实现乐观锁。
另外,Redis 中的原子性和 MySQL 中的原子性意义不同。MySQL 的原子性意味着,要么全部执行成功,要么就不执行,它会涉及回滚的操作。但 Redis 没有没有回滚的操作(为了性能,实现回滚需要维护事务日志等其他一些机制,会增加开销),它的原子性指,一组命令顺序执行,不会被其他命令打断。
还有,Redis 的事务并不会像 Pipeline 一样,将命令一起发送给 Redis,而是会一条一条发送。为什么?可以结合下面的过程,这样的话可以用来进行语法检查。
客户端发送 MULTI,开启事务。
客户端逐条发送命令(如 SET、INCR),每条命令立即传输到服务器。
服务器收到命令后,不执行,而是将其放入事务队列,并返回 QUEUED 响应。
客户端发送 EXEC,服务器原子性执行队列中的所有命令,并返回结果。
或者,客户端发送 DISCARD,清空队列,取消事务。
在实际使用中,比如在 Python 中的 redis-py 库,事务会以 Pipeline 的方式批量发送,来优化命令发送。
pipe = r.pipeline() # 创建 Pipeline
pipe.multi() # 开始事务
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.execute() # 发送并执行事务
典型场景:需要保证数据一致性的操作,如转账、库存扣减。
WATCH accountA -- 监控账户 A 的键,防止被其他客户端修改
MULTI
GET accountA -- 如果账户 A 的值发生改变,剩下的命令将不会执行
SET accountA $new_balanceA -- 更新账户 A 余额(减 100)
SET accountB $new_balanceB -- 更新账户 B 余额(加 100)
EXEC
Lua 脚本
Lua 脚本可以在一条命令中实现复杂逻辑,如条件判断、循环、错误控制。它和事务功能上有些相似,都是为了实现原子性。不过 Lua 脚本有两种选择,redis.call()
失败时脚本停止(前面已经执行完的命令不会受影响);redis.pcall()
捕获错误继续执行。
它比起事务有什么好处呢?
- 比如事务中需要依赖于一个 GET 操作的结果,来决定后面的操作,事务无法实现,需要结合客户端的代码,加大了复杂性。
- 事务中的每条命令都会与 Redis 服务器进行网络交互,增加了客户端与服务器的交互。
另外,为了避免每次都需要传输完整的 Lua 脚本给 Redis,Redis 还设立了一个缓冲区,来去存放 Lua 脚本的内容以及它的 SHA1 值(一种哈希算法,将 Lua 脚本内容映射为固定长度的唯一标识符)。之后只需传输对应的 SHA1 值即可执行对应的脚本。
总之,Lua 脚本会更灵活,事务能做的,Lua 都能做。所以能用 Lua 就用 Lua。
典型场景:需要保证数据一致性的操作,如转账、库存扣减。和事务差不多。
local accountA = KEYS[1] -- 账户 A 的键
local accountB = KEYS[2] -- 账户 B 的键
local amount = tonumber(ARGV[1]) -- 转账金额(转换为数字)
-- 获取账户 A 余额(不存在则默认为 0)
local balanceA = redis.call('GET', accountA) or 0
balanceA = tonumber(balanceA)
-- 检查余额是否足够
if balanceA < amount then
return 0 -- 余额不足,返回 0
end
-- 获取账户 B 余额(不存在则默认为 0)
local balanceB = redis.call('GET', accountB) or 0
balanceB = tonumber(balanceB)
-- 执行转账
redis.call('SET', accountA, balanceA - amount) -- 扣减账户 A
redis.call('SET', accountB, balanceB + amount) -- 增加账户 B
return 1 -- 转账成功,返回 1
-- Redis 调用方式:
EVAL "local accountA = KEYS[1] local accountB = KEYS[2] local amount = tonumber(ARGV[1]) local balanceA = redis.call('GET', accountA) or 0 balanceA = tonumber(balanceA) if balanceA < amount then return 0 end local balanceB = redis.call('GET', accountB) or 0 balanceB = tonumber(balanceB) redis.call('SET', accountA, balanceA - amount) redis.call('SET', accountB, balanceB + amount) return 1" 2 accountA accountB 30
参考
- Redis 事务 vs Lua,区别以及如何选择
- 【Redis】- 事务和Lua脚本
- Redis 管道、事务、Lua 脚本对比