事务

1 事务简介

Redis通过MULTIEXECWATCH等命令实现事务(transaction)功能。事务提供一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制。在事务执行期间,服务器不会中断事务去执行其他客户端的命令请求。

事务以MULTI开始,接着是多个命令放入事务之中,最后由EXEC将这个事务提交(commit)到服务器执行。

一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。

事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。

Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。

事务队列

每个Redis客户端都有自己的事务状态,保存在客户端状态的mstate属性中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct redisClient {
multiState mstate;
} redisClient;

typedef struct multiState {
// 事务队列,FIFO顺序
multiCmd *commands;

// 已入队命令计数
int count;
} multiState;

typedef struct multiCmd {
// 参数
robj **argv;

// 参数数量
int argc;

// 命令指正
struct redisCommand *cmd;
} multiCmd;

事务实现

一个事务从开始到结束经历三个阶段:

  1. 事务开始
  2. 命令入队
  3. 事务执行

事务开始

MULTI命令标志着事务的开始,它将客户端从非事务状态切换到事务状态,即打开客户端状态的flags属性的REDIS_MULTI标识:

1
2
3
def MULTI():
client.flags |= REDIS_MULTI
replyOK()

命令入队

客户端切换到事务状态后,服务器会根据不同的命令执行不同的操作:

  • EXECDISCARDWATCHMULTI其中一个,服务器立即执行该命令。
  • 否则,服务器将命令放入一个事务队列,然后向客户端返回 QUEUED 回复。

执行事务

服务器收到EXEC命令后,会遍历客户端的事务列表,执行其中的所有命令。最后将执行所得的结果返回给客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def EXEC():
# 创建空白的回复队列
reply_queue = []

# 遍历事务列表中的每个项
for argv, argc, cmd in client.mstate.commands:

# 执行命令
reply = execute_command(cmd, argv, argc)
reply_quque.append(reply)

# 移除 REDIS_MULTI 标识
client.flags &= ~REDIS_MULTI

# 清空客户端的事务状态,清零计数器,释放事务队列
client.mstate.count = 0
release_transaction_queue(client.mstate.commands)

send_reply_to_client(client, reply_queue)

2 WATCH命令的实现

WATCH命令是个乐观锁,它可以再EXEC执行之前,监视任意数量的数据库键,并在EXEC执行时,检查被监视的键是否至少有一个已经被修改过了。如果是,服务器将拒绝执行事务,并返回客户端事务执行失败的空回复。

使用 WATCH 命令监视数据库键

每个Redis数据库都保存了一个watched_keys字典,键是某个被WATCH的数据库键,值是一个链表,记录了所有监视该键的客户端:

1
2
3
typedef struct redisDb {
dict *watched_keys;
} redisDb;

监视机制的触发

所有对数据库进行修改的命令,执行之后都会调用multi.h/touchWatchKey函数对watched_keys字典进行检查。如果被监视的键被修改,那么打开监视该键的客户端的REDIS_DIRTY_CAS标识,表示该客户端的事务安全性已遭破坏。

判断事务是否安全

服务器收到EXEC命令后,根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务。

悲观锁与乐观锁

我正在买票ticket -1 , money -100而票只有1张, 如果在我multi之后,和exec之前, 票被别人买了,即ticket变成0了.我该如何观察这种情景,并不再提交

悲观的想法:

世界充满危险,肯定有人和我抢, 给 ticket上锁, 只有我能操作. [悲观锁]

乐观的想法:

没有那么人和我抢,因此,我只需要注意,
–有没有人更改ticket的值就可以了 [乐观锁]

Redis的事务中,启用的是乐观锁,只负责监测key没有被改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

具体的命令---- watch命令

redis 127.0.0.1:6379> watch ticket
OK
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> decr ticket
QUEUED
redis 127.0.0.1:6379> decrby money 100
QUEUED
redis 127.0.0.1:6379> exec
(nil) // 返回nil,说明监视的ticket已经改变了,事务就取消了.
redis 127.0.0.1:6379> get ticket
"0"
redis 127.0.0.1:6379> get money
"200"

watch key1 key2 ... keyN
作用:监听key1 key2..keyN有没有变化,如果有变, 则事务取消

unwatch
作用: 取消所有watch监听

3 事务的ACID性质

Redis的事务总是具有原子性(atomicity)、一致性(consistency)、隔离性(isolation),且当Redis运行在某种特定的持久化模式下,事务也具有耐久性(durability)。

原子性

事务的原子性是指,事务中的多个操作当做一个整体来执行,要么执行所有,要么一个也不执行。

Redis的事务与传统关系型数据库事务的区别在于,Redis不支持事务的回滚机制(rollback),即使事务队列中的某个命令执行出现错误,整个事务也会继续执行下去,直到所有命令执行完毕。

一致性

事务的一致性是指,如果数据库在事务执行前是一致的,那么执行后,无论事务是否执行成功,数据库也应该是一致的。「一致」是数据符合数据库本身的定义和要求,没有包含非法或无效的错误数据。

Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。

  1. 入队错误。
    如果事务在入队命令的过程中,出现了命令不存在,或者命令格式不正确等情况,Redis会拒绝执行该事务。

  2. 执行错误。
    执行过程中的错误是不能再入队时被服务器发现的,这些错误只会在命令实际执行时被触发。事务的执行过程中出现错误,服务器也不会中断事务的执行,而是继续执行其他命令,一致性的命令不会被出错的命令影响。

  3. 服务器停机。
    执行事务的过程中停机,不管服务器使用的何种持久化模式,Redis总能保持重启后的数据库一致性。

隔离性

事务的隔离性是指,即使数据库中有多个事务并发执行,各个事务之间不会相互影响,且与串行执行的结果相同。

Redis采用单线程执行事务,所以事务总是以串行的方式执行,也当然具有隔离性。

持久性

事务的持久性是指,一个事务执行完毕后,结果已经被保存到永久性存储介质中。即使服务器停机,执行事务所得的结果也不会丢失。

Redis没有为事务提供额外的持久化功能,事务的持久化由Redis使用的持久化模式决定的:

  • 无持久化:事务不具持久性,一旦停机,所有服务器的数据都将丢失。
  • RDB持久化:只有执行BGSAVE才会对数据库进行保存,且异步执行的BGSAVE不能保证事务数据在第一时间被保存。因此RDB持久化也不能保证事务的持久性。
  • AOF持久化,且appendfsync选项为always时:程序执行命令后会调用同步操作,将命令数据保存到硬盘。这时事务是有持久性的。
  • AOF持久化,且appendfsync选项为everysec时:每秒一次同步命令数据到硬盘,事务也不具有持久性。
  • AOF持久化,且appendfsync选项为no时:程序交由操作系统来决定何时同步到硬盘,事务也不具有持久性。