问题引入
互联网项目中,存在一个非常常见的问题,就是并发问题,举一个电商项目中很常见的一个场景: 下单减库存。常规业务中商品是不能出现超卖的情况,因为这可能导致后续一些很麻烦的问题,但是如何在高并发的减库存操作下,保证库存不会被减成负数呢?
场景模拟
MySQL环境
版本:10.0.17-MariaDB
数据库使用引擎: InnoDB
事务隔离级别: REPEATABLE-READ
模拟
比如库存表中该商品的库存只有1个了
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 1 |
+----------+-------+
此时有A 、B 两个用户并发在下单(下单逻辑一般都会添加事务控制,以此保证业务数据的一致性),我们来简单模拟一下库存的修改:
开启两个终端窗口,都开启事务:
> begin;
Query OK, 0 rows affected (0.00 sec)
这个时候 A 用户开始消耗一个库存:
> update stock set stock = stock - 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
A 用户毫无悬念地修改成功,库存变成了 0:
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 0 |
+----------+-------+
此时 B 用户还在下单逻辑当中,此时查看库存:
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 1 |
+----------+-------+
库存查询结果为:1 , 这是因为 B 用户还处于事务当中,加上此时的事务隔离级别用的是 MySQL
默认的 可重复读 级别(具体详情可参考:事务的隔离级别)
我们继续模拟 B 用户也来消耗掉一个库存,不过此时要注意,在 A 用户还未提交事务之前,B用户在修改同一行数据时,会等待锁的释放,因此这之前需要提交下 A 用户的事务
> commit;
Query OK, 0 rows affected (0.05 sec)
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 0 |
+----------+-------+
可以看到, A 用户成功消耗了一个库存,此时库存为0,那么从常规业务上来讲,其他用户就不能下单了,因为已经无货可卖了。
我们继续观察 B 用户的修改:
> update stock set stock = stock - 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
可以看到修改成功了,提交事务,再查看结果:
> commit;
Query OK, 0 rows affected (0.03 sec)
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | -1 |
+----------+-------+
问题出现:库存被减成了负数!
解决问题
当然,处理该问题有很多种方法,比如借助内存缓存来支持更大的并发,或者其他更加优秀的方法。但是这里我想分享的是使用
where
条件 来解决该问题。
我们重头来过,库存重置为1,然后 A B 用户同时开启事务,此时,A 先消耗一个库存,更新时,加上where条件
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 1 |
+----------+-------+
> update stock set stock = stock - 1 where stock-1 >= 0;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
> commit;
Query OK, 0 rows affected (0.00 sec)
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 0 |
+----------+-------+
提交后,库存被正常消耗掉,B 用户在事务里面查询库存依然为1
select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 1 |
+----------+-------+
然后 B 用户消耗库存,也加上where条件:
> update stock set stock = stock - 1 where stock-1 >= 0;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
其实此时就能看出来:更新失败了!
提交事务后,再次查看是否更新成功:
> commit;
Query OK, 0 rows affected (0.00 sec)
> select * from stock where goods_id = 1;
+----------+-------+
| goods_id | stock |
+----------+-------+
| 1 | 0 |
+----------+-------+
如此:库存得以保证!
思维发散
按照上面的结果,那么where也同样可以用于其他地方来保证数据的一致性,比如订单状态的修改、红包的领取等等