尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Redis 系列(七):事务、Pipeline 与 Lua 脚本——从原子性到 Functions

Redis 系列(七):事务、Pipeline 与 Lua 脚本——从原子性到 Functions 核心目标区分 Redis 事务与关系型数据库事务理解 Pipeline 的性能收益与边界用 Lua 脚本实现真正原子的复合操作了解 Redis 7 Functions 的定位。用秒杀扣库存贯穿三种工具。前置知识完成 Part 1单线程事件循环——本篇文章论的地基与 Part 2命令复杂度、RTT 成本。验证环境Redis 8.10.0cygwin 移植版主实例 6379、redis-py 8.1.0、Python 3.11.6、Windows 11Lua 阻塞实验在隔离实例 6380 上进行。最后复核日期2026-08-07。0. 本篇问题场景一个扣库存的三种写法库存 10 件100 个人同时下单怎么保证不超卖三种典型答案读 → 判断 → 写Python 代码三步100 个并发全读到 10全通过——超卖 90 件MULTI事务包裹入队执行但判断和扣减不是一条命令中间态仍然可被插入Lua 脚本把读库存、判断、扣减写进一段原子执行的脚本——这才是不超卖的正解。本篇把三者的能力边界讲透并回答一个高频疑问“Redis 事务和 MySQL 事务是一回事吗”——不是。Redis 事务保证的是串行不被打断不是出错回滚。1. 事务MULTI / EXEC / DISCARD / WATCH1.1 事务的原子性到底指什么MULTI开始入队之后命令都返回QUEUED而不是执行EXEC才一次性执行$ redis-cli MULTI OK SET t:a 1 QUEUED INCR t:counter QUEUED EXEC 1) OK 2) (integer) 1Redis 事务的原子性 “EXEC 执行期间不会被其他命令插入”单线程事件循环的天然结果而不是要么全部成功、要么全部回滚。它没有回滚因为入队阶段就能发现的错误命令不存在、参数不对→ 整个事务拒绝执行执行阶段才发现的错误如LPUSH打到 String 键→只跳过出错那条其余照常执行。1.2 实测一运行错误 部分成功MULTI OK SET t:ok1 1 QUEUED LPUSH t:str x ← t:str 是 String执行时才报错 QUEUED SET t:ok2 2 QUEUED EXEC 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) OK验证t:ok1和t:ok2都写成功了——事务中出错不连锁回滚。这与 MySQL 事务的失败即回滚是本质区别凡是依赖Redis 事务会回滚的代码都是误解。1.3 实测二入队错误 整体拒绝MULTI OK SET t:x 1 QUEUED INCR t:y extra_arg (error) ERR wrong number of arguments for incr command ← 入队即报错 EXEC (error) EXECABORT Transaction discarded because of previous errors.EXECABORT意味着整个事务没执行t:x未写入。所以 Redis 事务的规则是编译期错误全拒运行期错误部分执行——把能提前发现的错误提前到入队阶段把不能提前的留给执行阶段。1.4 WATCH乐观锁MULTI事务解决不被插入WATCH解决不被偷改——它监视一个键如果 EXEC 之前该键被其他连接修改事务直接放弃withr1.pipeline()aspipe:pipe.watch(balance)# 开始监视r2.set(balance,50)# 另一连接在监视期间修改pipe.multi()pipe.incrby(balance,-30)pipe.execute()# → WatchError实测两个场景场景 Awatch 后被修改WatchError: 监视期间被修改事务放弃 balance50保持对方值 场景 Bwatch 期间无修改EXEC 成功 balance70100-30WATCH 是 CASCompare-And-Swap监视 → 校验 → 执行失败则重试。注意它的代价竞争频繁时重试循环会放大请求量高冲突场景不如 Lua§3。2. Pipeline用一次往返换性能2.1 机制Pipeline 不是事务它把 N 条命令攒在客户端一次性发给服务器一次收齐响应减少的是RTT往返次数不提供任何原子性。Part 2 实测过单条命令的 RTT 成本5 万次单条 ZADD ≈ 6 秒。2.2 实测1000 条 INCR单条循环 1000 次: 79.0 ms pipeline 1000 次: 5.2 ms 加速比: 15.3x本机 loopback、Redis 8.10、中位耗时跨网络时收益更大。Pipeline 与事务的对比维度MULTI/EXECPipeline原子性执行期不被插入无命令间可被其他客户端插入往返1 次EXEC 一次性发1 次中途出错部分成功/整体拒绝错误在响应中逐条返回典型用途一致性要求批量写入、大量读redis-py 的pipeline(transactionFalse)就是纯 PipelinetransactionTrue默认会额外包一层 MULTI/EXEC——同一个 API两种语义必须显式选择。3. Lua 脚本真正的原子复合操作3.1 为什么脚本是原子的Redis 执行 Lua 脚本时整个脚本在事件循环中一次性跑完期间不会处理任何其他命令——脚本天然就是事务 条件逻辑的合体。相比 MULTIMULTI 只能排队执行不能读一个值再决定下一步WATCH 是笨拙的补丁Lua 可以GET 库存 → if 库存 0 then DECR判断与操作在同一次执行里完成没有中间态。3.2 实测秒杀扣库存100 并发零超卖localstocktonumber(redis.call(GET,KEYS[1]))ifstock0thenredis.call(DECR,KEYS[1])return1elsereturn0end100 个线程并发调用库存 10100 个并发请求成功 10 个库存 10超卖False 剩余库存: 010 个线程拿到 190 个拿到 0——脚本级原子性保证不超卖不需要锁、不需要重试。3.3 EVALSHA 与脚本缓存EVAL每次都要把脚本文本发给服务器EVALSHA只发脚本的 SHA1服务器缓存命中则直接执行脚本 SHA: edfeb7032922448cef24b8512a794cc6700ce6b5 EVALSHA 执行: 0 SCRIPT EXISTS: [True]redis-py 的register_script自动管理这一过程。注意脚本缓存的生命周期由服务器管理——服务器重启、SCRIPT FLUSH、主从角色切换都会清空缓存此时EVALSHA报NOSCRIPT客户端要回退到EVALFLUSHALL/FLUSHDB不会清脚本缓存。生产代码必须处理这个回退路径。3.4 实测脚本死循环 全局阻塞脚本执行期间单线程不处理任何命令死循环脚本会卡死整个服务器EVAL while true do end 0 异步触发 另一连接 PING → 阻塞 3 秒无响应证实全局阻塞 SCRIPT KILL → OK脚本被终止 恢复后 PING → PONGSCRIPT KILL只能终止尚未执行写命令的脚本一旦脚本写过数据只能SHUTDOWN NOSAVE重启。机制补充脚本执行超过lua-time-limit默认 5000ms后服务器开始拒绝其他客户端的命令返回BUSY Redis is busy running a script而非无限等待此时才能执行SCRIPT KILL——5 秒内是静默阻塞5 秒后是 BUSY 拒绝。这是把业务逻辑写进 Lua 的最大风险脚本必须短小、无副作用悬挂、无死循环。4. Redis 7 Functions脚本的工程化EVAL的脚本是客户端携带 服务器缓存管理散落Redis 7 的Functions把脚本注册到服务器成为一等公民维度EVALFunctions脚本存储客户端每次发送服务器缓存FUNCTION LOAD注册到服务器持久化版本管理无支持库版本、原子替换调用EVALSHANOSCRIPT 需回退FCALL无需关心缓存适用临时脚本、快速原型团队共享、上线管理FUNCTION LOAD #!lua namemylib\n... function ... end FCALL mylib.myfunc 1 stockFunctions 是Lua 能力 工程化管理生产团队用 Lua 落业务逻辑时应优先考虑。5. 版本与环境差异差异点官方 7.4本机 8.10.0cygwin 移植版影响MULTI/WATCH/EVAL语义稳定一致本文实测数据可直接复用Functions7.0可用生产优先FCALLSCRIPT KILL限制有写操作则不可杀一致死循环实验仅限无写脚本redis-py pipelinetransaction参数一致显式声明语义6. 测试与验收建议测试事务部分成功运行错误、EXECABORT入队错误、WATCH 竞态两场景、Lua 并发不超卖多线程、SCRIPT KILL恢复隔离实例Lua 死循环实验只在隔离实例做。本篇验收清单能说清Redis 事务无回滚及两种错误入队/执行的不同处置能区分 Pipeline省 RTT与事务防插入的语义能写出秒杀扣库存的 Lua 脚本并解释原子性来源知道EVALSHA的 NOSCRIPT 回退路径与 Functions 的定位知道脚本死循环会全局阻塞及SCRIPT KILL的边界。7. 常见误区“Redis 事务会回滚”——不会运行错误只跳过出错命令§1.2。“pipeline 就是事务”——pipeline 只省往返命令之间可被插入§2。“Lua 脚本可以随便写”——死循环/大循环会全局阻塞且写过数据后SCRIPT KILL失效§3.4。“EVALSHA永远可用”——服务器重启后缓存清空必须处理NOSCRIPT回退§3.3。“WATCH 适合高竞争场景”——竞争激烈时重试风暴反而放大压力高冲突用 Lua§1.4、§3。8. 本篇小结三种工具对应三种原子性需求防插入→MULTI/EXEC串行不被打断但不回滚防偷改→WATCH乐观锁CAS低竞争适用复合逻辑原子执行→ Lua 脚本判断 操作一体秒杀等场景的正解省往返→ Pipeline性能工具与一致性无关。下一篇 Part 8缓存设计模式与一致性 把这些原子性武器用到缓存架构上Cache Aside、先删缓存还是先更新库、穿透/击穿/雪崩的防护以及缓存一致性为什么是个无解的工程权衡。9. 官方资料Transactionshttps://redis.io/docs/latest/develop/interact/transactions/EVAL / EVALSHAhttps://redis.io/docs/latest/commands/eval/Functionshttps://redis.io/docs/latest/develop/programmability/functions-intro/Programming with Luahttps://redis.io/docs/latest/develop/programmability/
返回列表