You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

SQL事务如何保障数据完整性?以图书销售场景为例

如何用SQL事务解决购书场景的库存超卖问题

嘿,这个超卖场景绝对是电商后端的经典踩坑点!结合你说的多客户端购书案例,我来拆解下SQL事务到底怎么保障数据完整性,避免库存卖超。

先搞懂问题根源

你提到的场景里,问题出在检查库存和扣减库存是两个独立的操作,中间有时间窗口让其他事务钻空子:客户端2刚通过库存检查(此时库存还是10),还没扣减,客户端1也完成了检查,最后两个都扣减就导致库存变成10-6-7=-3,彻底超卖。

要解决这个问题,核心是把「检查+扣减」这两步放进同一个事务里,并且通过锁机制或者版本控制,让这一系列操作成为原子性的不可分割的动作

方案1:悲观锁(Pessimistic Locking)

适合并发量不是特别高的场景,思路是:我要操作这条库存记录时,先把它锁住,不让别人碰,直到我完成所有操作再释放锁。

具体做法是在查询库存时使用SELECT ... FOR UPDATE语句,这条语句会给查询到的记录加排他锁(Exclusive Lock),其他事务想要修改或者加锁这条记录时,必须等待当前事务提交或回滚。

示例SQL事务流程

-- 开启事务
START TRANSACTION;

-- 查询库存并加排他锁,此时其他事务无法修改这条记录
SELECT quantity FROM books WHERE id = 1 FOR UPDATE;

-- 假设查到库存是10,客户端要购买6本,检查后执行扣减
UPDATE books SET quantity = quantity - 6 WHERE id = 1;

-- 提交事务,释放锁
COMMIT;

在你的场景里:

  • 客户端2先执行SELECT ... FOR UPDATE,拿到锁开始处理
  • 客户端1此时再执行同样的查询,会被阻塞,直到客户端2的事务提交
  • 客户端2提交后,库存变成3,客户端1查询到的库存就是3,自然通不过buy.quantity <= available.quantity的检查,不会执行扣减,避免超卖。

方案2:乐观锁(Optimistic Locking)

适合高并发场景,思路是:我不提前锁记录,而是在真正更新库存时,验证这条记录在我查询后有没有被修改过,如果没被修改就更新,否则就失败重试。

常见的实现方式有两种:

  1. 版本号字段:给books表加一个version字段,每次更新时版本号+1,更新时带上查询到的版本号
  2. 直接用库存字段本身做验证:更新时确保库存足够

示例(用库存字段验证)

-- 开启事务
START TRANSACTION;

-- 先查询当前库存(不加锁)
SELECT quantity FROM books WHERE id = 1;

-- 假设查到库存是10,要购买6本,执行更新时附带库存条件
UPDATE books SET quantity = quantity - 6 WHERE id = 1 AND quantity >= 6;

-- 检查更新影响的行数:如果是1,说明更新成功;如果是0,说明库存已经被别人改了,不够了
ROW_COUNT();

-- 根据ROW_COUNT的结果决定提交还是回滚
IF ROW_COUNT() > 0 THEN
    COMMIT;
ELSE
    ROLLBACK;
END IF;

在你的场景里:

  • 客户端2先执行更新,库存从10变成3,更新影响行数1,提交成功
  • 客户端1再执行更新时,WHERE quantity >=6的条件不满足(此时库存是3),更新影响行数0,触发回滚,购买失败,不会超卖。

这种方式的好处是不会阻塞其他事务,并发性能更好,但需要在业务代码里处理更新失败的重试逻辑(比如提示用户“库存不足,请重试”或者自动重试几次)。

关键注意事项

  • 一定要把「查询+检查+更新」放在同一个事务里,不能拆分,否则锁机制起不到作用
  • 事务的隔离级别至少要设置为REPEATABLE READ(可重复读),避免出现不可重复读的问题(不同时间点同一个事务读到的库存不一样)
  • 悲观锁要注意锁的范围,别不小心锁了整张表,影响性能;乐观锁要处理好重试逻辑,避免用户体验不好

内容的提问来源于stack exchange,提问作者goldbalaji13

火山引擎 最新活动