高并发场景下的库存扣减是电商系统的核心难题——如何在保证性能的同时避免超卖(库存扣减为负)或少卖(库存未正确扣减)?本文将从数据库和缓存两个层面深入解析各类解决方案。

一、问题概述

在高并发场景下,如电商秒杀、商品抢购等,库存扣减常常面临超卖(库存扣减为负数)或少卖(库存未正确扣减)的问题。这些问题主要源于并发竞争、事务隔离、网络延迟等因素导致的数据一致性问题。

二、数据库层面解决方案

2.1 悲观锁方案

原理:通过数据库锁机制,在事务期间锁定库存记录,防止其他并发请求修改。

实现方式

1
2
3
4
5
6
7
8
-- 在事务中使用 SELECT FOR UPDATE 锁定记录
BEGIN TRANSACTION;
SELECT stock FROM product WHERE id = ? FOR UPDATE;
-- 检查库存是否充足
IF stock >= order_quantity THEN
UPDATE product SET stock = stock - order_quantity WHERE id = ?;
END IF;
COMMIT;

优点

  • 绝对保证数据一致性,完全防止超卖
  • 实现简单直观

缺点

  • 性能较差,在高并发场景下成为瓶颈
  • 容易导致死锁
  • 并发度低,用户体验差

适用场景:对数据一致性要求极高的金融级场景或小并发场景。

2.2 乐观锁方案

原理:假设不会发生冲突,只在更新时检查是否有其他线程修改过数据。

实现方式

1
2
3
4
5
6
7
-- 使用版本号字段检测冲突
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = ? AND version = ?;

-- 或者使用时间戳字段
UPDATE product SET stock = stock - 1, updated_at = NOW()
WHERE id = ? AND updated_at = ?;

优点

  • 性能好,并发度高
  • 无死锁风险
  • 适合读多写少的场景

缺点

  • 冲突时需要重试,失败率随并发增加
  • 需要额外的重试逻辑
  • 需要额外的版本字段

适用场景:并发适中、冲突不频繁的场景。

2.3 原子操作方案

原理:利用数据库本身的原子性保证库存扣减的安全性。

实现方式

1
2
3
-- 原子性的库存扣减操作
UPDATE product SET stock = stock - ?
WHERE id = ? AND stock >= ?;

优点

  • 简单高效
  • 利用数据库的ACID特性,避免超卖
  • 无需额外的锁机制

缺点

  • 需要重试机制处理失败情况
  • 对于复杂的业务逻辑支持有限

三、缓存层面解决方案

3.1 Redis 原子操作方案

原理:利用 Redis 单线程模型的原子性来处理库存扣减。

实现方式

1
2
3
4
5
6
7
-- Lua 脚本确保操作的原子性
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end

优点

  • 性能极高,QPS 可达万级以上
  • 天然防超卖
  • 支持高并发场景

缺点

  • 存在缓存与数据库一致性问题
  • 需要额外的缓存同步机制

适用场景:高并发、低延迟的秒杀场景。

3.2 Redis 分布式锁方案

原理:使用分布式锁确保同一时间只有一个请求可以扣减库存。

实现方式

  1. 基于 Redis 原生命令的实现(Java代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String lockKey = "stock_lock:" + productId;
String requestId = UUID.randomUUID().toString();

try {
// 获取锁
Boolean acquired = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofSeconds(30));

if (Boolean.TRUE.equals(acquired)) {
// 执行库存扣减
// ... 扣减库存的业务逻辑 ...
return true;
} else {
return false; // 获取锁失败
}
} finally {
// 释放锁(使用Lua脚本确保原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), requestId);
}
  1. 基于 Redisson 框架的实现:
1
2
3
4
5
6
7
8
9
10
// 使用 Redisson 实现分布式锁
RLock lock = redisson.getLock("stock_lock:" + productId);
try {
if (lock.tryLock(10, TimeUnit.SECONDS)) {
// 检查并扣减库存
// 更新数据库
}
} finally {
lock.unlock();
}

优点

  • 强一致性保证
  • 可以跨多个服务实例工作

缺点

  • 性能瓶颈,锁竞争严重
  • 可能出现死锁

适用场景:对一致性要求极高的场景。

3.3 Redis 预扣库存方案

原理:在 Redis 中预扣库存,后续异步同步到数据库。

实现方式

1
2
3
4
-- 扣减实时库存
DECRBY product_stock_{productId} {quantity}
-- 同时记录预扣信息
SADD reserved_orders_{productId} {orderId}

优点

  • 高性能,快速响应
  • 可以处理瞬时高并发

缺点

  • 实现复杂,需要处理各种异常情况
  • 存在缓存与数据库不一致的风险

4、总结

针对高并发下的库存扣减问题,数据库层面的乐观锁和原子操作方案各有优势,而缓存层面的 Redis 解决方案则适用于对性能要求更高的场景。

选择哪种方案需要根据具体的业务特点、并发程度和性能要求来决定。关键在于平衡数据一致性、系统性能和实现复杂度之间的关系。