redis事务与Lua脚本
1. Redis 原生事务
Redis 的事务通过 MULTI, EXEC, DISCARD, WATCH 等命令实现。
工作机制:
MULTI:开启一个事务,后续的命令都会被放入一个队列中,而不会立即执行。输入命令:将多个命令按顺序加入队列。
EXEC:一次性、按顺序地执行队列中的所有命令。WATCH:在MULTI之前执行,用于监控一个或多个键,如果在EXEC执行前这些键被其他客户端修改,则整个事务会失败(返回nil)。
特点:
原子性:
EXEC命令触发时,事务中的所有命令会作为一个独立的、连续的操作序列被执行。在EXEC命令执行期间,Redis 服务器不会处理其他客户端的任何命令。这保证了事务的原子性。没有回滚:这是 Redis 事务最著名的特点。如果事务中的某个命令执行失败(例如,对字符串执行了
HGET操作),后续的命令仍然会继续执行,并且之前已经执行的命令也不会回滚。Redis 认为这种错误通常是编程错误,应该在开发阶段被发现,而不是通过复杂的回滚机制来处理。隔离性:通过
WATCH实现乐观锁,可以保证在事务执行时,被监控的键没有被其他客户端修改,从而提供隔离性。
2. Lua 脚本
Redis 从 2.6 版本开始内置了对 Lua 脚本的支持。
工作机制:
使用
EVAL或SCRIPT LOAD+EVALSHA来执行一段 Lua 脚本。脚本中可以包含多个 Redis 命令和复杂的逻辑(如条件判断、循环等)。
特点:
真正的原子性:整个 Lua 脚本在执行时会被当作一个单命令。脚本在执行过程中,不会被其他任何命令或脚本打断。这是比
MULTI/EXEC更严格的原子性。减少网络开销:可以将多个操作打包在一个脚本中,一次发送,一次返回,显著减少网络延迟。
复杂性:可以在脚本中实现复杂的业务逻辑,这是简单的事务命令队列无法做到的。
可复用性:通过
SCRIPT LOAD和EVALSHA,可以预加载脚本并多次调用,避免重复传输脚本内容。
对比总结:为什么 Lua 脚本通常是更好的选择?
| 特性 | Redis 事务 | Lua 脚本 |
|---|---|---|
| 原子性 | 命令级别:EXEC 期间原子,但某个命令失败不影响后续。 | 脚本级别:整个脚本作为一个原子单位执行,要么全成功,要么全不执行。 |
| 错误处理 | 部分失败,无回滚。 | 如果脚本语法错误或 redis.call() 出错,整个脚本都不会执行。 |
| 复杂性 | 只能将命令简单排队,无法加入逻辑(如 if-else)。 | 支持复杂逻辑,可以包含条件、循环、局部变量等。 |
| 网络开销 | 需要发送 MULTI,N个命令,EXEC,共 N+2 次网络往返。 | 一次网络往返,将脚本和参数一次性发送。 |
| 性能 | 事务中的命令在排队时不会被解析,在 EXEC 时一次性解析和执行。 | 脚本在传输后被缓存,执行效率非常高。 |
| WATCH 乐观锁 | 支持,是保证 CAS 操作的核心。 | 不支持 在脚本内直接使用 WATCH,但脚本的原子性本身就解决了大部分竞态条件问题。 |
| 阻塞风险 | 事务中的命令应都是快速操作。 | 编写糟糕的脚本(如包含长循环或死循环)会长时间阻塞整个 Redis 服务器,是危险的。 |
关键区别和选择场景
需要条件逻辑时,必须使用 Lua 脚本
例如:只有在库存大于 0 时才执行扣减和创建订单。这在事务中无法实现,因为事务只是命令队列,无法根据中间结果做判断。
需要更严格的原子性和一致性时,推荐 Lua 脚本
由于 Lua 脚本的“全有或全无”特性,它更适合需要强一致性的场景。而原生事务的“部分失败”特性需要客户端进行更复杂的错误处理。
当需要
WATCH监控多个键,且逻辑简单时,可以使用原生事务虽然 Lua 脚本本身是原子的,但有些场景需要读取一个键的值,然后根据这个值(可能被其他客户端改变)来决定是否修改另一个键。这种跨键的、依赖于外部状态的 CAS 操作,使用
WATCH+ 事务仍然是标准做法。举例:你想在用户积分(key A)大于 100 时,给他发一条消息(修改 key B)。你需要先
WATCH积分 key,然后MULTI,在事务中检查积分并决定是否发消息。如果在EXEC前积分被其他客户端修改,事务会失败,你可以重试。如果这个逻辑写在 Lua 脚本里,脚本读取积分的那一刻值是确定的,但无法感知到在它执行前这个积分是否已经被其他客户端修改过(因为WATCH机制在脚本外)。
性能敏感和网络延迟高的场景,优先使用 Lua 脚本
一次网络往返的优势非常明显。
结论
Lua 脚本在功能上是 Redis 事务的超集,它提供了更强大的原子性、更丰富的逻辑能力和更低的网络开销。因此,对于绝大多数原本计划使用事务的场景,用 Lua 脚本来实现是更好、更推荐的选择。
唯一需要保留使用原生事务的情况是:你需要使用 WATCH 来实现一个涉及多个键的、逻辑简单的、依赖于在事务执行前键值是否被改变的乐观锁控制。即便如此,也可以考虑是否能用 Lua 脚本将相关键的操作合并到一个脚本中,从而从根本上避免竞态条件。
简而言之:能用 Lua 脚本就用 Lua 脚本。只有在 WATCH 是必需且逻辑无法融入单个脚本时,才使用原生事务。
