Administrator
Administrator
发布于 2026-10-27 / 0 阅读
0
0

数据库入门到初级(18):并发与锁:两个人同时买最后一件商品怎么办

数据库入门到初级 · 第 18/22 篇。实验口径:MySQL 8.4、InnoDB、utf8mb4。案例:小型商城。

库存只剩 1。请求 A 查到 1,请求 B 也查到 1,两个请求都说“可以买”,最后各自创建一张订单。如果程序只是把库存都写成 0,数据库看起来没有负数,但实际上已经卖了两件。这叫并发竞争,不能靠“先查询一下”解决。

1. 先准备独立的库存实验表

在 shop_lab 中执行一次,避免改动主教程商品:

CREATE TABLE stock_race_demo (
  id INT PRIMARY KEY,
  stock INT NOT NULL,
  CHECK (stock >= 0)
) ENGINE=InnoDB;
INSERT INTO stock_race_demo VALUES (1, 1);

打开两个独立数据库连接,分别叫会话 A、会话 B。两个编辑标签有时共用同一连接,要先确认它们确实独立,可比较 SELECT CONNECTION_ID(); 的返回值。

2. 把判断和扣减放进同一条 UPDATE

会话 A:

START TRANSACTION;
UPDATE stock_race_demo SET stock = stock - 1
WHERE id = 1 AND stock >= 1;
SELECT ROW_COUNT();
-- 暂时不要提交

会话 B:

START TRANSACTION;
UPDATE stock_race_demo SET stock = stock - 1
WHERE id = 1 AND stock >= 1;
-- 此时通常等待 A 持有的锁

回到 A 执行 COMMIT;。B 等待结束后,条件会基于可用于更新的当前记录重新判断,库存已经是 0,因此影响 0 行。在 B 紧接执行 SELECT ROW_COUNT(); 核对,再 ROLLBACK; 结束。

两个并发请求如何竞争同一库存

如果等待太久,B 也可能先发生锁等待超时。实验时及时切换会话完成提交;应用则必须处理超时和重试,不是无限等待。

3. 为什么它比先读再写可靠

SET stock = stock - 1 WHERE stock >= 1 把读取当前值、检查条件和修改放在数据库协调的一次更新中。只有影响一行,应用才继续创建订单;影响 0 行意味着条件不满足,也可能是编号不存在,需要根据业务给出提示。

CHECK (stock >= 0) 是额外防线,但不能独自防止“两个订单都创建、库存都覆盖为 0”的逻辑错误。需要同时保证扣减结果与订单创建处在同一事务里。

4. 需要先看详情时,用锁定读取

START TRANSACTION;
SELECT stock FROM stock_race_demo WHERE id = 1 FOR UPDATE;
-- 应用在本事务中检查库存,执行必要修改
ROLLBACK;

FOR UPDATE 在显式事务中锁定读取到的相关记录,适合需要先读取并据此修改的流程。不要理解为“任何查询都应该加锁”;普通查询经常采用一致性读取,并不会像这样阻止其他事务修改记录。

锁的范围还取决于索引、查询条件与隔离级别,不是只要写了 WHERE 就必然只锁一行。使用主键精确定位,通常更容易控制访问和加锁范围。

5. 隔离级别先掌握现象

脏读:读到别人尚未提交的修改。不可重复读:同一事务两次读取同一记录,看到不同值。幻读:相同条件的多行查询,其符合条件的记录集合发生变化。

MySQL InnoDB 提供 READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE 四种级别,默认通常是 REPEATABLE READ,可执行 SELECT @@transaction_isolation; 确认当前会话。

InnoDB 的快照读取与锁定读取行为不同,REPEATABLE READ 还涉及间隙锁、临键锁等机制,不能只背一个通用现象表就推断所有并发行为。需要结合实际读写方式分析。官方隔离级别说明 提供具体行为。

6. 死锁不等于数据库坏了

A 先锁商品 1,再等商品 2;B 先锁商品 2,再等商品 1,就可能形成死锁。InnoDB 检测后会选择事务回滚,应用应在适当情况下重试整个事务。

减少死锁的常见方法是:多件商品按一致编号顺序加锁,事务尽量短,查询使用合适索引。重试要有限次数、有退避,并确保外部副作用和请求幂等,不能无限循环。

练习与答案

练习:若 A 执行的是 ROLLBACK,B 会怎样? A 的扣减撤销后,库存仍是 1,B 有机会成功扣减,影响 1 行;最后由 B 决定提交或回滚。

实验结束后确认两个连接都已结束事务。需要重做时,在没有未完成事务的连接执行 UPDATE stock_race_demo SET stock = 1 WHERE id = 1;。


上一篇:第 17 篇 · 系列目录 · 下一篇:第 19 篇


评论