关系型数据库一般都支持事务,简单来说,事务允许请求提交的批量执行,且保证全部成功或全部失败。对于Redis来说,它也提供了对事务的简单实现和支持,下面来了解下。
事务实现
Redis通过watch、multi、exec
命令来实现事务功能。它实现了一次性、按顺序执行一系列命令,保证在执行期间不受其他变更影响的机制。
事务命令
watch
数据存储
watch
监听某个Key状态,对其进行监听标记以确保在事务操作过程中不会被其他操作修改破坏事务完整性。
typedef struct redisDb {
// 省略其他信息
// 正在被 WATCH 命令监视的键
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
} redisDb;
如上,关于被监视的Key是被存储在
redisDb
的
watched_keys
字典数组中的,字典中key1,key2是被监视的键,字典中的值则以一个
链表
存储
redisClient
结构引用,通过如此映射来表示哪些客户端在对哪些键Key进行监听。
触发机制
/* "Touch" a key, so that if this key is being WATCHed by some client the
* next EXEC will fail.
*
* “触碰”一个键,如果这个键正在被某个/某些客户端监视着,
* 那么这个/这些客户端在执行 EXEC 时事务将失败。
*/
void touchWatchedKey(redisDb *db, robj *key) {
list *clients;
listIter li;
listNode *ln;
// 字典为空,没有任何键被监视
if (dictSize(db->watched_keys) == 0) return;
// 获取所有监视这个键的客户端
clients = dictFetchValue(db->watched_keys, key);
if (!clients) return;
/* Mark all the clients watching this key as REDIS_DIRTY_CAS */
/* Check if we are already watching for this key */
// 遍历所有客户端,打开他们的 REDIS_DIRTY_CAS 标识
listRewind(clients,&li);
while((ln = listNext(&li))) {
redisClient *c = listNodeValue(ln);
c->flags |= REDIS_DIRTY_CAS;
}
}
如上是touchWatchedKey
方法,作用是对数据库中监听键Keywatched_keys
进行数据修改标识,具体是通过修改watched_keys
链表中对象redisClient
的flags
属性来进行标记,当有被监听键被修改时,会将其客户端标记REDIS_DIRTY_CAS
,表示该客户端监听键已被修改。
这里的touchWatchedKey
方法是一个旁路方法,当Redis服务端接收到set、lpush、zadd
等数据变更命令时会对其进行触发。
事务执行
watch
是一个
乐观锁
的实现方式,当多个Client客户端对同一个数据库键Key进行监听时,会将所有监听的客户端和键映射关系进行保存,并不会Client客户端在未提交事务过程中进行变更监听键Key的阻塞或拒绝,而是当Client客户端真正提交时才进行事务状态的检查,当发现数据变更时才拒绝事务提交,否则会执行成功。
multi
multi
开启事务,类似关系型数据库中的begin。执行时会将redisClient
的flags
标记为REDIS_MULTI
,表明客户端已开启事务。
discard
discard
取消事务,类似关系型数据库中的rollback。操作会清空事务队列中所有入队命令,并取消客户端事务状态。
exec
exec
执行事务,类似关系型数据库中的commit。操作会提交事务队列中所有入队命令,并取消客户端事务状态。
执行流程
事务执行分为三个阶段: 事务开始、命令入队、事务执行(事务丢弃)
- 事务开始 标记客户端已开启事务状态
- 命令入队 当开启事务状态后,所有后续执行命令会被放入到一个
FIFO
队列中,此时命令均不会被执行,等待事务执行命令发起后按序执行 - 事务执行 当发起事务执行时,Redis会将之前放入命令队列中的命令取出,按照存入顺序依次执行
- 事务丢弃 当发起事务丢弃时,Redis会清空命令队列,将客户端事务标记取消
网络异常,图片无法展示| - 当Redis服务端收到客户端标记为事务状态开启时,会立即执行
multi、watch、discard、exec
等事务命令 - 当Redis服务端收到客户端标记为事务状态开启时,命令是非以上事务命令则会将命令放入事务命令队列中,返回客户端
QUEUED
表明事务命令已入队 - 当不是以上情况,则不是事务状态,正常执行命令即可。
数据构成
typedef struct redisClient {
// 这里省略大部分其他信息
// 客户端状态标志
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// 被监视的键
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
} redisClient;
如上,redisClient
是客户端数据结构,Redis会为每一个客户端保存诸多信息。
flags
记录客户端状态标志,若包含REDIS_MULTI
标记则表明客户端在事务状态下mstate
记录事务状态是exec
执行还是multi
开启watched_keys
是watch操作后被监视的键
/*
* 事务状态
*/
typedef struct multiState {
// 事务队列,FIFO 顺序
multiCmd *commands; /* Array of MULTI commands */
// 已入队命令计数
int count; /* Total number of MULTI commands */
} multiState;
commands
存储的是事务状态下入队的命令集合,这里是一个FIFO
顺序的数组,先入队会被优先执行。count
记录入队的命令数量
/*
* 事务命令
*/
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;
multCmd
存储了事务命令的具体描述,argv
是一个以字符串对象存储的命令数组,argc
是参数数量,cmd
指向命令执行。
异常处理
在事务执行过程中,一般会有入队错误、执行错误两种错误存在。
- 入队错误 Redis会对入队命令进行检查,一旦有异常则返回提示,当事务提交时则拒绝队列命令执行,所有命令都不会提交
- 执行错误 对于逻辑错误的命令,如键Key是列表对象,而对其进行字符串对象操作,这种多是出现在应用程序运行时,而Redis本着Keep it simple的设计理念,并不会在命令入队阶段对数据类型进行检查,这完全可以在应用程序中进行避免。与入队错误不同的时,执行错误只会对错误的执行提交失败,其余正常命令无论顺序如何均会被正常提交。
入队错误
当事务未执行、命令入队阶段出现命令错误,则会进行提示,当事务提交时会直接拒绝,命令队列中的命令均不会生效。如下:
执行错误
ACID探讨
通过以上对Redis事务机制实现的剖析,下面对Redis中事务ACID特性进行分别探讨和总结。
原子性(Atomic)
Redis可以multi
标记客户端事务状态,通过命令队列来暂存所有事务开启期间的命令,使用watch
命令实现了对特定数据基于乐观锁实现的保护,在事务提交阶段检查客户端事务状态、检查监听数据变更情况从而来确保事务执行和提交过程中的事务完整性,由于Redis是工作在单线程环境下的,所有操作都可以确保顺序性,它的单线程设计确保了它无需考虑因竞态出现的数据不一致问题。
对于执行错误,只能确保正确命令可以被正确执行,而错误命令会被忽略,这需要上层应用使用时进行逻辑校验和容错
隔离性(Isolation)
Redis与如MySQL这种关系型数据库不同的一点是,无法支持多种维度的会话间的数据可见性。当在事务状态未提交,命令都会暂存命令队列,而此时事务会话中的变更对于其他事务会话是既不可见也未提交,只有在执行exec
才会真正进行事务提交。watch
命令提供了数据库键Key的监听功能,它相当于给不同的多个事务会话提供了一种监听通信的能力来察觉数据变更,但是它仅能监测已提交数据变更来保护事务完整性,却不能向MySQL那样提供不同隔离级别来察觉其他事务会话未提交或已提交数据变化。
持久性(Durability)
Redis中事务的持久性依赖于自身的AOF
或RDB
的开启和持久化机制,即使出现极端情况下的异常,只要将AOF
或RDB
文件持久化刷盘到磁盘上,那么事务操作便具有持久性了,在重启后也会自动加载到内存中进行还原。
一致性(Consistency)
数据一致性可以参考持久性部分。
总结Redis的ACID特性的话,相比MySQL这种关系型数据库的事务保证来说,还是要单薄和简单一些,毕竟Redis的定位和使用场景不同,设计复杂度也便不同,事务实现并不是它的第一要义。但是这并不影响我们了解Redis的事务实现机制和细节,透过内部实现来因场景来使用做事务取舍。
参考
《Redis设计与实现》