redis 通过 multi、watch、exec等命令来实现事务功能。

redis提供这样一种将多个待执行的命令入队,在事务开始之后执行入队的事务命令,即使事务队列中部分命令执行失败也不会中断事务,事务队列中的所有命令执行完毕后,才会去请求其他客户端的命令请求。

事务的实现

redis中事务从开始到结束有如下三个阶段:

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

命令开始

redis中,客户端使用multi命令标识当前客户端进入事务状态。
该命令将执行该命令的客户端从非事务状态切换到事务状态#define CLIENT_MULTI (1<<3)

1
2
3
4
typedef struct client {
uint64_t id; /* Client incremental unique ID. */
uint64_t flags; /* Client flags: CLIENT_* macros. */
}

命令入队

客户端处于非事务状态时,这个客户端发送的命令将会被服务端立即执行。
而客户端处于事务状态时,服务器会根据这个客户端发来的不同命令执行不同的操作:

  1. 如果是 multi、exec、discard、watch四个命令中的一个,服务器立即执行。
  2. 否则,将命令放入事件队列中,向客户端返回QUEUED回复。

事务队列

客户端中使用multiState mstate保存事务队列,其定义如下

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct multiState {
multiCmd *commands; // 事务队列,记录命令
int count; // 已入队的命令计数
int cmd_flags; /* The accumulated command flags OR-ed together.
So if at least a command has a given flag, it
will be set in this field. */
int cmd_inv_flags; /* Same as cmd_flags, OR-ing the ~flags. so that it
is possible to know if all the commands have a
certain flag. */
size_t argv_len_sums; /* mem used by all commands arguments */
int alloc_count; /* total number of multiCmd struct memory reserved. */
} multiState;

multiState中记录了事务命令,事务命令个数。其中,事务队列是一个FIFO队列。
commands则保存了每个入队的命令:

1
2
3
4
5
6
typedef struct multiCmd {
robj **argv; // 参数
int argv_len; // 参数个数
int argc; // 参数数量
struct redisCommand *cmd; // 命令指针
} multiCmd;

客户端执行如下命令:

1
2
3
4
5
6
7
8
9
10
redis> MULTI
OK
redis> SET "name" "Practical Common Lisp"
QUEUED
redis> GET "name"
QUEUED
redis> SET "author" "Peter Seibel"
QUEUED
redis> GET "author"
QUEUED

事务执行

处于事务状态的客户端向服务器发送exec命令时,该命令将被立即执行。服务端遍历客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果返回给客户端。

watch 命令的实现

watch命令是乐观锁,在执行exec命令之前,监视任意数量的数据库键,在执行EXEC命令时,检查被监视的键是否至少有一个已经被修改过,如果是,服务器将拒绝执行事务,并向事务所在的客户端发送代表事务执行失败的空返回。

watch 监视 键

数据库中使用watch_keys字典记录数据库中的键,其中字典key为被watch命令监视的数据库键,字典值为所有监控相关数据库键的客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for
* data, and should be unblocked if key is deleted (XREADEDGROUP).
* This is a subset of blocking_keys*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

监视触发

所有对数据库进行修改的命令,比如set、lpush、sadd、zrem,del在执行之后都会调用multi.c/touchKey函数对watched_keys进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有,将当前客户端的flags标识为#define CLIENT_DIRTY_CAS (1<<5)

判断事务是否安全

当服务端收到户端发送exec命令时,服务器将根据这个客户端的flags是否打开了CLIENT_DIRTY_CAS标识来决定是否执行事务。

  1. 如果CLIENT_DIRTY_CAS被打开,则说明客户端监视的数据库键中至少有一个已经被修改了,客户端提交的事务不再安全,服务器拒绝执行客户端提交的事务。
  2. 否则,认为事务安全,继续执行事务队列里的命令。

疑问质疑?

集群模式下是否不支持事务?
1
2
3
4
5
6
7
8
9
10
11
12
13
182.168.168.238:6379> multi
OK
182.168.168.238:6379(TX)> set name mqray
QUEUED
182.168.168.238:6379(TX)> get name
QUEUED
182.168.168.238:6379(TX)> set height 181
QUEUED
182.168.168.238:6379(TX)> get height
QUEUED
182.168.168.238:6379(TX)> exec
(error) CROSSSLOT Keys in request don't hash to the same slot
182.168.168.238:6379>

(集群模式下使用事务遇到的问题)
的确如此!

redis集群模式下能否支持事务?

redis集群模式下支持事务

引用

1. 事务的实现
2. redis集群模式下能否支持事务