Redis事务 Lua 与 ACID特性解析
要讨论Redis事务,首先要明确讨论的是什么,我们实质上在讨论“Redis有没有操作能实现ACID,或者部分实现ACID”,在这个定义的基础上,可以找到两个知识点:
- MULTI/EXEC
- Redis Lua
先介绍一下这两个东西,介绍完之后,再讨论ACID。
MULTI/EXEC
这是一组命令的集合,我们一般说的Redis事务指的就是这个方式。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
具体使用方法是:
先输入MULTI
,开启事务,然后输入若干命令,redis会将后续的命令逐个放入队列中,然后使用EXEC
命令来原子化执行这个命令系列。
如果执行过程中有运行时错误,会让这一条命令失败,剩下的会继续执行,不会回滚。如果有语法错误,会在执行前报错,全都不会执行。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
OK
OK
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>
或者不使用EXEC
,使用DISCARD
来取消事务,那么这些命令都不会执行。
进一步的,Redis提供了WATCH
和UNWATCH
两个命令,它们主要用于确保操作的隔离性。
WATCH
命令用于监控一个或多个键,在事务EXEC
时,如果监控的键值被其他客户端修改,那么事务将会不会执行,从而保证事务的执行不会受到其他操作的影响。
在事务执行前调用UNWATCH
命令,可以取消之前通过WATCH
命令设置的监控。
在Redis中,WATCH
、MULTI
和EXEC
三个命令通常一起使用,形成一个完整的事务操作流程:
- 使用
WATCH
命令监控一个或多个键。 - 使用
MULTI
命令开始一个事务。 - 在事务中执行一系列命令。
- 使用
EXEC
命令提交事务。
如果监控的键值被修改,事务将不会执行,并且所有命令都会被丢弃。
Redis Lua
Redis支持使用Lua脚本来编写更复杂的“事务”,通过EVAL
命令,可以执行一段Lua脚本,在脚本内通过redis.call()
或者redis.pcall()
,可以执行对应的操作。
EVAL script numkeys [key [key ...]] [arg [arg ...]]
call()
在中间指令出问题后,会中断执行,pcall()
不会。
ACID
此时,我们可以来分析这两种方案的ACID特性。
- Atom 原子性 不保证
这里涉及到原子性定义的问题,我认为原子性是指“要么全部成功,要么全部不成功”,在事务发生错误时,并不会回滚,所以不保证原子性。 - Consistency 一致性 能保证
一串Redis命令,不管有没有成功,Redis首先不会让命令本身破坏自己的结构,我们认为这种情况下是保证一致性的。如果执行过程中实例发生故障,恢复数据时RDB不会记录执行到一半的事务,AOF也可以通过redis-check-aof
来处理一下,也可以保证不记录执行到一半的事务。 - Isolation 隔离性 能保证
因为Redis是单线程程序,执行Lua脚本时,会逐条解释执行,在执行过程中不会出现并发问题,能保证原子性,Redis事务也一样,EXEC
指令一旦发出,过程中不存在并发问题。
再加上WATCH,WATCH
检查发生于EXEC
之前,是因为在shell编写的过程中并不是阻塞的,相当于一个乐观锁,保证了从编写阶段开始的隔离性 - Durability 持久性 不保证
Redis当然不保证持久性
这里还有一种特殊情况——Redis集群。
当Redis处于集群的集群模式时,首先直接不能支持原生事务,如果使用Lua脚本,连SLOT都不能跨需要用Hash Tag这类的方案来处理。