Redis乐观锁

# Redis乐观锁相关指令

三种命令:

  • watch
  • multi
  • exec

watch命令监听该属性,一旦监听了值之后,值就不能发生变化了,一旦变化,事务就会取消。

127.0.0.1:6379> watch stock
OK
1
2

multi命令开启事务

127.0.0.1:6379> multi
OK
1
2

再开启一个Redis客户端B,将stock的值(初始为5000)设置为4000

127.0.0.1:6379> set stock 4000
OK
1
2

回到初始客户端A,客户端A此时完全不知道stock的值已经被修改,此时客户端A仍然执行修改操作,会出现QUEUED(入队)提示。说明这个操作不会立刻被执行,只有在执行exec的时候才会执行。

127.0.0.1:6379(TX)> set stock 3000
QUEUED
1
2

使用exec命令,提示执行失败

127.0.0.1:6379(TX)> exec
(nil)
1
2

此时我们查询stock的值,发现值为客户端B修改的值

127.0.0.1:6379> get stock
"4000"
1
2

我们在客户端A中再次试验,这次没有其他客户端干扰,执行成功

127.0.0.1:6379> watch stock
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set stock 3999
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
127.0.0.1:6379> get stock
"3999"
1
2
3
4
5
6
7
8
9
10

# 代码实现Redis乐观锁

@Autowired
    private StringRedisTemplate redisTemplate;

    public void deduct() {
        //watch
        redisTemplate.watch("stock");
        //1.查询库存信息
        String stock = redisTemplate.opsForValue().get("stock");
        //2.判断库存是否充足
        if (stock != null && stock.length() != 0) {
            Integer st = Integer.valueOf(stock);
            if (st > 0) {
                //multi
                redisTemplate.multi();
                //3.扣减库存
                redisTemplate.opsForValue().set("stock", String.valueOf(--st));
                //exec
                redisTemplate.exec();
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

以上代码,在测试时会报以下错误:

io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI
1

原因是我们需要通过以下代码实现SessionCallback接口

redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                return null;
            }
        })
1
2
3
4
5
6

SessionCallback接口源码:

/**
 * Callback executing all operations against a surrogate 'session' (basically against the same underlying Redis
 * connection). Allows 'transactions' to take place through the use of multi/discard/exec/watch/unwatch commands.
 *
 * @author Costin Leau
 */
public interface SessionCallback<T> {

	/**
	 * Executes all the given operations inside the same session.
	 *
	 * @param operations Redis operations
	 * @return return value
	 */
	@Nullable
	<K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后我们进行代码改造,最终代码如下:

public void deduct() {
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                //watch
                operations.watch("stock");
                //1.查询库存信息
                String stock = operations.opsForValue().get("stock").toString();
                //2.判断库存是否充足
                if (stock != null && stock.length() != 0) {
                    Integer st = Integer.valueOf(stock);
                    if (st > 0) {
                        //multi
                        operations.multi();
                        //3.扣减库存
                        operations.opsForValue().set("stock", String.valueOf(--st));
                        //exec执行事务
                        List exec = operations.exec();
                        //如果执行结果返回值为空
                        if (exec == null || exec.size() == 0) {
                          //Thread.sleep(40);看自己计算机性能  
                          //重试
                            deduct();
                        }
                        return exec;
                    }
                }
                return null;
            }
        });
    }
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

这里我们使用RedisOperations对象操作Redis,查看RedisOperations接口源码,可以知道RedisOperations被RedisTemplate类实现了,并且RedisTemplate也被StringRedisTemplate实现了。

image-20221008135353505

# 最终测试

127.0.0.1:6379> get stock
"0"
1
2

通过压力测试

# Redis乐观锁的问题

  • 性能较低
  • 可能因为连接不够用导致乐观锁失效