本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!
数据库事务
事务有begin、commit、rollback操作,begin表示事务的开始,commit表示事务的提交,rollback表示事务的回滚
Redis事务
Redis事务也有multi、exec、discard,multi表示事务的开始,exec表示事务的执行,discard表示丢弃事务中的所有指令;Redis事务在遇到错误的指令时不会自动回滚,而是会继续执行之后的指令;discard指令只能在exec之前执行
> multi
OK
> incr books
QUEUED
> incr books
QUEUED
> exec
(integer) 1
(integer) 2
Redis事务中指令在exec执行前是不会执行的,所有的指令会被缓存在服务器的一个事务队列中,服务器收到exec请求后才开始执行整个事务队列,执行完毕后一次性返回所有指令的的运行结果;Redis是单线程的,所以事务执行是有先后顺序的,Redis事务不具有“原子性”,具有“隔离性--串行化执行”
Redis事务执行优化
Redis事务一次性会执行多条指令,如果一条一条请求的话会经历多次网络读写,所以Redis客户端在执行事务时会强制使用pipeline
Watch
watch会在Redis事务开始执行前监控1个或多个关键变量,watch指令是一种乐观锁,当服务器收到exec指令要开始执行事务时,Redis会检查关键变量从watch开始之后是否被修改。如果关键变量被修改过,那么exec指令将会返回null告诉客户端事务执行失败,这个时候客户端一般会选择重试
> watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败
(nil)
当服务器给exec指令返回一个null回复时,客户端知道了事务执行是失败的,通常客户端 (jedis) 不会抛出异常,而是通过在exec方法里返回一个null,这样客户端就能判断事务是否执行了
实现一个对余额加倍的操作
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TransactionDemo {
public static void main(String[] args) {
Jedis jedis = new Jedis();
String userId = "abc";
String key = keyFor(userId);
jedis.setnx(key, String.valueOf(5)); # setnx 做初始化
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 String.format("account_{}", userId);
}
}
注意事项
Redis禁止在multi和exec之间执行watch,必须在multi之前使用watch监控关键变量
为什么Redis不支持回滚
Redis命令在事务中可能会执行失败,但是Redis事务不会回滚,而是继续会执行余下的命令
Redis这样做,主要是因为:
只有当发生语法错误(这个问题在命令队列时无法检测到)了,Redis命令才会执行失败, 或对keys赋予了一个类型错误的数据:这意味着这些都是程序性错误,这类错误在开发的过程中就能够发现并解决掉,几乎不会出现在生产环境。由于不需要回滚,这使得Redis内部更加简单,而且运行速度更快