当前位置: 首页 > news >正文

Redis 的指令执行方式:Pipeline、事务与 Lua 脚本的对比

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 thenreturn 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)  -- 增加账户 Breturn 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

参考

  1. Redis 事务 vs Lua,区别以及如何选择
  2. 【Redis】- 事务和Lua脚本
  3. Redis 管道、事务、Lua 脚本对比

相关文章:

  • ROS机器人一般用哪些传感器?
  • 初识Redis · 客户端“Hello world“
  • R 语言科研绘图 --- 饼状图-汇总
  • Yum镜像源
  • 中间件--ClickHouse-10--海量数据存储如何抉择ClickHouse和ES?
  • 【系统分析师】-软件工程
  • 【文件操作与IO】详细解析文件操作与IO (一)
  • 探索 Higress:下一代云原生 API 网关
  • 前端融合图片mask
  • 高级java每日一道面试题-2025年4月13日-微服务篇[Nacos篇]-Nacos如何处理网络分区情况下的服务可用性问题?
  • ubantu18.04(Hadoop3.1.3)之MapReduce编程
  • pnpm解决幽灵依赖问题
  • Model Context Protocol (MCP) 开放协议对医疗多模态数据整合的分析路径【附代码】
  • Kaamel隐私与安全分析报告:Microsoft Recall功能评估与风险控制
  • hadoop和Yarn的基本介绍
  • 使用Java动态数据生成PDF报告:简化您的报告导出流程
  • AI语音助手 React 组件使用js-audio-recorder实现,将获取到的语音转成base64发送给后端,后端接口返回文本内容
  • kafka菜鸟教程
  • Android 证书 是什么
  • 在服务器上安装安装mysql
  • 股市劝服马斯克
  • 北大学者:过度依赖技术工具可能会削弱人类主动思考的能力
  • 上海常务副市长:持续提升跨境投融资便利化水平,稳步扩大金融领域的制度型开放
  • 推进“三个免于”,上海试点首发进口化妆品快速通关模式
  • 外媒:特朗普称或将“大幅降低”对中国的关税
  • 一季度提高两只医药基金股票仓位,中欧基金葛兰加仓科伦药业、百利天恒