014.Redis事务案例研究

用户及用户包裹的数据结构如下:

2021-11-03-19-31-17

市场的数据结构如下:

2021-11-03-19-31-52

商品上架

为了将商品放到市场上进行销售,程序除了要使用MULTI命令和EXEC命令之外,还需要配合使用WATCH命令,有时候甚至还会用到UNWATCH或DISCARD命令。

在用户使用WATCH命令对键进行监视之后,直到用户执行EXEC命令的这段时间里面,如果有其他客户端抢先对任何被监视的键进行了替换、更新或删除等操作,那么当用户尝试执行EXEC命令的时候,事务将失败并返回一个错误(之后用户可以选择重试事务或者放弃事务)。

通过用WATCH,MULTI/EXEC UNWATCH/DISCARD等命令,程序可以在执行某些重要操作的时候,通过确保自己正在使用的数据没有发生变化来避免数据出错。

理解Discard

UNWATCH命令可以在WATCH命令执行之后、MULTI命令执行之前对连接进行重置(reset);同样地,DISCARD命令也可以在MULTI命令执行之后、EXEC命令执行之前对连接进行重置。

这也就是说,用户在使用WATCH监视一个或多个键,接着使用MULTI开始一个新的事务,并将多个命令入队到事务队列之后,仍然可以通过发送DISCARD命令来取消WATCH命令并清空所有已入队命令。

小总结:

  1. WATCH、MULTI、EXEC的使用流程是:先试用watch监视一两个键,然后使用multi开启管道,然后输入执行命令,然后调用exec进行执行。

  2. 如果调用了watch之后,在调用multi之前,可以使用unwatch取消监控,如果在multi之后,则可以调用discard进行取消。

我先简单的这么理解。

购买商品

  1. 程序首先使用WATCH对市场以及买家的个人信息进行监视,然后获取买家拥有的钱数以及商品的售价,并检查买家是否有足够的钱来购买该商品。

  2. 如果买家没有足够的钱,那么程序会取消事务;相反地,如果买家的钱足够,那么程序首先会将买家支付的钱转移给卖家,然后将售出的商品移动至买家的包裹,并将该商品从市场中移除。

  3. 当买家的个人信息或者商品买卖市场出现变化而导致WatchError异常出现时,程序将进行重试,其中最大重试时间为10秒。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

stringRedisTemplate.execute((RedisCallback<Object>) connection -> {
    String buyer = String.format("users:%s", buyerId);
    String seller = String.format("users:%s", sellerId);
    String item = String.format("%s.%s", itemId, sellerId);
    String inventory = String.format("inventory:%s", buyerId);

    LocalDateTime end = LocalDateTime.now().plusSeconds(10);

    // 这个地方肯定写法有问题,但是不纠结
    while (LocalDateTime.now().isBefore(end)) {

        try {
            // 对商品买卖市场以及买家的个人信息进行监视
            connection.watch(String.format("market:%s", buyer).getBytes(StandardCharsets.UTF_8));

            // 检查买家想购买的商品的价格是否出现了变化,以及买家是否有
            // 足够的钱来购买这件商品
            Double price = connection.zScore(
                    "market:".getBytes(StandardCharsets.UTF_8),
                    item.getBytes(StandardCharsets.UTF_8));
            Double founds = Double.valueOf(Arrays.toString(connection.hGet(
                    buyer.getBytes(StandardCharsets.UTF_8),
                    "funds".getBytes(StandardCharsets.UTF_8))));
            if (price != lprice || price > founds) {
                connection.unwatch();
                return null;
            }

            // 先将买家支付的钱转义给卖家,然后将被购买的商品移交给买家
            connection.multi();

            connection.hIncrBy(
                    seller.getBytes(StandardCharsets.UTF_8),
                    "founds".getBytes(StandardCharsets.UTF_8),
                    price);
            connection.hIncrBy(
                    buyer.getBytes(StandardCharsets.UTF_8),
                    "founds".getBytes(StandardCharsets.UTF_8),
                    price);
            connection.sAdd(
                    inventory.getBytes(StandardCharsets.UTF_8),
                    itemId.getBytes(StandardCharsets.UTF_8));
            connection.zRem(
                    "market:".getBytes(StandardCharsets.UTF_8),
                    item.getBytes(StandardCharsets.UTF_8));

            connection.exec();

            return true;
        } catch (Exception e) {
            // 如果买家的个人信息或者商品买卖市场在交易的过程中出现了变化,
            // 那么进行重试
            e.printStackTrace();
        }
    }
    return false;
});