有界的 Redis Lua 脚本将读改写放进同一个执行窗口,但没有自动回滚。

Redis Lua 脚本执行时不被穿插

Redis Lua 脚本可以先读取数据、判断条件,再更新数据,而不让另一个客户端的命令插进这些步骤之间。但这不意味着每个语句都会成功,也不意味着后面发生错误时会撤销前面的写入,更不意味着执行结果自动具备持久性。原子执行与失败回滚,是必须拆开理解的两个问题。 把判断放在数据旁边 例如,仅当令牌仍属于当前持有者时才删除它。客户端先 GET、稍后再 DEL,中间可能已经被新持有者替换了值;随后删除就误伤了别人。脚本可以在同一次执行中比较当前值,匹配才删除。因此旧持有者即使带着之前读到的令牌来操作,也不会直接删除新持有者的值。 脚本应通过 redis.call 访问 Redis 数据,这正是脚本接口的用途。沙箱不提供普通文件系统或网络访问,不能把邮件、支付或文件读写搬进脚本。所有会访问的键都应通过 KEYS 传入,普通值用 ARGV,不要根据数据库里读出的内容临时拼接键名。Redis Cluster 中的常规多键使用还要求相关键位于同一哈希槽。 如果原生原子命令已经表达了条件,应优先使用它。Redis 8.4 新增的 DELEX 配合 IFEQ 就能按字符串值比较后删除。下方 Lua 版本用于理解分支与响应,不代表需要用自定义代码替换原生命令。删除所有权令牌本身也不是完整的分布式锁协议,无法单独阻止过期工作者继续修改外部资源。 坏掉的样子 没有被捕获的 redis.call 错误会让脚本停止。例如先 SET 一个标记,再对字符串键执行 LPUSH,后者失败时,前面的 SET 仍然保留,而失败语句之后的代码不会继续运行。这又与 MULTI/EXEC 有区别:事务中单条命令发生运行期错误,通常不会阻止后面的排队命令继续执行。两者都不提供先前成功写入的自动回滚。 redis.pcall 会把 Redis 命令错误作为包含 err 字段的表交回脚本,由脚本显式判断并决定是否继续。捕获错误既没有撤销旧写入,也不能把失败的业务操作自动变成成功。应尽可能在第一条写入之前校验参数和预期类型,并明确剩余失败路径如何收场,而不是依赖脚本名义上的原子性掩盖部分结果。 ...

一月 11, 2028 · 3 分钟 · XB Field Notes
WATCH 保护依赖读取结果的事务,EXEC 无穿插地执行通过检查的命令队列。

MULTI EXEC 把一串命令当作一次

MULTI/EXEC 能阻止别的客户端命令插进队列的执行过程,却不会自动保护应用此前已经读到的值。如果两个购买方先读到相同库存,再各自写回计算结果,即使每次 SET 都放进事务,也仍然可能丢失一次更新。问题出在读取与提交之间的空隙,而不是 SET 执行得是否足够连续。 先监视,再读取依赖 WATCH 让 EXEC 变成有条件的执行:被监视的键从建立监视之后不能发生修改。因此,必须先 WATCH,再读取参与决策的数据。若键在提交前改变,Redis 拒绝整个队列并返回空响应;这只是不执行自己的队列,不会撤销竞争方已经完成的修改。 监视与事务状态都属于同一条连接。redis-py 流水线在 watch 之后保留连接,multi 之前的 GET 会立即发出,应用可以读取结果并做判断;multi 之后才转为缓冲命令,等待 execute。用上下文管理器包住一次尝试,可以在结束时重置流水线状态并释放专用连接。 凡是会影响决策的值或“不存在”条件,都要纳入监视范围。只监视库存,却忽略另一个键保存的额度,并不能保护额度判断。WATCH 也不只是比较最终字节:即便 SET 写回相同内容,也可以使监视失效;原来不存在的被监视键被创建,同样会导致冲突。本次版本中,由过期和内存淘汰引起的变化也计算在内。 重试整个决策,而不是旧计算结果 收到明确的冲突结果后,应建立新监视,重新读取依赖,再检查业务条件和计算写入值。继续使用旧库存,相当于把同一个错误判断再执行一次。EXEC 无论成功还是因监视冲突取消,都会清除监视;如果应用读完后决定放弃,也可以通过 UNWATCH 主动解除。 重试需要次数或时间上限,并让调用方知道何时已耗尽预算。热点键上的大量冲突会放大成本,即使最终没有丢数据也会增加延迟。退避可以缓和竞争,却补不上遗漏的监视键。若原生命令已经能表达所需的原子操作,应优先使用它;实验刻意拆开库存算术,是为了展示旧值覆盖问题。 监视检查通过,也不会让事务获得自动回滚能力。EXEC 内部某一条命令运行失败,其它成功命令仍可能已经生效。应校验相关数据模型并逐项检查响应,不要把数个互不相干的副作用放在一起,就称为业务上的全成全败。 坏掉的样子 先读后 WATCH,会漏掉建立监视前的那段竞争窗口。用一条连接监视、另一条连接提交,不会保护后者的队列。在重试循环中发邮件或发起支付,则可能在 Redis 拒绝本次尝试之前,就已经产生了无法随队列取消的外部效果。 还必须区分服务端明确报告的冲突与传输故障。redis-py 8.1.0 在监视期间遇到连接异常,也可能抛出 WatchError;仅凭异常类型,不能断言 EXEC 从未执行。提交之后应答丢失,需要核对业务结果或依赖幂等协议,不能无条件重新执行不可逆动作。 ...

七月 22, 2027 · 3 分钟 · XB Field Notes