在电商、新零售等业务场景中,商品库存的准确扣减是核心且敏感的环节。完全依赖数据库处理库存扣减,其核心目标是在高并发下保证数据的一致性(不超卖)、操作的原子性以及系统的性能。以下是几种经过实践检验、相对“好一点儿”的数据库处理方案。
方案一:基于乐观锁的版本号控制
这是最经典且易于理解的方案。其核心思想是假设并发冲突不常发生,在更新时检测数据是否被其他事务修改过。
实现方式:
1. 在商品库存表中增加一个版本号字段(如 version,初始为0)。
2. 扣减时,将版本号作为更新条件。
UPDATE product_stock
SET stock = stock - #{purchase_quantity},
version = version + 1
WHERE id = #{product_id}
AND stock >= #{purchase_quantity}
AND version = #{current_version}; -- 带入查询时获取的版本号
优点: 实现简单,在冲突率低的场景下性能很好。
缺点: 在高并发抢购场景下,大量事务会因版本号不一致而更新失败(需要业务层重试或提示用户),成功率低。
方案二:基于数据库行级锁(悲观锁)SELECT ... FOR UPDATE
在事务内,先锁定要更新的库存行,再进行操作,确保操作的串行化。
实现方式:`sql
BEGIN; -- 开启事务
-- 1. 锁定目标行,防止其他事务修改
SELECT stock FROM productstock WHERE id = #{productid} FOR UPDATE;
-- 2. 应用层判断库存是否充足
-- 3. 执行更新
UPDATE productstock SET stock = stock - #{quantity} WHERE id = #{productid};
COMMIT; -- 提交事务`
优点: 绝对保证一致性,逻辑简单直接。
缺点: 性能瓶颈明显,大量请求会排队等待锁,数据库连接容易被打满,不适合超高并发场景。
方案三:直接条件更新(无锁方案,推荐)
这是最推荐的纯数据库扣减方案。它利用数据库更新语句本身的原子性和行锁,在一个SQL中完成判断和扣减。
实现方式:`sql
UPDATE productstock
SET stock = stock - #{purchasequantity}
WHERE id = #{productid}
AND stock >= #{purchasequantity}; -- 核心:将库存判断放在WHERE条件中`
执行后,通过判断数据库返回的“受影响行数”(affected rows):
- 如果受影响行数为 1,说明扣减成功,库存充足且已原子性扣减。
- 如果受影响行数为 0,说明扣减失败,原因是库存不足或商品不存在。
优点:
1. 极致高效: 单条SQL,网络开销小,利用数据库原生原子性,无需额外锁。
2. 绝对安全: 在WHERE条件中校验库存,彻底杜绝超卖。
3. 简单可靠: 业务逻辑清晰,依赖数据库本身能力,维护成本低。
这是处理数据库库存扣减的黄金法则,在绝大多数场景下应作为首选。
方案四:设置库存字段为无符号整数
这是一个辅助性的“防御”策略,与上述方案结合使用。
实现方式:
在数据库表设计时,将库存字段 stock 定义为 UNSIGNED(无符号整数)。
CREATE TABLE product_stock (
id BIGINT PRIMARY KEY,
stock INT UNSIGNED NOT NULL COMMENT '库存,无符号保证不为负'
);
作用: 当执行 UPDATE SET stock = stock - 10 而库存不足时,由于字段不能为负,数据库会直接报错(如“BIGINT UNSIGNED value is out of range”)。这可以作为防止异常数据操作的最后一道防线,但业务层仍需以方案三的条件更新为主逻辑进行友好处理。
方案五:扣减与记录分离(预扣库存)
对于更复杂的场景(如购物车待付款),可以引入“可用库存”和“预扣库存”的概念。
实现方式:
1. 商品表中有两个字段:available<em>stock(可用库存)、locked</em>stock(预扣库存,如已下单未支付)。
2. 用户下单时,扣减available<em>stock,同时增加locked</em>stock。
`sql
UPDATE productstock
SET availablestock = availablestock - #{quantity},
lockedstock = lockedstock + #{quantity}
WHERE id = #{productid}
AND available_stock >= #{quantity};
`
- 支付成功时,扣减
locked_stock。 - 支付超时或取消时,将
locked<em>stock加回available</em>stock。
优点: 清晰区分库存状态,支持复杂的电商业务流程。
缺点: 业务逻辑和状态机变得更复杂。
与建议
- 通用首选: 方案三(直接条件更新) 是简单、高效、可靠的“银弹”,应作为满足基础扣减需求的首选。
- 组合使用: 将 方案三 与 方案四(无符号字段) 结合,实现代码逻辑与数据库级别的双重保障。
- 场景选择:
- 对性能要求极高,业务逻辑简单 → 方案三。
- 需要处理中间状态(如待支付)→ 方案五。
- 遗留系统或特定复杂事务 → 方案二(需谨慎评估性能)。
- 必要补充: 无论采用哪种方案,都应在数据库表上为库存字段和商品ID建立合适的索引,以加速更新操作。业务层必须妥善处理更新失败(返回受影响行数为0)的情况,给用户明确的反馈。
通过以上纯数据库层面的优化,可以在不引入复杂中间件(如Redis)的情况下,构建出高并发下稳定、准确的商品库存扣减系统。