redis事务实现原理(源码分析)

Author: bugall
Wechat: bugallF
Email: 769088641@qq.com
Github: https://github.com/bugallgit

一:简介

Redis事务一般会使用MULTI,EXEC,WATCH等命令来完成,redis实现事务实现的机制与常见的关系型数据库有很大的区别,好比redis的事务不支持回滚,事务执行时会阻塞其它客户端的请求执行。github

二:事务实现细节

redis事务从开始到结束一般会经过三个阶段:redis

1.事务开始数据库

2.命令入队编程

3.事务执行数组

咱们从下面的例子看下安全

redis > MULTI 
OK

redis > SET "username" "bugall"
QUEUED

redis > SET "password" 161616
QUEUED

redis > GET "username"

redis > EXEC
1) ok
2) "bugall"
3) "bugall"
redis > MULTI

标记事务的开始,MULTI命令能够将执行该命令的客户端从非事务状态切换成事务状态,这一切换是经过在客户端状态的flags属性中打开REDIS_MULTI标识完成,
咱们看下redis中对应部分的源码实现服务器

void multiCommand(client *c) {
    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }
    c->flags |= CLIENT_MULTI;    //打开事务标识
    addReply(c,shared.ok);
}

在打开事务标识的客户端里,这些命令,都会被暂存到一个命令队列里,不会由于用户会的输入而当即执行并发

redis > SET "username" "bugall"
redis > SET "password" 161616
redis > GET "username"

执行事务队列里的命令。app

redis > EXEC

这里须要注意的是,在客户端打开了事务标识后,只有命令:EXEC,DISCARD,WATCH,MULTI命令会被当即执行,其它命令服务器不会当即执行,而是将这些命令放入到一个事务队列里面,而后向客户端返回一个QUEUED回复
redis客户端有本身的事务状态,这个状态保存在客户端状态mstate属性中,mstate的结构体类型是multiState,咱们看下multiState的定义

typedef struct multiState {
    multiCmd *commands;     //存放MULTI commands的数组
    int count;              //命令数量
} multiState;

咱们再看下结构体类型multiCmd的结构

typedef struct multiCmd {
    robj **argv;    //参数
    int argc;    //参数数量
    struct redisCommand *cmd; //命令指针
} multiCmd;

事务队列以先进先出的保存方法,较先入队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面.

三:执行事务

当开启事务标识的客户端发送EXEC命令的时候,服务器就会执行,客户端对应的事务队列里的命令,咱们来看下EXEC
的实现细节

void execCommand(client *c) {
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;
    int must_propagate = 0; //同步持久化,同步主从节点

    //若是客户端没有开启事务标识
    if (!(c->flags & CLIENT_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }
    //检查是否须要放弃EXEC
    //若是某些被watch的key被修改了就放弃执行
    if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
        addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
                                                  shared.nullmultibulk);
        discardTransaction(c);
        goto handle_monitor;
    }

       //执行事务队列里的命令
    unwatchAllKeys(c); //由于redis是单线程的因此这里,当检测watch的key没有被修改后就统一clear掉全部的watch
    orig_argv = c->argv;
    orig_argc = c->argc;
    orig_cmd = c->cmd;
    addReplyMultiBulkLen(c,c->mstate.count);
    for (j = 0; j < c->mstate.count; j++) {
        c->argc = c->mstate.commands[j].argc;
        c->argv = c->mstate.commands[j].argv;
        c->cmd = c->mstate.commands[j].cmd;

        //同步主从节点,和持久化
        if (!must_propagate && !(c->cmd->flags & CMD_READONLY)) {
            execCommandPropagateMulti(c);
            must_propagate = 1;
        }
        //执行命令
        call(c,CMD_CALL_FULL);
        c->mstate.commands[j].argc = c->argc;
        c->mstate.commands[j].argv = c->argv;
        c->mstate.commands[j].cmd = c->cmd;
    }
    c->argv = orig_argv;
    c->argc = orig_argc;
    c->cmd = orig_cmd;
    //取消客户端的事务标识
    discardTransaction(c);
    if (must_propagate) server.dirty++;

handle_monitor:
    if (listLength(server.monitors) && !server.loading)
        replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}

四:watch/unwatch/discard

watch:

命令是一个乐观锁,它能够在EXEC命令执行以前,监视任意数量的数据库键,并在执行EXEC命令时判断是否至少有一个被watch的键值

被修改若是被修改就放弃事务的执行,若是没有被修改就清空watch的信息,执行事务列表里的命令。
unwatch:

顾名思义能够看出它的功能是与watch相反的,是取消对一个键值的“监听”的功能能

discard:

清空客户端的事务队列里的全部命令,并取消客户端的事务标记,若是客户端在执行事务的时候watch了一些键,则discard会取消全部

键的watch.

五:redis事务的ACID特性

在传统的关系型数据库中,尝尝用ACID特质来检测事务功能的可靠性和安全性。

在redis中事务老是具备原子性(Atomicity),一致性(Consistency)和隔离性(Isolation),而且当redis运行在某种特定的持久化
模式下,事务也具备耐久性(Durability).

①原子性

事务具备原子性指的是,数据库将事务中的多个操做看成一个总体来执行,服务器要么就执行事务中的全部操做,要么就一个操做也不执行。

可是对于redis的事务功能来讲,事务队列中的命令要么就所有执行,要么就一个都不执行,所以redis的事务是具备原子性的。咱们一般会知道

两种关于redis事务原子性的说法,一种是要么事务都执行,要么都不执行。另一种说法是redis事务当事务中的命令执行失败后面的命令还
会执行,错误以前的命令不会回滚。其实这个两个说法都是正确的。可是缺一不可。咱们接下来具体分析下

咱们先看一个能够正确执行的事务例子
redis > MULTI
OK

redis > SET username "bugall"
QUEUED

redis > EXEC
1) OK
2) "bugall"
与之相反,咱们再来看一个事务执行失败的例子。这个事务由于命令在放入事务队列的时候被服务器拒绝,因此事务中的全部命令都不会执行,由于
前面咱们有介绍到,redis的事务命令是统一先放到事务队列里,在用户输入EXEC命令的时候再统一执行。可是咱们错误的使用"GET"命令,在命令
放入事务队列的时候被检测到事务,这时候尚未接收到EXEC命令,因此这个时候不牵扯到回滚的问题,在EXEC的时候发现事务队列里有命令存在
错误,因此事务里的命令就全都不执行,这样就达到里事务的原子性,咱们看下例子。
redis > MULTI
OK

redis > GET
(error) ERR wrong number of arguments for 'get' command

redis > GET username
QUEUED

redis > EXEC
(error) EXECABORT Transaction discarded because of previous errors
redis的事务和传统的关系型数据库事务的最大区别在于,redis不支持事务的回滚机制,即便事务队列中的某个命令在执行期间出现错误,整个事务也会
继续执行下去,直到将事务队列中的全部命令都执行完毕为止,咱们看下面的例子
redis > SET username "bugall"
OK

redis > MULTI
OK

redis > SADD member "bugall" "litengfe" "yangyifang"
QUEUED

redis > RPUSH username "b" "l" "y" //错误对键username使用列表键命令
QUEUED

redis > SADD password "123456" "123456" "123456"
QUEUED

redis > EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3
redis的做者在十五功能的文档中解释说,不支持事务回滚是由于这种复杂的功能和redis追求的简单高效的设计主旨不符合,而且他认为,redis事务的执行时
错误一般都是编程错误形成的,这种错误一般只会出如今开发环境中,而不多会在实际的生产环境中出现,因此他认为没有必要为redis开发事务回滚功能。因此
咱们在讨论redis事务回滚的时候,必定要区分命令发生错误的时候。

②一致性

事务具备一致性指的是,若是数据库在执行事务以前是一致的,那么在事务执行以后,不管事务是否执行成功,数据库也应该仍然一致的。
    ”一致“指的是数据符合数据库自己的定义和要求,没有包含非法或者无效的错误数据。redis经过谨慎的错误检测和简单的设计来保证事务一致性。

③隔离性

事务的隔离性指的是,即便数据库中有多个事务并发在执行,各个事务之间也不会互相影响,而且在并发状态下执行的事务和串行执行的事务产生的结果彻底
    相同。
    由于redis使用单线程的方式来执行事务(以及事务队列中的命令),而且服务器保证,在执行事务期间不会对事物进行中断,所以,redis的事务老是以串行
    的方式运行的,而且事务也老是具备隔离性的

④持久性

事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保持到永久存储介质里面。
    由于redis事务不过是简单的用队列包裹起来一组redis命令,redis并无为事务提供任何额外的持久化功能,因此redis事务的耐久性由redis使用的模式
    决定
    - 当服务器在无持久化的内存模式下运行时,事务不具备耐久性,一旦服务器停机,包括事务数据在内的全部服务器数据都将丢失
    - 当服务器在RDB持久化模式下运做的时候,服务器只会在特定的保存条件知足的时候才会执行BGSAVE命令,对数据库进行保存操做,而且异步执行的BGSAVE不
    能保证事务数据被第一时间保存到硬盘里面,所以RDB持久化模式下的事务也不具备耐久性
    - 当服务器运行在AOF持久化模式下,而且appedfsync的选项的值为always时,程序总会在执行命令以后调用同步函数,将命令数据真正的保存到硬盘里面,所以
    这种配置下的事务是具备耐久性的。
    - 当服务器运行在AOF持久化模式下,而且appedfsync的选项的值为everysec时,程序会每秒同步一次命令数据到磁盘由于停机可能会刚好发生在等待同步的那一秒内,这种可能形成事务数据丢失,因此这种配置下的事务不具备耐久性
相关文章
相关标签/搜索