事务

Redis通过MULTIEXECWATCH等命令实现事务功能。

事务是将多个命令打包,然后原子地按顺序地执行的机制,执行期间服务器不会中断事务执行其他客户端的命令请求。下面展示了一次完整事务的执行命令:

1
2
3
4
5
6
7
8
9
redis> MULTI
OK
redis> SET "name" "The Design and Implementation of Redis"
QUEUED
redis> GET "name"
QUEUED
redis> EXEC
1) OK
2) "The Design and Implementation of Redis"

事务的实现

可以看出,事务主要有3个阶段:事务开始、命令入队、事务执行。

事务开始

MULTI命令表示事务的开始,将客户端从非事务状态切换为事务状态,在flags属性中打开REDIS_MULTI标识

命令入队

当客户端处于事务状态时,命令不会被立即执行(除了EXEC、DISCARD、WATCH、MULTI),而是加入事务队列。

事务队列

客户端的事务状态保存在mstate里:

1
2
3
4
5
typedef struct redisCLient{
//事务状态
multiState mstate;
...
} redisClient;

事务状态包括事务队列入队命令计数器

1
2
3
4
5
6
typedef struct multiState{
//数组,事务队列
multiCmd *commands;
//入队命令计数器
int count;
} multiState;

事务队列的实例结构:

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

先入队的命令先放入数组,后入队的后放入。

执行事务

当收到客户端的EXEC命令时,将立即执行,然后服务器遍历客户端的事务队列,保存命令,执行命令,返回结果给客户端,最后移除事务标识。

WATCH命令的实现

WATCH命令是一个乐观锁,可以在EXEC前监视任意数量的键,如果在EXEC执行时,发现这些被监视的键被修改过,服务器将拒绝执行事务。

使用WATCH命令监视数据库键

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

1
2
3
4
5
typedef struct redisDb{
//正在被WATCHED命令监视的键
dict *watched_keys;
...
} redisDb;

监控机制的触发&事务安全

在执行数据库修改命令时,都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视的刚被命令修改过的键,如果有,将watched_keys该键对应的值,也就是监听的客户端都打开REDIS_DIRTY_CAS标识,表示事务的安全性已经被破坏。此时,服务器拒绝执行该客户端的事务。

事务的ACID性质

Redis的事务有原子性、一致性和隔离性,当Redis运行在特定的持久化模式下时,才具有持久性。

原子性

Redis事务队列中的命令,要么全部都执行,要么一个都不执行,因此,具有原子性。Redis进行事务命令入队时,如果命令入队出错,会被拒绝执行。但是命令的语法错误(执行错误),不会导致整个命令不被执行,也就是说Redis不支持事务的回滚机制。

下面例子表示发生入队错误(一致性时将提到入队错误和执行错误)时,事务中的所有命令都不会被执行:

1
2
3
4
5
6
7
8
9
10
redis> MULTI
OK
redis> SET msg "he1lo"
QUEUED
redis> GET
(error) ERR wrong number of arguments for 'get' command
redis> GET msg
QUEUED
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

发生执行错误,不影响其他命令的执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
redis> SET msg "hello" # msg键是一个字符串
OK
redis> MULTI
OK
redis> SADD fruit "apple" "banana" "cherry"
QUEUED
redis> RPUSH msg "good bye" "bye bye" #错误地对字符串键msg执行列表键的命令
QUEUED
redis> SADD alphabet "a" "b" "c"
QUEUED
redis> EXEC
1) (integer) 3
2)(error) WRONGTYPE Operation against a key holding the wrong kind of value
3)(integer) 3

不支持事务回滚是考虑到了复杂性,与其简单高效的理念不符,并且Redis的设计者认为,Redis事务的执行时错误通常都是编程错误产生的,在开发环境中会有,但生产环境不应该出现,因此,没有设计回滚机制。

一致性

一致性表示在事务的执行前后,成功与否,数据库都是一致的,也就是数据符合数据库本身定义和要求,没有非法或无效错误数据

Redis通过简单的错误检测来保证一致性。

  1. 入队错误

在2.6.5之后的版本,如果一个事务在入队时出现了命令不存在,Redis则拒绝执行这个事务。

  1. 执行错误

对于命令执行期间发现的错误,不会影响其他命令的执行。服务器会识别出错的命令,并进行相应处理,这些命令不会对数据库做修改,不影响一致性。

  1. 服务器停机

如果Redis在执行事务过程中停机,数据也是一致的。如果没有开启持久化,重启后数据库是空白的。开启持久化后,重启后会还原到一致状态。

隔离性

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

Redis是单线程的,并且服务器保证在执行事务期间不会对事务进行中断,因此Redis的事务总是以串行的方式运行,是具有隔离性的。

持久性

持久性的意思是,事务执行的结果被永久性地保存,执行事务的结果不会丢失。

因为Redis没有单独为事务队列提供持久化功能,所以取决于持久化模式,只有AOF方式持久化并且appendsync的值为always,而且没有打开no-appendfsync-on-rewrite时,才具有持久性。因为其他方式并不能保证事务的执行结果被第一时间保存到硬盘里。

注:no-appendfsync-on-rewrite打开后,在执行BGSAVEBGREWRITEAOF时会暂停对AOF文件的同步。

慢查询日志

Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求。可通过两个参数配置:

  • slowlog-log-slower-than:执行时间超过多少微秒的命令会被记录到日志上。
  • slowlog-max-len:指定服务器最多保存多少条慢查询日志,超过时会删除最久的那条日志。

可以使用CONSIG SET slowlog-log-slower-than <microsecond>直接修改配置,使用SLOWLOG GET查询慢查询日志

慢查询记录的保存

相关慢查询日志的属性记录在redisServer中:

1
2
3
4
5
6
7
8
9
10
11
12
struct redisServer {

//下一条慢查询日志的ID,初始为0,每产生一条就加1
long long slowlog_entry_id;
//保存了所有慢查询日志的链表
list *slow1og;
//服务器配置slowlog-log-slower-than选项的值
long 1ong slowlog_1og_slower_than;
//服务器配置slowlog-max-len选项的值
unsigned long slowlog_max_len;
// ...
};

slowlog是一个链表,有几个节点就表示有几条慢查询日志,节点是一个slowlogEntry实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct slow1ogEntry {
//唯一标识符
long 1ong id;
//命令执行时的时间,格式为UNIX时间戥
time_t time;
//执行命令消耗的时间,以微秒为单位
1ong long duration;
//命令与命令参数
robj **argv;
//命令与命令参数的数量
int argc;
} slowlogEntry;

新添加的日志会被放到slowlog链表的表头。

监视器

执行MONITOR命令,客户端就成为了监视器,实时接收并打印服务器处理的命令。当其他客户端发送请求时,服务器除了执行,还会将相关信息发送给所有监视器。

成为监视器

redisServer中有monitors链表,记录所有成为监视器的客户端。如果某个客户端发送MONITOR命令,就会打开它的REDIS_MONITOR标志,并将其插入到该链表的尾部

向监视器发送命令信息

服务器处理命令前都会调用replicationFeedMonitors函数,将相关信息发送给各个监视器。主要是封装要发送给监视器的信息、遍历监视器、发送信息这三步。

总结

本文的重点是Redis的事务,其他的作以了解。