Redis源码系列~事务


前言

对于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命令都会把自己watchkey放到自己的客户端watch结构中,这个结构是双向链表。也会把watchkey存储在服务器的watch结构中,这个结构是个哈希表,keywatch所指的keyvalue是个双向链表,用于保存所有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++;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coding决定未来

你的鼓励将是我最大的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值