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 的watch 命令不是不能用于分片集群吗,为什么你这里不报错