在电商、秒杀等高并发场景中,“超卖”问题指库存被过量扣减,导致实际库存不足。以下是使用 分布式锁 和 乐观锁 解决超卖问题的原理与实现方案:
一、超卖问题的核心原因
多个并发请求同时读取库存余量,并在本地计算后发起写操作,导致实际扣减后的库存为负数。
二、解决方案 1:分布式锁
核心思想
通过 强制串行化 扣减库存操作,同一时间仅一个请求能处理库存。
实现步骤(以 Redis 为例):
-
获取锁
使用SET key uuid NX EX timeout
命令,确保原子性加锁。import redis r = redis.Redis()def acquire_lock(product_id, uuid, expire=10):key = f"lock:{product_id}"return r.set(key, uuid, nx=True, ex=expire)
-
扣减库存
在锁的保护下执行库存操作:UPDATE inventory SET stock = stock - 1 WHERE product_id = 1 AND stock > 0;
-
释放锁
使用 Lua 脚本保证原子性释放(避免误删其他请求的锁):if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1]) elsereturn 0 end
优缺点
- 优点:强一致性,逻辑简单。
- 缺点:性能瓶颈(串行化)、锁失效风险(需合理设置超时时间)。
三、解决方案 2:乐观锁
核心思想
基于 版本号 或 条件判断,在更新时校验数据未被修改,若冲突则重试或失败。
实现步骤(以 MySQL 为例):
-
查询库存与版本号
SELECT stock, version FROM inventory WHERE product_id = 1;
-
更新库存(带条件)
通过版本号或库存量确保原子性:-- 版本号方式 UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE product_id = 1 AND version = {old_version} AND stock > 0;-- 条件判断方式(直接校验库存) UPDATE inventory SET stock = stock - 1 WHERE product_id = 1 AND stock = {queried_stock} AND stock > 0;
-
检查更新结果
若影响行数为 0,说明冲突,需重试或返回错误。
代码示例(伪代码):
def deduct_stock():retries = 3for _ in range(retries):# 查询库存和版本stock, version = db.query("SELECT stock, version FROM inventory WHERE product_id=1")if stock <= 0:return "库存不足"# 尝试更新rows = db.execute("UPDATE inventory SET stock=stock-1, version=version+1 ""WHERE product_id=1 AND version=%s AND stock>0",version)if rows > 0:return "成功"return "请重试"
优缺点
- 优点:高并发性能好,无锁竞争。
- 缺点:需处理重试逻辑,冲突频繁时性能下降。
四、方案对比与选型
方案 | 适用场景 | 性能 | 复杂度 | 一致性 |
---|---|---|---|---|
分布式锁 | 强一致性、低并发冲突 | 低 | 中 | 强一致性 |
乐观锁 | 高并发、冲突较少 | 高 | 低 | 最终一致性 |
五、增强方案
-
结合缓存优化
- 使用 Redis 预扣库存,异步同步到数据库。
- 例如:先扣减 Redis 中的库存,再通过消息队列更新数据库。
-
库存分段
- 将库存拆分为多个段(如 100 个库存拆为 10 段),每段独立加锁,提升并发度。
-
限流与降级
- 使用令牌桶或漏桶算法限制请求流量,防止系统过载。
六、注意事项
- 分布式锁的可靠性
- 推荐 Redlock 算法(多节点 Redis)或 ZooKeeper 实现高可用锁。
- 乐观锁的重试策略
- 限制最大重试次数,避免无限循环。
- 事务隔离级别
- 确保数据库隔离级别为 Read Committed 或以上,避免脏读。
通过合理选择锁机制并结合业务场景优化,可有效解决超卖问题。