订单锁

业务情况如下:
订单大致存在三处修改的地方,分别为

  • 下单
  • 分车
  • 订单状态流转

前两处都涉及到车辆的分配,即库存
第三处,当订单失效时,涉及到已分配库存的回收

所以,针对同一产品(线路) 的库存修改,必须保证原子性。原子性可用队列单线程,也可对产品库存加锁实现

而因为一些原因,直接选择加锁。由于可能存在部署多个实例的情况,所以采用分布式锁

本文的重点在于,锁的使用(即,在哪里,什么时候加锁),而非分布式锁的实现。

由于资源的复杂度远小于线路,所以后面只讨论购买一个线路的情况

下单锁

下单时,可能分车,也可能不分车,由车辆调度状态决定。但是两种情况下,都涉及到库存,所以:

1
2
3
对下单过程中的库存检查,订单添加,分车逻辑一起加锁。
加锁的对象为线路库存
若订单中存在多个线路,则对对多个线路加锁

分车锁

分车以线路为单位,即:对每一条线路上的所有订单进行分车,所以:

1
分车时,对当前线路进行加锁

锁的位置与事物的位置的关系

存在如下两种情况:

情况一:

1
2
3
4
lock(order);
begin;
commit;
unlock(order);

情况二:

1
2
3
4
begin;
lock(order);
unlock(order);
commit;

情况一下,由于先加锁,后进入事物,做增删改查的操作,即:
同一时间,只有一个线程可以进入事物临界区且对库存做操作
所以不存在问题

但是,在情况二下,就复杂得多了
因为是先进入的事物,后加的锁,那么事物的隔离级别对于加锁临界区的代码所看到的数据是有一定影响的
下面先说事物隔离级别

事务隔离级别

三个名词解释

脏读

一个事务读取了另一个未提交事务写入的数据。

当两个事物并发时,考虑如下情况

1
2
3
4
5
6
7
TABEGIN			TBBEGIN
| |
读表A, 100 |
| 写表A, 100 -> 200
读表A, 200 |
| |
COMMIT COMMIT

这里,TA第二次读数据时,和第一次的不一致,即读到了在TB中为提交的修改,此为 脏读

不可重复读

一个事务重新读取前面读取过的数据,发现该数据已经被另一个已经提交的事务修改。
1
2
3
4
5
6
7
8
9
10
TABEGIN			TBBEGIN
| |
读表A, 100 |
| 写表A, 100 -> 200
| |
| COMMIT
|
读表A, 200
|
COMMIT

这里,TA第二次读数据时,和第一次的不一致,即读到了在TB中已提交的修改,此为 不可重复读
与脏读的区别在于,TB 是否已提交

幻读

一个事务重新执行一个查询,返回符合查询条件的行的集合,发现满足查询条件的行的集合因为其它最近提交的事务而发生了改变。

看起来和不可重复读一样,实际上两者不同,其区别在于
不可重复读时针对数据的修改,
幻读时针对数据的添加和删除

隔离级别

1
2
3
4
5
隔离级别	脏读	不可重复读	幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能

在postgres中,内部只存在三种隔离级别,分别对应读已提交,可重复读和可串行化。
而更严格的,在PostgreSQL的可重复读实现中,幻读是不可能的

针对以上,做如下测试

读已提交测试

TA

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
simpletour_ydf=# begin transaction isolation level read committed;
BEGIN
simpletour_ydf=# select * from test; # 初始时的查询
id | version
----+---------
2 | 1
3 | 1
1 | 8
(3 rows)

simpletour_ydf=# select * from test; # TB中,执行insert into test values(4, 1);后查询
id | version
----+---------
2 | 1
3 | 1
1 | 8
(3 rows)

simpletour_ydf=# select * from test; # TB中,执行update test set version=9 where id=1;后查询
id | version
----+---------
2 | 1
3 | 1
1 | 8
(3 rows)

simpletour_ydf=# select * from test; # TB中,执行commit;后查询
id | version
----+---------
2 | 1
3 | 1
4 | 1
1 | 9
(4 rows)

TB

1
2
3
4
5
6
7
8
9
simpletour_ydf=# begin;
BEGIN
simpletour_ydf=# insert into test values(4, 1);
INSERT 0 1
simpletour_ydf=# update test set version=9 where id=1;
UPDATE 1
simpletour_ydf=# commit;
COMMIT
simpletour_ydf=#

与上表对应,读已提交,未发生脏读,不可重复读(id为1的数据其version变为9),幻读发生(新增一行id为4)

可重复读测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
simpletour_ydf=# begin transaction isolation level repeatable read;
BEGIN
simpletour_ydf=# select * from test; # 初始查询
id | version
----+---------
1 | 7
(1 row)

simpletour_ydf=# select * from test; # 执行insert into test values(2, 1);后查询
id | version
----+---------
1 | 7
(1 row)

simpletour_ydf=# select * from test; # 执行commit;后查询(注意和上面读已提交commit;后查询的结果区分)
id | version
----+---------
1 | 7
(1 row)

simpletour_ydf=#

事物B中:

1
2
3
4
5
6
7
simpletour_ydf=# begin;
BEGIN
simpletour_ydf=# insert into test values(2, 1);
INSERT 0 1
simpletour_ydf=# commit;
COMMIT
simpletour_ydf=#

对应上表以及postgres特性 脏读,不可重复读,幻读都未发生

两个事物同时更新一行数据测试

#### 读已提交
TA

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
simpletour_ydf=# begin transaction isolation level read committed;
BEGIN
simpletour_ydf=# select * from test; # 初始查询
id | version
----+---------
2 | 1
3 | 1
4 | 1
1 | 9
(4 rows)

simpletour_ydf=# select * from test; # TB执行update test set version=10 where id=1;后查询
id | version
----+---------
2 | 1
3 | 1
4 | 1
1 | 9
(4 rows)

simpletour_ydf=# update test set version=11 where id=1; # TB执行update且未commit时,这里会阻塞等待, TB提交后,执行成功
UPDATE 1
simpletour_ydf=# select * from test; # 更新后,查询,发现已经覆盖掉TB的修改
id | version
----+---------
2 | 1
3 | 1
4 | 1
1 | 11
(4 rows)

simpletour_ydf=#

TB

1
2
3
4
5
6
7
simpletour_ydf=# begin transaction isolation level read committed;
BEGIN
simpletour_ydf=# update test set version=10 where id=1;
UPDATE 1
simpletour_ydf=# commit;
COMMIT
simpletour_ydf=#

可重复读

TA

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
simpletour_ydf=# begin transaction isolation level repeatable read;
BEGIN
simpletour_ydf=# select * from test; # 初始查询
id | version
----+---------
2 | 1
3 | 1
4 | 1
1 | 11
(4 rows)

simpletour_ydf=# select * from test; # TB执行update test set version=12 where id=1;后查询
id | version
----+---------
2 | 1
3 | 1
4 | 1
1 | 11
(4 rows)

simpletour_ydf=# update test set version=13 where id=1; # TB执行update未commit时,阻塞等待,TB提交后,postgres抛出下一行异常
ERROR: could not serialize access due to concurrent update # 当前事物由于抛出异常,已经被回滚
simpletour_ydf=#
simpletour_ydf=# select * from test; # 再执行查询,报错,需手动关闭事物
ERROR: current transaction is aborted, commands ignored until end of transaction block
simpletour_ydf=#

TB

1
2
3
4
5
6
7
simpletour_ydf=# begin transaction isolation level repeatable read;
BEGIN
simpletour_ydf=# update test set version=12 where id=1;
UPDATE 1
simpletour_ydf=# commit;
COMMIT
simpletour_ydf=#

从错误
ERROR: could not serialize access due to concurrent update
也可以看出,postgres的可重复读也是采用串行化的方式

锁与事物隔离级别

从上面事物隔离级别的分析可知,

postgres中读未提交与读已提交相同,只分析读已提交
postgres中可重复读与可串行化相似,只分析可串行化

可串行化,由于其他事物所导致的修改,一直无法读取到,即无法读取到最新数据,但因为其严格按串行化执行,若无法决定串行化顺序则抛出异常
所以可保证数据一致,但业务代码中需要捕获异常并做重试的操作

读已提交,虽然可以读取到最新数据,但有可能数据是在读取之后再在其他事务中做的更新,此时感知不到,也不是最新数据,即有能力读取最新提交数据
但不一定时最新提交数据,且存在两个事物都修改的情况(即使对更新数据的代码加锁,也会存在问题),所以读已提交不能保证数据一致性
但是,若使用乐观锁,可达到和串行化类似的效果,只不过仍然需要捕获乐观锁异常并做重试

乐观锁

update something where id=the id and version=old version

当version 与old version不相等时,更新的行数为0,即更新失败,需要重试

最终方案

了解了这么多,最终使用的方案为第一种简单方案,即

1
2
3
4
lock(order);
begin;
commit;
unlock(order);

此方案保证同一时间针对同一数据只有一个事物,可保证数据一致性,且不会有需要重试的情况

几个trick

@Transactional

使用spring ,事物都是用@Transactional注解,其存在限制条件,即:若事物要生效,必须通过代理对象调用接口才可
所以要达到最终方案的效果还需要实现如下逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
void addOrder(Order o) {
lock(o) ;
try {
self.doAddOrder(o); // 这里self需要是此对象的代理对象
} finally {
unlock(o);
}
}

@Transactional
void doAddOrder(Order o) {
...
}

锁订单,锁线路

锁订单,其实是对订单包含的资源在指定日期的库存加锁
线路属于资源的一种
所以,锁订单,锁线路,效果相同

文章目录
  1. 1. 下单锁
  2. 2. 分车锁
  3. 3. 锁的位置与事物的位置的关系
  4. 4. 事务隔离级别
    1. 4.1. 三个名词解释
      1. 4.1.1. 脏读
      2. 4.1.2. 不可重复读
      3. 4.1.3. 幻读
    2. 4.2. 隔离级别
      1. 4.2.1. 读已提交测试
      2. 4.2.2. 可重复读测试
      3. 4.2.3. 两个事物同时更新一行数据测试
        1. 4.2.3.1. 可重复读
  5. 5. 锁与事物隔离级别
    1. 5.1. 乐观锁
  6. 6. 最终方案
  7. 7. 几个trick
    1. 7.1. @Transactional
    2. 7.2. 锁订单,锁线路
,