写在前面

Redis 通过 MULTIEXECWATCH 等命令来实现事务功能。Redis的事务是将多个命令请求打包,然后一次性、按照顺序的执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而该去执行其他客户端的命令请求。 就像下面这样:

redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> SET fantwo 2
QUEUED
redis(TX)> GET fanone
QUEUED
redis(TX)> EXEC
1) OK
2) OK
3) "1"

本文我们就从redis的事务执行过程以及ACID四个方面来介绍redis的事务

事务实现

从上面的例子我们可以知道,redis的事务是从MULTI命令开始的,所有的命令都会按照FIFO的顺序进入一个QUEUE队列中,当执行EXEC操作后才将这些命令逐步执行。

MULTI

事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量:

typedef struct multiCmd{
 robj **argv;  // 参数
 int argc;   // 参数数量
 struct redisCommand *cmd // 命令指针
multiCmd;

事务队列以FIFO先进先出的方式保存入队的命令,还是用上面的例子来画一个原型图:

当一个处于事务状态的客户端向服务器发送EXEC命令的时候,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。EXEC的伪代码如下:

void EXEC() {
    std::vector<Reply> reply_queue;  // 创建空白的回复队列
    for (const auto& cmd : client.mstate.commands) { // 执行事务中的所有命令
        Reply reply = execute_command(cmd.command, cmd.argv, cmd.argc);
        reply_queue.push_back(reply);
    }
    client.flags &= ~REDIS_MULTI; // 清理事务状态
    client.mstate.count = 0;
    release_transaction_queue(client.mstate.commands);
    send_reply_to_client(client, reply_queue);// 发送回复给客户端
}

WATCH

接着我们来讲讲WATCH命令,其实这是一个乐观锁(optimistic locking),可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否有已经被修改过了的,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复

比如下面这个例子:

时间
客户端A
客户端B
T1
WATCH “fanone”

T2
MULTI

T3
SET “fanone” 1

T4

SET “fanone” 2
T5
EXEC

而在时间T4,客户端B修改了fanone键的值,当客户端A在T5执行EXEC命令的时候,服务器会发现WATCH监视的键fanone已经被修改了,因此服务器拒绝执行客户端A的事务,并且向客户端A返回空回复

每个Redis数据库都保存着一个watched_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视数据库键的客户端。

typedef struct redisDb {
 dict *watched_keys; // 正在被WATCH命令监视的键 
}
watched_keys

ACID

Redis的事务是否符合ACID特性呢?

图解 Redis 事务|源码解析|ACID|EXEC、WATCH、QUEUE

原子性 Atomicity

原子性

事务具有原子性是指,数据库将事务中的多个操作当作一个整体来执行,要么全部执行,要么全部不执行。对于Redis的事务功能来说,事务队列中的命令要么就都全部都执行,要么就一个都不执行,因此Redis的事务是具有原子性的。 比如以下成功执行的事务,事务中的所有命令都被执行:

redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> GET fanone
QUEUED
redis(TX)> EXEC
1) OK
2) "1"

与此相反,如果其中有一个命令是错误的,那么整个命令就不会执行。fanone这个key是一个string,并不是set,所以不能使用SADD命令执行。

redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> SADD fanone 2
QUEUED
redis(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value

那么大家会发现Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制,也就是rollback。

Redis的作者在事务功能的文档中也解释道,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符。

redis官网

一致性 Consistency

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

比如执行完事务不会多一个之前没有的命令,或者某个key是string类型,不会变成set类型。我们用上面同一个例子来说明:

redis> MULTI
OK
redis(TX)> SET fanone 1
QUEUED
redis(TX)> SADD fanone 2
QUEUED
redis(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value

fanone 这个key是string类型,但是无法通过SADD命令将fanone变成set类型。虽然在入队列的时候,redis没有报错,但是在EXEC的时候,redis报了,所以这个key一开始是string类型,事务执行完后也会是string类型,事务执行前后保持了一致。

一致性

隔离性 Isolation

事务的隔离型是指即使数据库中有多个事务并发的执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和穿行执行的事务的结果是完成相同的。

隔离性

因为Redis是使用单线程的方式来执行事务以及事务队列中的命令,并且在服务器稳定的情况下,执行事务不会中断,因此,redis的事务总是串行的方式执行的,所以具备隔离性。

持久性 Durability

事务的持久性值得是当一个事务执行完毕的时候,执行这个事务所得的结果已经被保存到永久性存储介质里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。

由于Redis的事务比较简洁,没有提供持久化的能力,所以Redis的事务是依赖于Redis所使用的持久话模式,也就是AOF、RDB,我们一个个来讨论

  • 当服务器无持久化运行的时候,事务不具备持久性,一旦服务器宕机,事务数据将会丢失。

  • 当服务器在RDB持久化模式下运作的时候,服务器只会在特定的保存条件下满足,比如使用BGSAVE命令,队数据库进行保存,但是异步执行的BGSAVE也不能保证,第一时间保存在硬盘中,因此RDB持久化模式下事务不具备持久性

  • 而当服务器运行在AOF持久化模式下,并且appendfsync选项是always的时候,服务器总会在执行完命令之后调用同步sync函数,将命令数据保存在硬盘中,而此时事务具备持久性,其他选择比如everysec或者no的时候都不具备持久性。

appendfsync的三种情况