redis 实现 CAS 及 redis 事务的坑

内容纲要

CAS

Compare And Swap,即 CAS,是一种乐观锁,java 的原子类就是利用的 CAS 来实现了并发时的无锁修改

其核心要点就是,当原值和指定的值相等时,才会用新的值去替换原值,否则不会替换原值

redis 的 CAS

想一下,如下的需求要怎么做

要删除 redis 的某个 key,条件是该 key 的值是我们指定的值

简单的写个方法如下

public void delete(String key, String value) {
    if(value.equals(jedis.get(key))) {
        jedis.del(key);
    }
}

这样的实现是有隐患的,我们先要 get 到 value,然后再 del key,问题是在我们 get 之后,del 之前,key 的 value 是可能被修改的

考虑到 redis 是单线程执行,如果能确保 get 和 del 之间不会有别的命令执行,这个方法就是可行的,but 这个很难保证,我想过用 pipeline,但是在 pipeline 的执行过程里,无法得到 get 的 value,行不通

watch

前面的需求就是很典型的 CAS,redis 的 del 原生并未提供 CAS,不过 redis 的事务有个 watch 方法,可以用来实现 CAS

当我们 watch 了某个(或多个) key 以后,开启事务对 key 进行修改,如果这个 key 的值在我们 watch 以后被修改过,那么事务提交时将不会被执行

这样我们就可以如下实现

public void delete(String key, String value) {

    jedis.watch(key);
    if (value.equals(jedis.get(key))) {
        Transaction tx = jedis.multi();
        tx.del(key);
        tx.exec();
    } else {
        jedis.unwatch();
    }

}

redis 事务的一个小坑

话说使用 watch 来进行 CAS 的删除后一直很 OK,某天因为某个需求,我对上述的删除进行了小小的修改,在删除 key 的时候,顺便删除了另一个关联的 key,代码如下

public void delete(String key, String value, String key2) {

    jedis.watch(key);
    if (value.equals(jedis.get(key))) {
        Transaction tx = jedis.multi();
        tx.del(key);
        tx.del(key2);
        tx.exec();
    } else {
        jedis.unwatch();
    }

}

在测试环境一切正常,发布到生产环境后,抛出如下异常

ERR keys of command in MULTI calls must be in same slot
redis.clients.jedis.exceptions.JedisDataException: ERR keys of command in MULTI calls must be in same slot
    at redis.clients.jedis.Protocol.processError(Protocol.java:135)
    at redis.clients.jedis.Protocol.process(Protocol.java:169)
    at redis.clients.jedis.Protocol.read(Protocol.java:223)
    at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:352)
    at redis.clients.jedis.Connection.getUnflushedObjectMultiBulkReply(Connection.java:314)
    at redis.clients.jedis.Connection.getObjectMultiBulkReply(Connection.java:319)
    at redis.clients.jedis.Transaction.exec(Transaction.java:46)

这个异常是第一次遇到,在网上搜索一番,原因是在事务里操作的多个 key,不在同一个节点上

为什么测试环境从来没有遇到问题呢?因为我们测试环境是单节点 redis,而生产环境是 8 节点的 redis 集群

解决办法:在要 CAS 删除的 key 被删除以后,在事务之外删除关联的 key,这也要求我们能正确判断出 CAS 删除是否成功,代码如下

public void delete(String key, String value, String key2) {

    boolean result = false;

    jedis.watch(key);
    if (value.equals(jedis.get(key))) {
        Transaction tx = jedis.multi();
        tx.del(key);
        result = tx.exec() != null;
    } else {
        jedis.unwatch();
    }

    if (result) {
        jedis.del(key2);
    }

}

如果被 watch 的 key 发生了变化, exec 时事务会放弃执行并返回 null

redis 实现 CAS 及 redis 事务的坑

One thought on “redis 实现 CAS 及 redis 事务的坑

  1. 大佬,有看了你的文章收获匪浅。有个疑问,Redis 的watch 命令不是不能用于分片集群吗,为什么你这里不报错

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Scroll to top
粤ICP备2020114259号 粤公网安备44030402004258