简介
为了确保连读多个操作的原子性,一个成熟的数据库通常都会有事务支持。Redis事务也不例外,Redis的事务使用非常简单,不同于关系数据库,我们无需理解那么多复杂的事务模型,就可以直接使用。不过也正是因为这种简单性,他的事务模型很不严格。要求我们不能像使用关系型数据库的事务一样来使用Redis。
Redis事务的基本使用
每个事务的操作都有begin、commit和rollback,大致形式如下:
begin();
try {
command1();
command2(); ....
commit();
}
catch(Exception e) {
rollback();
}
Redis在形式上看起来也差不多,分别是multi/exec/discard。multi指示事务的开始,exec指示事务的执行,discard指示事务的丢弃。
> multi
OK
> incr books QUEUED
> incr books QUEUED
> exec
(integer) 1
(integer) 2
上面的指令演示了一个完整的事务过程,所有的指令在exec之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到exec指令,才开始执行整个事务队列
。执行完毕后,将执行结果一次性返回。因为Redis单线程特性,他不用担心自己在执行队列的时候被其他指令打搅,可以保证他们能得到【原子性】执行。
原子性
事务的原子性指要么事务全部成功,要么全部失败,那么Redis事务执行时原子性的吗? 答案:不是
> multi
OK
> set books iamastring
QUEUED // 表示指令已被缓存
> incr books
QUEUED
> set poorman iamdesperate
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range 3) OK
> get books
"iamastring"
> get poorman
"iamdesperate
上面例子中事务执行中间遇到失败了,但是后面的指令还继续执行,poorman的值能继续得到设置。
所以Redis的事务根本不能算【原子性】,他仅仅满足了事务的【隔离性】,隔离性中的串行化——当前执行的事务不被其他事务打断的权利。
discard
Redis为事务提供一个discard指令,用于丢弃事务缓存队列中的所有指令,在exec执行之前。
> get books
(nil)
> multi
OK
> incr books
QUEUED
> incr books
QUEUED
> discard
OK
> get books
(nil)
discard之后,队列中所有指令都没执行
,就好像multi和discard中间的所有指令从未发生过。
优化
上面的Redis事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络IO时间也会线性增长。所以通常Redis的客户端在执行事务时都会结合pipeline一起使用,这样可以将多次IO操作压缩为单次IO操作。
Watch
业务场景: Redis存储了我们的账户余额数据,他是一个整数。现在有两个并发的客户端要对账户余额进行修改操作,这个修改不是一个简单的incrby指令,而是要对余额乘以倍数。我们需要先取出余额然后在内存里乘以倍数,再将结果写回Redis。
这样就会出现并发问题,因为有多个客户端会并发进行操作,我们可以通过Redis的分布式锁来避免冲突,但是分布式锁属于悲观锁,此外,Redis提供了watch机制,他属于一种乐观锁。具体使用方式如下:
while True:
do_watch()
commands()
multi()
send_commands()
try:
exec()
break
except WatchError:
continue
watch会在事务开始之前盯住1个或多个关键变量,当事务执行时,也就是服务器收到了exec指令要顺序执行缓存的事务队列时,Redis会检查关键变量自watch之后,是否发生变化,如果被修改了,exec指令将返回null或者其他异常告知客户端事务执行失败。此时,客户端可以选择进行重试。
> watch books
OK
> incr books
(integer) 1
# 被修改了
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败 (nil)
注意:Redis禁止在multi和exec之间执行watch指令
。
业务场景代码
public class TransactionDemo {
public static void main(String[] args) {
Jedis jedis = new Jedis();
String userId = "abc";
String key = keyFor(userId);
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);
}
}