概述
Redis 的事务使用很简单,不同于关系数据库,它的事务模型很不严格,我们不能像使用关系数据库的事务一样来使用 Redis。
基本使用
每个事务的操作都有 begin、commit 和 rollback ,begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。它大致的形式如下。
begin();
try {
command1();
command2();
....
commit();
} catch(Exception e) {
rollback();
}
Redis 在形式上看起来也差不多,分别是 multi/exec/discard。
multi指示事务的开始exec指示事务的执行discard指示事务的丢弃。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set num 5
QUEUED
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 6
3) (integer) 7
上面的指令演示了一个完整的事务过程,所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name dog
QUEUED
127.0.0.1:6379> incr name
QUEUED
127.0.0.1:6379> set name 2
QUEUED
127.0.0.1:6379> incr name
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range # 字符型累加,异常
3) OK
4) (integer) 3 # 异常后,仍能正常执行
Redis 的事务根本不能算「原子性」,比如上面,中间操作抛出了异常,执行失败,但是后边的指令还是得到了执行,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。
discard(丢弃)
127.0.0.1:6379> set num 20
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get num
"20"
discard 之后,队列中的所有指令都没执行,就好像 multi 和 discard 中间的所有指令从未发生过一样。
Watch
当有多个客户端会并发进行操作,会出现并发问题。我们可以通过 Redis 的分布式锁来避免冲突,这是一个很好的解决方案。分布式锁是一种悲观锁,那是不是可以使用乐观锁的方式来解决冲突呢?
Redis 提供了这种 watch 的机制,它就是一种乐观锁。watch 的使用方式如下:
while True:
do_watch()
commands()
multi()
send_commands()
try:
exec()
break
except WatchError:
continue
watch 会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null 回复告知客户端事务执行失败,这个时候客户端一般会选择重试。
127.0.0.1:6379> watch num # 开始盯住变量
OK
127.0.0.1:6379> incr num # 被修改
(integer) 21
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> exec
(nil) # 返回为null,事务执行失败
注意事项 : Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错。
余额加倍代码示例
package jedis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
public class TransactionTest {
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) {
// watch 库存
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; // 成功了
}
// 否在,返回null, 库存被其他人改动了,重新执行
}
return Integer.parseInt(jedis.get(key)); // 重新获取余额
}
public static String keyFor(String userId) {
return String.format("account_%s", userId);
}
}