Redis乐观锁
早睡蛋
# Redis乐观锁相关指令
三种命令:
- watch
- multi
- exec
watch命令监听该属性,一旦监听了值之后,值就不能发生变化了,一旦变化,事务就会取消。
127.0.0.1:6379> watch stock
OK
1
2
2
multi命令开启事务
127.0.0.1:6379> multi
OK
1
2
2
再开启一个Redis客户端B,将stock的值(初始为5000)设置为4000
127.0.0.1:6379> set stock 4000
OK
1
2
2
回到初始客户端A,客户端A此时完全不知道stock的值已经被修改,此时客户端A仍然执行修改操作,会出现QUEUED(入队)提示。说明这个操作不会立刻被执行,只有在执行exec的时候才会执行。
127.0.0.1:6379(TX)> set stock 3000
QUEUED
1
2
2
使用exec命令,提示执行失败
127.0.0.1:6379(TX)> exec
(nil)
1
2
2
此时我们查询stock的值,发现值为客户端B修改的值
127.0.0.1:6379> get stock
"4000"
1
2
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
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
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
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
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
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实现了。
# 最终测试
127.0.0.1:6379> get stock
"0"
1
2
2
通过压力测试
# Redis乐观锁的问题
- 性能较低
- 可能因为连接不够用导致乐观锁失效