文章

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​三个命令通常一起使用,形成一个完整的事务操作流程:

  1. 使用WATCH​命令监控一个或多个键。
  2. 使用MULTI​命令开始一个事务。
  3. 在事务中执行一系列命令。
  4. 使用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这类的方案来处理。

License:  CC BY 4.0