Redis 事务

Redis的事务是非原子性的,且没有回滚;事务中如果有一条命令执行失败,后续的命令还会执行;
事务中所有命令都会序列化,按顺序地执行;事务在执行过程中,不会被其他客户端发送来的命令打断
事务中单个命令时具有原子性的
在没有exec之前,事务中的命令不会被执行;

一、用法

事务执行流程

1、开启事务:MULTI命令用于开启一个事务;
2、命令入队:客户端继续像服务器发送任意多条命令,这些命令不会立即执行,而是放在一个队列中;
3、

  • 执行事务:EXEC命令执行所有在队列中的命令。
  • 取消事务:DISCARD放弃执行事务;
//事务开始到执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
4) "v1"

//事务开始到取消事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get k1
(nil)

二、事务中遇到错误怎么办??

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 "ab"
QUEUED
127.0.0.1:6379> incr k1  //往字符串上自增
QUEUED
127.0.0.1:6379> exec  
1) OK
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr b a c
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

总结

如果入队队列是因为语法错误,则直接报错,事务执行也会失败;
如果组队中没有问题,执行中部分成功部分失败;

语法错误是在命令入队前就进行检测的,而类型错误是在执行时检测的

三、为什么Redis不支持回滚 ?

Redis在事务失败时不支持回滚,而是继续执行余下的命令

优点:Redis命令只因为语法的错误而失败,或者命令用在了错误类型的键上面。也就是说,失败的命令是由编程错误造成的,而这些错误在开发过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以Redis的内部可以保持简单且快速。

回滚并不能解决编程错误带来的问题;

四、WATCH来实现乐观锁

Redis事务完全具有事务回滚的能力可以用WATCH这个命令;

WATCH:监视一个或多个key,如果事务在执行前,这个key被其他命令修改,则事务被中断,不会执行事务中的任何命令;
UNWATCH可以手动取消对所有键的监视;

WATCH命令可以被调用多次。对键的监视从WATCH执行之后开始生效,直到调用EXEC为止;当EXEC被调用时,不管事务是否成功执行,对所有键的监视都会被取消。

127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set money 100
QUEUED
127.0.0.1:6379> incr money  //此时所有的命令进入命令队列
QUEUED

//另一个客户端对这个被监控的值进行修改
127.0.0.1:6379> set money 1000
OK


//在第一个客户端执行事务操作发现失败了
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get money
"1000"
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get money
QUEUED
127.0.0.1:6379> exec
1) "100"

【此图讲解很详细】
在这里插入图片描述

五、php实现代码

<?php

try{
	$redis = new Redis();
	$redis->connect('127.0.0.1',6379);
	
} catch(Exception $e){
	echo $e->getMessage();
	die;
}

$key1 = 'x1';
$key2 = 'x2';

$redis->set($key1,'123');
$redis->set($key2,'234');

try{
	$redis->watch($key1);//对key1实行监控
	
	$redis->set($key1,'1234566');//重新设置key1的值
	$redis->multi();//开启事务
	$redis->set($key1,'1233');
	
	$status = $redis->exec();//执行事务

	if(!$status){
		$redis->discard();//取消事务
	}
}catch(Exception $e){
	echo $e->getMessage();
	die;
}

六、源码分析

开启事务:

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);//返回结果
}

添加一些命令入队

void queueMultiCommand(client *c) {
    multiCmd *mc;
    int j;

    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);
    for (j = 0; j < c->argc; j++)
        incrRefCount(mc->argv[j]);
    c->mstate.count++;
    c->mstate.cmd_flags |= c->cmd->flags;
}

执行:

void execCommand(client *c) {
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;
    int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? 标记是否需要把multi/exec传递到AOF或者slaves节点*/
    int was_master = server.masterhost == NULL;//标记当前redis节点是否为主节点

    if (!(c->flags & CLIENT_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }

    /* Check if we need to abort the EXEC because: 检查是否需要终止exec
     * 1) Some WATCHed key was touched.   一些被监视的key被改变了
     * 2) There was a previous error while queueing commands.将命令排入队列时出现以前的错误。
     * A failed EXEC in the first case returns a multi bulk nil object
     * (technically it is not an error but a special behavior), while
     * in the second an EXECABORT error is returned. 第一中错误情况结果为nil,第二种则为error*/
     //CAS   check-and-set  watch命令
    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;
    }

    /* If there are write commands inside the transaction, and this is a read
     * only slave, we want to send an error. This happens when the transaction
     * was initiated when the instance was a master or a writable replica and
     * then the configuration changed (for example instance was turned into
     * a replica). 如果事务中有写命令,而这是一个只读从机,我们希望发送一个错误。*/
    if (!server.loading && server.masterhost && server.repl_slave_ro &&
        !(c->flags & CLIENT_MASTER) && c->mstate.cmd_flags & CMD_WRITE)
    {
        addReplyError(c,
            "Transaction contains write commands but instance "
            "is now a read-only slave. EXEC aborted.");
        discardTransaction(c);
        goto handle_monitor;
    }

    /* Exec all the queued commands */
    unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles尽快取消连接,否则我们将浪费CPU周期 */
    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;

        /* Propagate a MULTI request once we encounter the first command which
         * is not readonly nor an administrative one.
         * This way we'll deliver the MULTI/..../EXEC block as a whole and
         * both the AOF and the replication link will have the same consistency
         * and atomicity guarantees. */
        if (!must_propagate && !(c->cmd->flags & (CMD_READONLY|CMD_ADMIN))) {
            execCommandPropagateMulti(c);
            must_propagate = 1;
        }

        call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);

        /* Commands may alter argc/argv, restore mstate. */
        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);

    /* Make sure the EXEC command will be propagated as well if MULTI
     * was already propagated. */
    if (must_propagate) {
        int is_master = server.masterhost == NULL;
        server.dirty++;
        /* If inside the MULTI/EXEC block this instance was suddenly
         * switched from master to slave (using the SLAVEOF command), the
         * initial MULTI was propagated into the replication backlog, but the
         * rest was not. We need to make sure to at least terminate the
         * backlog with the final EXEC. */
        if (server.repl_backlog && was_master && !is_master) {
            char *execcmd = "*1\r\n$4\r\nEXEC\r\n";
            feedReplicationBacklog(execcmd,strlen(execcmd));
        }
    }

    handle_monitor:
    /* Send EXEC to clients waiting data from MONITOR. We do it here
     * since the natural order of commands execution is actually:
     * MUTLI, EXEC, ... commands inside transaction ...
     * Instead EXEC is flagged as CMD_SKIP_MONITOR in the command
     * table, and we do it here with correct ordering. */
    if (listLength(server.monitors) && !server.loading)
        replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}

取消事务:

void discardCommand(client *c) {
	//如果客户端没有处于事务状态,则返回错误信息
    if (!(c->flags & CLIENT_MULTI)) {
        addReplyError(c,"DISCARD without MULTI");
        return;
    }
    //删除事务
    discardTransaction(c);
    addReply(c,shared.ok);
}

void discardTransaction(client *c) {
    freeClientMultiState(c);//释放与MULTI/EXEC状态关联的所有资源
    initClientMultiState(c);//初始化相关联的所有资源
    c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);
    unwatchAllKeys(c);//取消被监控的key,
}

释放队列中的每个命令


void freeClientMultiState(client *c) {
    int j;

    for (j = 0; j < c->mstate.count; j++) {
        int i;
        multiCmd *mc = c->mstate.commands+j;

        for (i = 0; i < mc->argc; i++)
            decrRefCount(mc->argv[i]);
        zfree(mc->argv);
    }
    zfree(c->mstate.commands);
}

WATCH 和UNWATCH

void watchCommand(client *c) {
    int j;

    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");//由此可看watch必须要事物开始前使用
        return;
    }
    //watch客户端的各个参数,说明watch命令可以一次watch一个或者多个key
    for (j = 1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    addReply(c,shared.ok);
}


void watchForKey(client *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;

    /* Check if we are already watching for this key 检查我们是否已经在监视此密钥*/
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; /* Key already watched */
    }
    /* This key is not already watched in this DB. Let's add it此数据库中尚未监视此密钥。让我们加上它 */
    clients = dictFetchValue(c->db->watched_keys,key);
    if (!clients) {
        clients = listCreate();
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    listAddNodeTail(clients,c);
    /* Add the new key to the list of keys watched by this client 将新密钥添加到此客户端监视的密钥列表中*/
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}

void unwatchCommand(client *c) {
    unwatchAllKeys(c);//取消watch所有的key
    c->flags &= (~CLIENT_DIRTY_CAS);
    addReply(c,shared.ok);
}


void unwatchAllKeys(client *c) {
    listIter li;
    listNode *ln;
	
    if (listLength(c->watched_keys) == 0) return;//如果没有watch任何key则直接返回
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        list *clients;
        watchedKey *wk;

        /* Lookup the watched key -> clients list and remove the client
         * from the list查找关注的键->客户端列表并删除客户端从名单上 */
        wk = listNodeValue(ln);
        clients = dictFetchValue(wk->db->watched_keys, wk->key);
        serverAssertWithInfo(c,NULL,clients != NULL);
        listDelNode(clients,listSearchKey(clients,c));
        /* Kill the entry at all if this was the only client */
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);
        /* Remove this watched key from the client->watched list 从客户端->监视列表中删除此监视的密钥

*/
        listDelNode(c->watched_keys,ln);
        decrRefCount(wk->key);
        zfree(wk);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值