Redis通过MULTI
,EXEC
,WATCH
等命令实现事务(transaction)的功能。
事务执行过程:
开启事务(
MULTI
)依次输入Redis命令(命令入队)
提交(执行)事务(
EXEC
)
Redis事务执行过程:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET name jiejie
QUEUED
127.0.0.1:6379> SET age 18
QUEUED
127.0.0.1:6379> set sex 1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) OK
127.0.0.1:6379>
事务开始
MULTI
命令通过打开在客户端状态的flags
属性中的REDIS_MULTI
标识,将客户端从非事务状态切换至事务状态。
伪代码:
def MULTI();
# 打开事务标识
client.flags |= REDIS_MULTI
# 返回 OK 回复
replyOK();
命令入队
当客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行。客户端切换到事务状态后服务器会根据这个客户端发来的不同的命令执行不同的操作。
每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate
属性里面:
typedef struct redisClient {
// ...
// 事务状态
multiState mstate;
// ...
}redisClient;
事务状态包含一个事务队列,以及一个已入队列命令的计数器(也可以说是事务队列的长度):
typedef struct multiState {
// 事务队列,FIFO顺序
multiCmd *commands;
// 已入队列命令计数
int count;
}multiState;
事务队列是一个multiCmd
类型数组,数组中每个multiCmd
结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量:
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
}multiCmd;
事务队列以先进先出的方式保存入队命令。
执行事务
当一个处于事务状态的客户端向服务器发送EXEC
指令时,这个指令将立即被服务器执行。服务器遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行结果返回给客户端。
伪代码:
def EXEC():
# 创建空白的回复队列
reply_queue = [];
# 遍历事务队列中的每个项
# 读取命令参数、参数的个数、以及要执行的命令
for argv ,argc,cmd in client.mastate.commands:
# 执行命令,并取得命令的返回值
reply = execute_command(cmd,argv,argc)
# 将返回值追加到回复队列末尾
reply_queue.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)
WATCH 命令
WATCH
命令是一个乐观锁(optimistic locking) ,它可以在执行EXEC
命令执行之前,监视任意数量的数据库键,并在EXEC
命令执行时,检查被监视的键是否至少有一个已经被修改,如果是的话,服务器将拒绝执行事务,并向客户端返回代表执行失败的回复。
// client-1
127.0.0.1:6379> WATCH name
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set name jiejie
QUEUED
127.0.0.1:6379> set age 18
QUEUED
127.0.0.1:6379> set sex 1
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379>
// client-2
127.0.0.1:6379> set name nayeyangguang
OK
127.0.0.1:6379>
当client-1执行完set name jiejie
后,client-2执行set name nayeyangguang
修改了name的值,client-1 执行EXEC
命令时,服务器会发现WATCH
监视的键"name"
已经被修改,因此服务器拒绝执行客户端client-1的事务。
每个Redis数据库都保存着一个watched_keys
字典,这个字典的键是某个被WATCH
命令监视的数据库键,而字典的值是一个链表。链表记录中监视相应数据库键的客户端。
typedef struct redisDb {
// ...
// 正在被WATCH命令监视的键
dict *watched_keys;
// ...
}redisDb;
WATCH
字典示意图:
监视机制的触发:
所有对数据库进行修改的命令,比如:set
、lpush
、sadd
、del
、zrem
、flushdb
等等,在执行之后都会对watched_keys
字典进行检查。查看是否有客户端正在监视刚刚被命令修改过的数据库键,有的话,那么服务器会将监视被修改键的客户端的REDIS_DIRTY_CAS
标识打开,表示该客户端的事务安全性已经被破坏。
def touchWatchKey(db,key):
# 如果见key存在于数据库的watched_keys 字典中
# 那么说明至少有一个客户端在监视这个key
if key in db.watched_keys:
# 遍历所有监视键key的客户端
for client in db.watched_keys [key]:
# 打开标识
client.flags |= REDIS_DIRTY_CAS
Redis判断事务是否安全流程图:
事务的ACID性质
在Redis中,事务总是具有原子性、一致性、和隔离性,并且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性。
原子性:
事务具有原子性指的是,数据库事务中的多个操作当作一个整体来执行。服务器要么就执行事务中的所有操作,要么就一个操作也不执行。
Redis的事务和传统关系型数据库事务的最大区别在于,Redis不支持事务回滚机制。即使事务队列中某个命令在执行期间出现了错误,整个事务也会继续执行下去,知道将事务队列中的所有命令执行完毕为止。
一致性:
事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库应该仍然是一致的。
隔离性:
事务的隔离性指的是,即使数据库中有多个事务并发的执行,各个事务之间也不会相互影响,并且在并发的状态下执行的事务和串行执行的事务产生的结果完全相同。
因为Redis使用单线程的方式来执行事务(以及事务队列的命令),并且服务器保证在执行事务期间不会对事务进行中断。因此Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性。
耐久性:
事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
Redis事务的耐久性由Redis所使用的持久化模式决定:
Redis服务器在无持久化的内存模式下运行时,事务不具有耐久性。一旦服务器停机,Redis服务器数据将丢失。
Redis服务器运行在RDB(Redis DataBase),服务器只会在特定的保存条件满足是,才会执行
BGSAVE
命令,对数据库进行保存操作,并且异步执行的BGSAVE
不能保证事务数据第一时间保存到硬盘里面。因此RDB持久化的事务也不具有耐久性。Redis服务器运行在AOF(Append-only file)持久化模式下,并且
appendfsync
选项的值为everysec
,程序会每秒同步一次命令数据到硬盘。如果停机恰好发生在等待同步的那一秒钟之内,也会造成事务数据丢失。也不具有耐久性。Redis服务器运行在AOF(Append-only file)持久化模式下,并且
appendfsync
选项的值为always
,程序会在执行命令之后调用同步函数,将命令数据真正地保存到硬盘里面,这种配置下的事务具有耐久性。Redis服务器运行在AOF(Append-only file)持久化模式下,并且
appendfsync
选项的值为no
,程序会交给操作系统决定何时将命令数据同步到硬盘。事务数据可能在等待同步过程中丢失(断电),这种配置下不具有耐久性。
不论Redis在什么模式下运行,在一个事务的最后加上SAVE
命令总可以保证事务的耐久性。不过这种做法的效率太低,不具有实用性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。