概述
Redis事务提供一种将多个命令打包,然后一次性、按顺序地执行的机制,在事务执行的期间不会主动中断,服务器在执行完事务中的所有命令之后,才会继续处理其他客户端的其他命令。
三个重要的保证:
- 批量操作在发送EXEC命令前被放入队列缓存
- 收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中
Redis中的事务是一组命令的集合,事务也是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。
Redis事务由5个命令来实现:
- MULTI:标记一个事务块的开始。开启Redis事务,置客户端为事务态
- EXEC:提交事务,执行从MULTI到此命令前的命令队列,置客户端为非事务态
- DISCARD:取消事务,置客户端为非事务态
- WATCH:监视键值对,作用时如果事务提交EXEC时发现监视的监视对发生变化,事务将被取消
- UNWATCH:取消WATCH命令对所有Key的监视
事务从开始到执行会经历以下三个阶段:
- 开始事务
- 命令入队
- 执行事务
MULTI标记事务的开始,将客户端状态的flags属性的REDIS_MULTI
选项打开,让客户端从非事务状态切换到事务状态。
命令入队
Redis客户端处理非事务状态时,命令会立即被服务端执行。但是事务状态下,如果客户端发送的命令是上面四个命令其一,则服务器立即执行;除此之外,服务器并不立即执行命令,而是将命令放入事务队列里面,向客户端返回QUEUED回复。
WATCH
Redis使用WATCH命令监视给定的Key,当EXEC时如果监视的Key从调用WATCH后发生过变化,则整个事务会失败。也可以调用WATCH多次监视多个Key,这样就可以对指定的Key加乐观锁。注意WATCH的Key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然EXEC,DISCARD,UNWATCH命令都会清除连接中的所有监视。
实战
本文使用的Redis版本为Windows下7.4.0
版本。
empty array
执行MULTI命令后没有其他命令,直接输入EXEC命令,不报错:
sh
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> exec
(empty array)
WATCH
可用于实现类似于乐观锁效果,即CAS,compare and set。
WATCH用于监视Key是否被改动过,支持同时监视多个Key,只要还没真正触发事务,WATCH都会尽职尽责的监视。当多个线程更新同一个Key值时,会跟原值做比较,一旦发现它被修改过,则拒绝执行命令,执行EXEC时就会返回nil,表示事务无法触发。
双击redis-cli.exe
打开一个命令行窗口,依次执行如下命令:
sh
127.0.0.1:6379> watch age
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set age 31
QUEUED
双击redis-cli.exe
打开另一个命令行窗口,执行如下命令:
sh
127.0.0.1:6379> set age 32
OK
回到第一个窗口(客户端),提交EXEC命令:
sh
127.0.0.1:6379(TX)> exec
(nil)
命令返回(nil),表示事务无法触发。因为另外一个客户端通过set
命令更新过Key
。
哪怕是值不变,有过set key
动作就会更新更新时间戳字段(猜测),和MVCC机制比较类似,Redis源码待调研。
客户端一执行:
sh
127.0.0.1:6379> set age 11
OK
127.0.0.1:6379> watch age
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set age 12
QUEUED
客户端二执行:
sh
127.0.0.1:6379> get age
"11" # 在客户端一执行set后,可get获取
127.0.0.1:6379> set age 11
OK # 在客户端一执行watch后,尝试set更新到相同的年龄
客户端一执行:
sh
127.0.0.1:6379(TX)> exec
(nil)
执行结果也是(nil)。
事务错误
两类错误:
- 调用EXEC之前的错误
- 调用EXEC之后的错误
EXEC执行前出错:如语法有误,内存不足导致。只要出现某个命令无法成功写入缓冲队列的情况,Redis都会进行记录,在客户端调用EXEC时,Redis会拒绝执行这一事务。
sh
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> he
(error) ERR unknown command 'he', with args beginning with:
127.0.0.1:6379(TX)> set he he
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
EXEC执行后出错:Redis不会理睬这些错误,而是继续向下执行事务中的其他命令。对于应用层面的错误,并不是Redis需要考虑和处理的问题,所以事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行。
sh
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set age 34
QUEUED
127.0.0.1:6379(TX)> sadd age 35
QUEUED
127.0.0.1:6379(TX)> set age 35
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
(0.53s)
127.0.0.1:6379> get age
"35"
这里可以得出一个很重要的结论:在执行事务时某个命令执行失败,并不会影响其他命令的执行,即Redis 的事务并不会回滚。
Lua集成
Lua脚本集成,参考Redis系列之Lua脚本整合。
ACID
传统事务四个核心特性,即ACID。为了保持简单,Redis事务保证其中的一致性和隔离性;不满足原子性和持久性;
原子性
在执行事务命令时,在命令入队时,Redis检测事务里的命令是否正确,即是否有语法错误,如果不正确则会产生错误。事务里的命令是批量提交执行的,也就是命令还没执行,当然也就不存在回滚一说;
当命令格式正确,而因为操作数据结构引起的错误,则该命令执行时才会出现错误,而其之前和之后的命令都会被正常执行。
参考You Don't Need Transaction Rollbacks in Redis,以及Redis Transactions:
Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.
翻译:支持回滚会对 Redis 的简单性和性能产生重大影响。
一致性
一致性指的就是事务执行前后的数据符合数据库的定义和要求。
Redis符合要求,不论是发生语法错误还是运行时错误,错误的命令均不会被执行。
隔离性
多个事务并发执行,各个事务之间不会互相影响。原因:Redis事务不会中断,且是单线程执行事务。
持久性
对于事务的执行来说,如果Redis开启AOF持久化,那么一旦事务被成功执行,事务中的命令就会通过write命令一次性写到磁盘中去,如果在向磁盘中写的过程中恰好出现断电、硬件故障等问题,可能出现只有部分命令进行AOF持久化,这时AOF文件就会出现不完整的情况,这时可使用redis-check-aof
工具将AOF文件中不完整的信息移除,确保AOF文件完整可用。
原理
Redis中每个客户端都有记录当前客户端的事务状态multiState,客户端client定义,在server.h
源码里:
c
typedef struct client {
uint64_t id; // 客户端唯一id
multiState mstate; // MULTI和EXEC状态(即事务状态)
// 省略其他属性
} client;
multiState定义如下:
c
typedef struct multiState {
multiCmd *commands; // 存储命令的FIFO队列
int count; // 命令总数
// 省略其他属性
} multiState;
multiCmd是一个队列,用来接收并存储开启事务之后发送的命令,定义如下:
c
typedef struct multiCmd {
robj **argv; // 用来存储参数的数组
int argv_len; // 数组长度
int argc; // 参数的数量
struct redisCommand *cmd; // 命令指针
} multiCmd;
类结构图:
拓展
Redis事务在真实业务场景中的应用
- 批量操作:如果你需要对多个键进行原子性修改,Redis事务可以确保这些操作在没有被中断的情况下执行。例如,批量更新用户积分、库存等;
- 乐观锁场景:在高并发环境下,使用WATCH可以监控某些关键键的变化,防止并发修改带来的数据不一致问题;
- 复杂多键操作:在需要对多个键进行依赖性操作时,比如从一个账户扣款同时向另一个账户转账,Redis事务可以确保这类操作的完整性。
局限性:
- 缺乏回滚机制:如果事务中的某个命令执行失败,Redis不会回滚已经执行的命令,这在某些需要严格数据一致性的场景中是个问题;
- 命令执行顺序无法调整:事务中的命令执行顺序是固定的,无法在运行时调整,如果某些条件改变,无法动态改变执行顺序;
- 性能开销:由于事务的执行需要在内存中排队,如果队列中的命令较多,可能导致阻塞和性能问题。