弃之如可惜,食之无所得
一.Redis事务简述
Redis的事务是下面4个命令来实现
1.multi,开启Redis的事务,置客户端为事务态。
2.exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
3.discard,取消事务,置客户端为非事务态。
4.watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。
将是否有watch命令分为普通类型事务和CAS(Check And Set)类型事务,无watch命令的为普通类型事务,有watch命令的为CAS类型事务,下面为普通类型事务演示
[img]http://dl2.iteye.com/upload/attachment/0111/1223/7b570d1e-8fc4-3f12-9b91-d85e2ea10e78.png[/img]
事务失败的原因可以分为静态错误(如不存在的命令)和运行时错误(如cas错误、对string用lpop操作)。静态错误会在提交exec时返回错误信息,使事务不能执行,而除cas以外的运行时错误不会阻止事务继续执行。
为方便后面的理解,可以勉强将Redis状态分为四种,普通状态、正常事务状态、CAS错误事务状态和静态错误事务状态,这四种状态在Redis源码中用3个状态位(REDIS_MULTI、REDIS_DIRTY_CAS、REDIS_DIRTY_EXEC)来标识,下面是个状态转换图。
[img]http://dl2.iteye.com/upload/attachment/0111/1225/a49162ac-bb87-34a0-8872-465393a38130.png[/img]
当Redis处于事务态时,除multi、exec、discard和watch以外的所有命令皆会放到事务队列,向客户端返回QUEUED。
二.Redis事务实现
1.普通类型事务
Redis的事务实现比较简单,真的可以理解为"一个打包的批量执行脚本"。
当用户输入multi命令时,用户将本客户端标识位REDIS_MULTI置为1,所有的命令将会检查这个标识位,如果为1则将命令放到本客户端的命令队列中,如果非1则将命令直接执行。
每个客户端redisClient中皆有一个结构体存储着本客户端命令队列的状态,结构体如下:
typedef struct multiState {
/*命令组成的数组*/
multiCmd *commands;
/*命令的个数*/
int count;
} multiState;
每个命令有下面的结构体标识
typedef struct multiCmd {
/*命令的参数*/
robj **argv;
/*命令的参数个数*/
int argc;
/*命令的指针*/
struct redisCommand *cmd;
} multiCmd;
我们上面演示事务的例子在Redis中的示例
[img]http://dl2.iteye.com/upload/attachment/0111/1227/86dd241e-5cc9-33da-b326-a4b837e48e3a.png[/img]
当客户端输入exec,将commands数组执行一边,整个事务就完成了,是不是简单的快哭了。
如果用户在正常事务态输入错误的命令,Redis会将对应客户端置为静态错误事务状态,在代码中表现为将用户端redisClient的REDIS_DIRTY_EXEC标识位置为1;如果用户watch的key变化,将变为CAS错误事务状态,在代码中表现为将用户端redisClient的REDIS_DIRTY_CAS标识位置为1。当用户exec提交事务时,Redis首先检测用户这两个标志位,任何一个被置为1,则不执行队列中的命令,直接给用户返回错误。
exec提交事务以后,会调用discard进行清理,包括重置标志位,释放所有事务状态相关的资源(释放命令队列、参数等)。
2.CAS类型事务
CAS类型事务整个的事务执行逻辑和普通类型事务一样,只不过在普通类型事务增加watch功能而已。
watch功能相当于乐观锁,用户在进入事务态之前,监控任意数量的数据库键,当提交事务时,检测这些键是否被更改。如果被更改则拒绝用户的事务,并返回用户空数据。
在代码中实现即为事务提交前会检测用户端redisClient的REDIS_DIRTY_CAS标识位。
每个RedisDB都保存一个watched_keys的字典,key为数据库键,value为用户端redisClient,当“触碰”一个key时,此key对应的value(即redisClient)的REDIS_DIRTY_CAS将置为1,value中存储的这个/这些客户端在执行 exec 时事务将失败,下图为watched_keys在Redis中的示例。
[img]http://dl2.iteye.com/upload/attachment/0111/1231/706bcb6c-2e7c-3d97-bb5a-c54384f462f4.png[/img]
当其他客户端更新此数据库的year时将导致客户端clientA和clientC的事务提交失败,代码实现即为遍历year的value链表,将链表中的redisClient的REDIS_DIRTY_CAS将置为1。
三.总结
Redis的事务还比较原始,加之Redis的持久化容忍数据丢失,阻挡利用事务来达到精确数据操作的业务。
Redis的不支持回滚更是硬伤,这也导致很多人认为Redis的事务并不满足事务原子性。