Redis事务

Redis事务:是一个单独的隔离操作。一个事务中的所有命令都会被序列化、按顺序执行。事务在执行的过程中不会被其他命令打断。主要作用就是串联多个命令,防止其他命令插队。

Redis事务流程

  • 开启事务 (multi)
  • 命令入队
  • 执行事务(exec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
4) "v2"
5) OK

放弃事务

1
2
127.0.0.1:6379> discard
OK

组队的时候发生异常,事务中的所用命令都不会执行。

运行时发生异常,其他命令正常执行,异常命令抛出异常。

监控(使用watch实现乐观锁)

悲观锁

  • 认为任何时候都会出现问题,无论做什么都加锁。

乐观锁

  • 认为什么时候都不会出问题,所以不会上锁,更新数据时去判断一下,在此期间是否有人修改过这个数据。
    • 获取version
    • 更新的时候比较version

测试监视功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> set money 100  
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视money对象
OK
127.0.0.1:6379> multi # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

测试多线程修改值,使用watch可以当作redis的乐观锁操作

客户端1:对money对象进行监视,并开启事务,先不执行事务

1
2
3
4
5
6
7
8
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED

再打开一个客户端2,对money对象的值进行修改

1
2
3
4
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 100
OK

在客户端1中执行事务,因为监视的值发生了改变,所以事务执行失败!

1
2
127.0.0.1:6379> exec
(nil)

自旋锁

执行失败后,放弃监视(解锁),再重新监视(加锁),获取监视对象的最新值,再执行事务,直到成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 40

Reids事务特性

  • 单独的隔离操作
    • 事务中的所有命令都会被序列化、按顺序执行。在事务执行的过程中不会被其他客户端发送过来的命令请求所打断。
  • 不保证原子性
    • 事务中如果有一条命令执行失败,其后的命令仍然会执行,没有回滚。
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
@RestController
@RequestMapping("/testRedis")
public class RedisTestController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@GetMapping("/secKill")
public String secKill(String userId, String productId) {
Assert.notNull(userId, "用户id不能为空!");
Assert.notNull(productId, "产品id不能为空!");
// 秒杀成功用户key
String successUserKey = "sk:successUser" + productId;
// 商品库存key
String productKey = "sk:product:" + productId;
// 监视库存的key
redisTemplate.watch(productKey);
// 获取库存
Integer stock = (Integer) redisTemplate.opsForValue().get(productKey);
if (stock == null) {
return "秒杀没有开始!";
}
// 判断用户是否重复秒杀
Long count = redisTemplate.opsForSet().add(successUserKey, userId);
if (count == null || count <= 0) {
return "已经秒杀成功,不可重复参与!";
}
// 判断秒杀是否结束
if (stock <= 0) {
return "秒杀结束!";
}

redisTemplate.multi();
// 秒杀成功 库存减1
redisTemplate.opsForValue().decrement(productKey);
// 将userId添加到秒杀成功的set中
redisTemplate.opsForSet().add(successUserKey, userId);
if (redisTemplate.exec().size() == 0) {
return "秒杀失败!";
} else {
return "秒杀成功!";
}
}
}