如果对redis事务不熟悉,建议避免使用redis事务
Redis 的事务使用非常简单,不同于关系数据库,它的事务模型很不严格。Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。
Redis 事务命令分别是 multi/exec/discard。multi 指示事务的开始,exec 指示事务的执行,discard 指示事务的丢弃。但是,Redis的事务是不支持回滚的。
优化
Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。
Watch
考虑到一个业务场景,Redis 存储了我们的账户余额数据,它是一个整数。现在有两个并发的客户端要对账户余额进行修改操作,这个修改不是一个简单的 incrby 指令,而是要对余额乘以一个倍数。Redis 可没有提供 multiplyby 这样的指令。我们需要先取出余额然后在内存里乘以倍数,再将结果写回 Redis。这就会出现并发问题,因为有多个客户端会并发进行操作。
我们可以通过 Redis 的分布式锁来避免冲突,这是一个很好的解决方案。分布式锁是一种悲观锁,那是不是可以使用乐观锁的方式来解决冲突呢?Redis 提供了这种 watch 的机制,它就是一种乐观锁。有了 watch 我们又多了一种可以用来解决并发修改的方法。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.text.MessageFormat;
import java.util.List;
/**
* @Auther: majx2
* @Date: 2019-3-23 20:29
* @Description:
*/
public class TransactionDemo {
public static void main(String[] args) {
Jedis jedis = RedisDS.create().getJedis();
String userId = "abc";
String key = keyFor(userId);
jedis.setnx(key, String.valueOf(5));
System.out.println(doubleAccount(jedis, userId));
jedis.close();
}
public static int doubleAccount(Jedis jedis, String userId) {
String key = keyFor(userId);
while (true) {
jedis.watch(key);
int value = Integer.parseInt(jedis.get(key));
value *= 2; // 加倍
Transaction tx = jedis.multi();
tx.set(key, String.valueOf(value));
List<Object> res = tx.exec();
if (res != null) {
break; // 成功了
}
}
return Integer.parseInt(jedis.get(key)); // 重新获取余额
}
public static String keyFor(String userId) {
return MessageFormat.format("account_{0}", userId);
}
}
网友评论