前言
对于Redis事务,大家可能知道multi、exec、discard、watch这四个命令,并且也知道通过这四个命令去运用事务。本章不会讲解事务的基础知识,而是讲解Redis事务的源码实现,让你在业务中运用事务更了然于心。很多文章都会讲解,使用watch配合事务是可以实现乐观锁,但是下面会详细讲解为什么可以?相信你的收获会超出你的想象。
一、事务是什么?
1.事务的特性ACID这里不过多描述,网上很多。
2.Redis的事物就是一次性的执行一批命令,这些命令不会被其他命令所打断,等事务中的所有命令都处理完,才会执行其他命令。
这里抛出来一个问题,Redis客户端的Pipeline功能也可以批量执行命令,这个可以代替事务吗?为什么不能代替事务?这里强调一个大多数人都会有的错误认知,Pipeline是客户端实现的功能,不是Redis服务器实现的功能。Pipeline是客户端实现的功能。Pipeline是客户端实现的功能。重要的事情说三遍~
二、代码实现
// 这就是一个简单的事务使用。
// 下面沿用这个思路来看Redis的源码实现。
watch name
multi
set name zhangsan
get age
exec
multi命令的实现
// 客户端的flags属性就是一个标记客户端状态的(不懂可以忽略,看到下面就会懂)。
void multiCommand(client *c) {
// 不允许嵌套使用multi命令,
// 因为执行multi命令后会把flags的CLIENT_MULTI状态打开
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
// 打开客户端flags的CLIENT_MULTI状态打开
c->flags |= CLIENT_MULTI;
addReply(c,shared.ok);
}
为什么执行完multi,执行除了事务的这几个命令,其他命令会返回一个QUEUE?
// 如果客户端打开了CLIENT_MULTI状态,也就是执行了multi命令。
// 除了exec、discard、watch、multi命令可以正常执行,其他命令都放到待执行队列里了。
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) {
// 把命令放到队列里
queueMultiCommand(c);
// 放到队列里后,给客户端返回一个QUEUE
// shared.queued这个就是QUEUE的字符串常量
addReply(c, shared.queued);
} else {
// call()真正执行命令的地方
call(c, CMD_CALL_FULL);
}
队列的具体实现对于理解事务不重要,实现源码会放在后面,感兴趣的小伙伴可以看看。
exec命令的实现
void execCommand(client *c) {
// 在该客户端不断往队列里放命令时,有其他客户端更改了watch的值,
// 会把客户端的CLIENT_DIRTY_CAS标记打开。
// 在该客户端往队列里放命令时,有语法错误的命令,
// 会把客户端的CLIENT_DIRTY_EXEC标记打开。
// 如果这两个标记被打开,exec会返回一个错误。
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
// 删除事务中的所有数据
discardTransaction(c);
return;
}
// 删除watch相关的数据
unwatchAllKeys(c);
for (j = 0; j < c->mstate.count; j++) {
// 循环执行队列里的命令
call(c,CMD_CALL_FULL);
}
}
watch命令的实现
watch命令就是把key存储到客户端和服务端用于保存watch数据的数据结构中。因为代码很简单,就是麻烦了一些,所以不展示实现代码了,看下面的图更清晰一些。
1.每个客户端执行watch命令都会把自己watch的key放到自己的客户端watch结构中,这个结构是双向链表。也会把watch的key存储在服务器的watch结构中,这个结构是个哈希表,key是watch所指的key,value是个双向链表,用于保存所有watch这个key的所有客户端。这里注意奥,存在多个客户端watch同一个key的情况,例如用事务实现乐观锁。
2.每个key在执行写操作的时候,都会到服务器的watch结构中找这个key到底被哪些客户端所watch,把这些客户端的CLIENT_DIRTY_CAS标记打开,这样在执行上面讲解的exec命令时,就会失败。这个也是乐观锁的实现原理。
总结
1.现在想要实现一个每次增长的积分都是之前的2倍的功能。(忽略积分为0的情况)
String ageStr = redis.get("age");
Integer age = Integer.valueOf(ageStr) * 2;
redis.set("age", age.toString());
因为上面的代码不是原子性的,在并发情况下,是不是有问题呀。就需要在外面用分布式锁来控制原子性,那么就会遇到锁超时等问题。下面会用乐观锁来实现。
redis.watch("score");
redis.multi();
String scoreStr = redis.get("score");
Integer scoreStr = Integer.valueOf(scoreStr) * 2;
redis.set("score", scoreStr.toString());
res = redis.exec();
if (res == null) {
// 执行重试策略,可以同步重试,也可以延时重试,这里看业务需要哈~
}
上面是用事务和watch命令实现的乐观锁,来满足需求。是不是更好呢?如果有更好的实现评论区给我留言,我们一起来探讨下。
最近一直在更新一些从Redis源码角度看问题的文章。小伙伴们想要了解Redis的什么,可以评论区留言或私信。
原创不易,多多转发+关注。
加餐
事务的队列是如何实现的?
这个队列的实现就是multiState和multiCmd两个结构实现的。
// 队列结构
typedef struct multiState {
multiCmd *commands; // 是个数组,保存所有的命令multiCmd结构。先进先出原则
int count; // 命令的个数。
} multiState;
// 命令结构
typedef struct multiCmd {
robj **argv; // 保存命令的实际参数
int argc; // 保存命令的参数个数
struct redisCommand *cmd; // 保存具体哪个命令
} multiCmd;
// 在事务期间,除了exec、discard、watch命令可以正常执行,其他命令都放到这个队列里了。
void queueMultiCommand(client *c) {
multiCmd *mc;
int j;
// 申请内存,数组比上次大1个
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
// 取出引用,并把命令的参数放进去
mc = c->mstate.commands+c->mstate.count;
mc->cmd = c->cmd;
mc->argc = c->argc;
mc->argv = zmalloc(sizeof(robj*)*c->argc);
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
// 增加队列的命令数
c->mstate.count++;
}