当前位置: 首页 > 工具软件 > Keywatch > 使用案例 >

Redis WATCH事务监视机制与回滚

澹台聪
2023-12-01

笔者Redis事务相关文章链接:Redis 事务机制深入浅出

概述

Redis内置了WATCH命令,使用WATCH命令可以实现乐观锁的功能,在事务被EXEC命令执行前,如果此前被WATCH监视的某数据库键已经被修改过,则拒绝执行当前事务,返回空并重置客户端事务标识。在并发场景下易使用该命令,很容易理解,有并发的场景就有同步处理。

WATCH的使用

首先明确一点,底层实现中每个Redis客户端内都有一个用于标识事务状态的属性,在每一条命令送到服务器端时,在执行前都会先检查该属性,因为同一条命令在事务状态和非事务状态下应该有不同的处理方法。

WATCH的使用时机

WATCH只能在事务未开启的状态下使用,命令执行前会先对客户端下的事务状态属性做检查,如果发现事务已经开启,则返回错误。此处也可以理解为不应允许WATCH命令进入事务的命令队列。

演示如下;

127.0.0.1:6379> multi
OK
127.0.0.1:6379> watch a
(error) ERR WATCH inside MULTI is not allowed

使用WATCH监视数据库键

该命令语法如下watch key [key ...],语法中可以看出,Redis允许同时监视多个数据库键,当被监视任一数据库键被修改后,EXEC后事务返回nil且不会实际执行。

正常使用演示如下,演示内将两个客户端的操作做串行化处理演示:

// 客户端1
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set b 1
OK
127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi
OK

// 客户端2
127.0.0.1:6379> set a 2
OK

//客户端1
127.0.0.1:6379> incr b
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> mget a b
1) "2"
2) "1"

从以上命令中可见,在客户端1监视了键a后,客户端2对键2执行了修改操作,此时事务执行结果返回nil,获取键a和b的值后明确事务未执行,监视成功触发。

取消监视

使用UNWATCH指令可以取消当前客户端所有正在监视的键,与WATCH不同,UNWATCH命令可以入队。

演示如下:

127.0.0.1:6379> watch a
OK
127.0.0.1:6379> unwatch
OK

关于WATCH的细节和常见误区

监视的持续时间

无论哪种情况,监视都只持续一个事务周期。

经笔者测试,下列任一情况后不执行任何其他操作,立即开启新的事务,并在修改原本被监视的键后,执行事务都会成功。

  1. 被监视的键被修改,事务返回nil未执行成功。
  2. 被监视的键未被修改,事务成功执行。
  3. 被监视的键被修改或未被修改,事务未执行,使用DISCARD弃用当前事务。

其他特殊情况

  1. 被监视的若干键只有一个键被修改:事务不会执行。
  2. 被监视的键被修改为原值(比如键a的值从1被修改为1):事务不会执行。WATCH监视的是修改操作,不是值比较,不存在任何ABA问题,只要对被监视键执行修改命令,事务就不会执行。
  3. 被监视的键不存在:事务不会执行。例如监视了一个不存在的键a,监视后使用set a 1等指令修改键a,事务同样不会执行。
  4. 被监视的键在整个事务队列的命令中并未被使用:事务不会执行。WATCH监视只关注被监视的键本身是否被修改过。

Redis中的回滚

首先明确一点,Redis不支持回滚。

在事务监视机制中体到回滚,主要是因为笔者在翻阅事务相关文章时,发现的确有人认为Redis中存在事务回滚机制。此处的理解偏差在于,WATCH监视并不是在事务中某一条使用了被监视键的命令执行前检查,而是在整个事务开始前就检查所有被监视的键是否被修改

事务执行前会检查客户端的flags属性,如果属性为CLIENT_DIRTY_CAS,表明当前客户端监视的某一键已被修改,事务不应该被执行。而触发监视修改这个属性的函数为touchWatchedKey,源码如下:

void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;

	// 首先检查当前数据库内是否有被监视的键
    if (dictSize(db->watched_keys) == 0) return;
    // 再检查监视目标键的所有客户端 不存在符合条件客户端时返回
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /* Mark all the clients watching this key as CLIENT_DIRTY_CAS */
    /* Check if we are already watching for this key */
    // 将这些客户端的flags 增加CLIENT_DIRTY_CAS标识
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);

        c->flags |= CLIENT_DIRTY_CAS;
    }
}

因此WATCH监视机制与回滚没有任何关系。

不存在监视的条件下,在事务正常执行过程中,命令入队时出现错误则整个事务不会执行,命令执行时出现错误不会影响队列中其他命令的执行,同样不存在回滚机制。

为什么Redis不支持回滚机制

关于这一问题,在Redis官方中文文档中可以得到答案。

Redis事务中的命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令。

这么做的原因有两点:

Redis 命令只在两种情况失败: 语法错误的时候才失败(在命令输入的时候不检查语法)。
要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
因为不需要回滚,所以Redis内部实现简单并高效。
当出现bug的时候Redis的这种做法并不友好,可是需要注意的是回滚并不能解决程序bug。

例如,对于需要增加1的逻辑增加了2,或者操作的key类型不对,这些情况回滚并没有什么帮助。

考虑到没有人能避免程序员错误,并且这种错误也基本不能进入生产环境,我们选择了更简单且更高效的方法,不支持错误回滚。

总结为两个原因,①错误应该在开发阶段被测试出,回滚无法实际解决bug,②为了保持Redis的简单高效,不支持错误回滚。

 类似资料: