乐趣区

关于java:Redis-事务详解

文章收录在 GitHub JavaKeeper,N 线互联网开发必备技能兵器谱

假如当初有这样一个业务,用户获取的某些数据来自第三方接口信息,为防止频繁申请第三方接口,咱们往往会加一层缓存,缓存必定要有时效性,假如咱们要存储的构造是 hash(没有 String 的 ’SET anotherkey “will expire in a minute” EX 60‘ 这种原子操作),咱们既要批量去放入缓存,又要保障每个 key 都加上过期工夫(以防 key 永不过期),这时候事务操作是个比拟好的抉择

为了确保间断多个操作的原子性,咱们罕用的数据库都会有事务的反对,Redis 也不例外。但它又和关系型数据库不太一样。

每个事务的操作都有 begin、commit 和 rollback,begin 批示事务的开始,commit 批示事务的提交,rollback 批示事务的回滚。它大抵的模式如下

begin();
try {command1();
    command2();
    ....
    commit();} catch(Exception e) {rollback();
}

Redis 在模式上看起来也差不多,分为三个阶段

  1. 开启事务(multi)
  2. 命令入队(业务操作)
  3. 执行事务(exec)或勾销事务(discard)
> multi
OK
> incr star
QUEUED
> incr star
QUEUED
> exec
(integer) 1
(integer) 2

下面的指令演示了一个残缺的事务过程,所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行结束后一次性返回所有指令的运行后果。

Redis 事务能够一次执行多个命令,实质是一组命令的汇合。一个事务中的所有命令都会序列化,按程序地串行化执行而不会被其它命令插入,不许加塞。

能够保障一个队列中,一次性、程序性、排他性的执行一系列命令(Redis 事务的次要作用其实就是串联多个命令避免别的命令插队)

官网文档是这么说的

事务能够一次执行多个命令,并且带有以下两个重要的保障:

  • 事务是一个独自的隔离操作:事务中的所有命令都会序列化、按程序地执行。事务在执行的过程中,不会被其余客户端发送来的命令申请所打断。
  • 事务是一个原子操作:事务中的命令要么全副被执行,要么全副都不执行

这个原子操作,和关系型 DB 的原子性不太一样,它不能齐全保障原子性,后边会介绍。

Redis 事务的几个命令

命令 形容
MULTI 标记一个事务块的开始
EXEC 执行所有事务块内的命令
DISCARD 勾销事务,放弃执行事务块内的所有命令
WATCH 监督一个(或多个)key,如果在事务执行之前这个(或多个)key 被其余命令所改变,那么事务将被打断
UNWATCH 勾销 WATCH 命令对所有 keys 的监督

MULTI 命令用于开启一个事务,它总是返回 OK。

MULTI 执行之后,客户端能够持续向服务器发送任意多条命令,这些命令不会立刻被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。

另一方面,通过调用 DISCARD,客户端能够清空事务队列,并放弃执行事务。

废话不多说,间接操作起来看后果更好了解~

一帆风顺

失常执行(能够批处理,挺爽,每条操作胜利的话都会各取所需,互不影响)

放弃事务(discard 操作示意放弃事务,之前的操作都不算数)

思考个问题:假如咱们有个有过期工夫的 key,在事务操作中 key 生效了,那执行 exec 的时候会胜利吗?

事务中的谬误

上边规规矩矩的操作,看着还挺好,可是 事务是为解决数据安全操作提出的,咱们用 Redis 事务的时候,可能会遇上以下两种谬误:

  • 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量谬误,参数名谬误等等),或者其余更重大的谬误,比方内存不足(如果服务器应用 maxmemory 设置了最大内存限度的话)。
  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能解决了谬误类型的键,比方将列表命令用在了字符串键下面,诸如此类。

Redis 针对如上两种谬误采纳了不同的解决策略,对于产生在 EXEC 执行之前的谬误,服务器会对命令入队失败的状况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并主动放弃这个事务(Redis 2.6.5 之前的做法是查看命令入队所得的返回值:如果命令入队时返回 QUEUED,那么入队胜利;否则,就是入队失败)

对于那些在 EXEC 命令执行之后所产生的谬误,并没有对它们进行特地解决:即便事务中有某个 / 某些命令在执行时产生了谬误,事务中的其余命令依然会继续执行。

整体连坐(某一条操作记录报错的话,exec 后所有操作都不会胜利)

冤头债户(示例中 k1 被设置为 String 类型,decr k1 能够放入操作队列中,因为只有在执行的时候才能够判断出语句谬误,其余正确的会被失常执行)

为什么 Redis 不反对回滚

如果你有应用关系式数据库的教训,那么“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你感觉有点奇怪。

以下是官网的 自夸

  • Redis 命令只会因为谬误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了谬误类型的键下面:这也就是说,从实用性的角度来说,失败的命令是由编程谬误造成的,而这些谬误应该在开发的过程中被发现,而不应该呈现在生产环境中。
  • 因为不须要对回滚进行反对,所以 Redis 的外部能够放弃简略且疾速。

有种观点认为 Redis 处理事务的做法会产生 bug,然而须要留神的是,在通常状况下,回滚并不能解决编程谬误带来的问题。举个例子,如果你原本想通过 INCR 命令将键的值加上 1,却不小心加上了 2,又或者对谬误类型的键执行了 INCR,回滚是没有方法解决这些状况的。

鉴于没有任何机制能防止程序员本人造成的谬误,并且这类谬误通常不会在生产环境中呈现,所以 Redis 抉择了更简略、更疾速的无回滚形式来处理事务。

带 Watch 的事务

WATCH 命令用于在事务开始之前监督任意数量的键:当调用 EXEC 命令执行事务时,如果任意一个被监督的键曾经被其余客户端批改了,那么整个事务将被打断,不再执行,间接返回失败。

WATCH 命令能够被调用屡次。对键的监督从 WATCH 执行之后开始失效,直到调用 EXEC 为止。

用户还能够在单个 WATCH 命令中监督任意多个键,就像这样:

redis> WATCH key1 key2 key3 
OK 

EXEC 被调用时,不论事务是否胜利执行,对所有键的监督都会被勾销。另外,当客户端断开连接时,该客户端对键的监督也会被勾销。

咱们看个简略的例子,用 watch 监控我的账号余额(一周 100 零花钱的我),失常生产

但这个卡,还绑定了我媳妇的支付宝,如果在我生产的时候,她也生产了,会怎么样呢?

犯困的我去楼下 711 买了包烟,买了瓶水,这时候我媳妇在超市间接刷了 100,此时余额有余的我还在挑口香糖来着,,,

这时候我去结账,发现刷卡失败(事务中断),难堪的一批

你可能没看明确 watch 有啥用,咱们再来看下,如果还是同样的场景,咱们没有 watch balance,事务不会失败,储蓄卡成正数,是不不太合乎业务呢

应用无参数的 UNWATCH 命令能够手动勾销对所有键的监督。对于一些须要改变多个键的事务,有时候程序须要同时对多个键进行加锁,而后查看这些键的以后值是否合乎程序的要求。当值达不到要求时,就能够应用 UNWATCH 命令来勾销目前对键的监督,中途放弃这个事务,并期待事务的下次尝试。

watch 指令,相似乐观锁,事务提交时,如果 key 的值已被别的客户端扭转,比方某个 list 已被别的客户端 push/pop 过了,整个事务队列都不会被执行。(当然也能够用 Redis 实现分布式锁来保障安全性,属于乐观锁)

通过 watch 命令在事务执行之前监控了多个 keys,假使在 watch 之后有任何 key 的值发生变化,exec 命令执行的事务都将被放弃,同时返回 Null 应答以告诉调用者事务执行失败。

乐观锁

乐观锁(Pessimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为他人会批改,所以每次在拿数据的时候都会上锁,这样他人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为他人不会批改,所以不会上锁,然而在更新的时候会判断一下在此期间他人有没有去更新这个数据,能够应用版本号等机制。乐观锁实用于多读的利用类型,这样能够进步吞吐量。乐观锁策略:提交版本必须大于记录以后版本能力执行更新

WATCH 命令的实现原理

在代表数据库的 server.h/redisDb 构造类型中,都保留了一个 watched_keys 字典,字典的键是这个数据库被监督的键,而字典的值是一个链表,链表中保留了所有监督这个键的客户端,如下图。

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */

WATCH 命令的作用,就是将以后客户端和要监督的键在 watched_keys 中进行关联。

举个例子,如果以后客户端为 client99,那么当客户端执行 WATCH key2 key3 时,后面展现的 watched_keys 将被批改成这个样子:

通过 watched_keys 字典,如果程序想查看某个键是否被监督,那么它只有查看字典中是否存在这个键即可;如果程序要获取监督某个键的所有客户端,那么只有取出键的值(一个链表),而后对链表进行遍历即可。

在任何对数据库键空间(key space)进行批改的命令胜利执行之后(比方 FLUSHDB、SET、DEL、LPUSH、SADD,诸如此类),multi.c/touchWatchedKey 函数都会被调用 —— 它会去 watched_keys 字典,看是否有客户端在监督曾经被命令批改的键,如果有的话,程序将所有监督这个 / 这些被批改键的客户端的 REDIS_DIRTY_CAS 选项关上:

void multiCommand(client *c) {
    // 不能在事务中嵌套事务
    if (c->flags & CLIENT_MULTI) {addReplyError(c,"MULTI calls can not be nested");
        return;
    }
    // 关上事务 FLAG
    c->flags |= CLIENT_MULTI;
    addReply(c,shared.ok);
}

/* "Touch" a key, so that if this key is being WATCHed by some client the
 * next EXEC will fail. */
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;

    // 遍历所有客户端,关上他们的 CLIENT_DIRTY_CAS 标识
    listRewind(clients,&li);
    while((ln = listNext(&li))) {client *c = listNodeValue(ln);

        c->flags |= CLIENT_DIRTY_CAS;
    }
}

当客户端发送 EXEC 命令、触发事务执行时,服务器会对客户端的状态进行查看:

  • 如果客户端的 CLIENT_DIRTY_CAS 选项曾经被关上,那么阐明被客户端监督的键至多有一个曾经被批改了,事务的安全性曾经被毁坏。服务器会放弃执行这个事务,间接向客户端返回空回复,示意事务执行失败。
  • 如果 CLIENT_DIRTY_CAS 选项没有被关上,那么阐明所有监督键都平安,服务器正式执行事务。

小总结:

3 个阶段

  • 开启:以 MULTI 开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立刻执行,而是放到期待执行的事务队列外面
  • 执行:由 EXEC 命令触发事务

3 个个性

  • 独自的隔离操作:事务中的所有命令都会序列化、按程序地执行。事务在执行的过程中,不会被其余客户端发送来的命令申请所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会理论的被执行,因为事务提交前任何指令都不会被理论执行,也就不存在”事务内的查问要看到事务里的更新,在事务外查问不能看到”这个让人万分头痛的问题
  • 不保障原子性:Redis 同一个事务中如果有一条命令执行失败,其后的命令依然会被执行,没有回滚

在传统的关系式数据库中,经常用 ACID 性质来测验事务性能的安全性。Redis 事务保障了其中的一致性(C)和隔离性(I),但并不保障原子性(A)和持久性(D)。

最初

Redis 事务在发送每个指令到事务缓存队列时都要通过一次网络读写,当一个事务外部的指令较多时,须要的网络 IO 工夫也会线性增长。所以通常 Redis 的客户端在执行事务时都会联合 pipeline 一起应用,这样能够将屡次 IO 操作压缩为单次 IO 操作。

退出移动版