首先说明,redis事务只满足了隔离性,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。
1. 命令无法排队(外表错误;): 命令参数数量错误,或者命令不存在等,导致无法排队。2.6.5 之前版本会导致其余有效命令正常执行,2.6.5之后的版本一旦遇到无法排队错误,会拒绝执行整个事务的所有语句;
2. 调用Esec后发现的错误(执行期间错误): 一般由于对错误的Key或Val执行了不符合场景的操作,例如:对字符串执行incr操作;或文档中介绍的对字符串执行列表才支持的操作; Exec执行命令执行后出现的错误的命令外,其余事务中的语句仍然会正常执行,即所谓“部分”语句执行成功了
除了不能回滚外,一旦进程挂了,会造成事务丢失。一般解决这两个问题,会采用预写日志策略。比如,undo log实现事务回滚,redo实现事务重做
Redis 在形式上看起来也差不多,分别是 multi/exec/discard。multi 指示事务的开始,exec 指示事务的执行,discard 指示事务的丢弃。
> multi
OK
> incr books
QUEUED
> incr books Q
UEUED
> exec
(integer) 1
(integer) 2
优化:使用pipeline进行压缩
上面的 Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。比如我们在使用 Python 的 Redis 客户端时执行事务时是要强制使用 pipeline 的。
pipe = redis.pipeline(transaction=true)
pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute()
watch:乐观锁,解决并发修改的问题(分布式锁是一种悲观锁)
> watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败 (nil)
注意事项
Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错。
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_%s", userId);
}
}