Redis 事务

116 阅读3分钟

概述

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);
    }
}