预扣库存模式存在的问题
1. 库存超卖
问题描述:这是最核心的问题。虽然在预扣时已经检查并占用了库存,但在高并发场景下,多个请求可能同时检查到库存充足(例如最后一件商品),然后都成功执行了预扣操作,导致实际预扣数量大于真实库存。
产生原因:
数据库更新非原子性:经典的“查询-判断-更新”流程不是原子操作。在查询和更新之间,其他请求可能已经修改了库存数据。
缓存与数据库不一致:为了性能,库存经常放在缓存(如Redis)中。如果缓存和数据库的数据同步有延迟,也可能导致读到脏数据。
2. 恶意或无效占用
问题描述:买家下单后占用库存,但既不付款也不取消订单,直到超时释放。如果有恶意用户利用脚本大量下单,会瞬间占用大量库存,导致正常用户无法购买,相当于一种库存层面的DDoS攻击。
产生原因:系统为每个订单都提供了“信用”和“等待期”,被别有用心的人滥用。
3. 系统复杂性增加
问题描述:相比于简单的“付款减库存”,预扣模式引入了状态管理和超时释放机制,系统复杂度显著上升。
状态管理:库存需要区分“总库存”、“可用库存”、“已预扣库存”。
定时任务:需要一个非常可靠的延时任务(如消息队列的延迟消息、定时扫表)来在超时后自动释放库存。
数据一致性:需要保证预扣、支付成功扣减、超时释放等多个操作的数据一致性。
4. 用户体验可能受损
问题描述:用户在付款时可能被提示“库存不足,付款失败”。这比在下单时就告知无货的体验更差,因为用户已经完成了地址选择、提交订单等操作,产生了更高的购买预期和心理落差。
5. 数据库性能压力
问题描述:预扣和释放库存都是数据库的写操作。在大促期间(如双11),大量的并发下单和订单超时释放,会给数据库带来巨大的读写压力。
相应的解决方案
针对以上问题,业界有成熟的应对策略:
1. 解决超卖问题:保证操作的原子性
在数据库中:使用数据库的悲观锁(如 )或乐观锁(通过版本号
SELECT ... FOR UPDATE 字段)来更新库存。在更新时,条件中必须包含库存数量或版本号。
version
SQL示例(乐观锁):
sql
UPDATE inventory SET available_stock = available_stock - 1, locked_stock = locked_stock + 1 WHERE item_id = 123 AND available_stock >= 1;
如果这条SQL执行后影响的行数为0,说明预扣失败,库存不足。
在缓存中(推荐):使用Redis等高性能缓存。Redis的 (递减)命令是原子性的,非常适合处理库存。
DECR
流程:将商品总库存预先加载到Redis。预扣时,执行 命令扣减可用库存,同时在一个独立的集合(Set)或哈希(Hash)中记录用户预扣的记录。超时或付款后,再执行
DECR 命令恢复或最终扣减。
INCR
2. 解决恶意占用问题:多管齐下
风险控制:引入风控系统,识别异常下单行为(如单一IP/账号短时间内大量下单),并进行拦截(如弹出验证码、限制购买)。
限制购买数量:对热门商品实施单人购买数量上限。
动态调整预扣时间:对于被识别为高需求的商品,可以适当缩短预扣时间(如从30分钟缩短到5分钟),加速库存回流。
用户信用体系:对于频繁下单不付款的用户,可以降低其信用评分,或在后续订单中缩短其预扣时间。
3. 降低系统复杂性:采用成熟组件和清晰架构
使用消息队列处理超时:订单创建后,向消息队列(如RabbitMQ, RocketMQ, Kafka)发送一个延迟消息。消息到期后,消费者检查订单状态,若未支付则执行释放库存的操作。这比数据库定时任务更高效、可靠。
清晰的库存服务:将库存的扣减、锁定、释放等操作抽象成一个独立的“库存服务”,对外提供稳定的API,内部处理所有复杂逻辑。
4. 优化用户体验:清晰的提示与流程
在关键节点明确提示:在商品页、订单确认页明确提示“库存紧张”或“为您保留XX分钟”。
友好的失败提示:当付款时发现库存不足,应向用户清晰地解释原因(如“商品非常抢手,您下单后已被其他买家买走”),并引导用户查看其他类似商品或设置到货通知。
5. 缓解性能压力:读写分离与缓存
读写分离:数据库采用主从架构,写操作(预扣、释放)走主库,读操作(查询库存)走从库。
缓存扛量:如前所述,将库存扣减的核心逻辑放在Redis中,利用其极高的并发处理能力来应对流量洪峰。
实现一个完整的预扣库存系统
1. 核心依赖和配置
xml
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>
</dependencies>
yaml
# application.yml
spring:
redis:
host: localhost
port: 6379
database: 0
inventory:
reserve-timeout: 600 # 预扣超时时间(秒)
2. 库存服务核心实现
java
// 库存服务接口
public interface InventoryService {
/**
* 预扣库存
* @param productId 商品ID
* @param quantity 数量
* @param userId 用户ID
* @param orderId 订单ID
* @return 预扣结果
*/
boolean reserveStock(String productId, int quantity, String userId, String orderId);
/**
* 确认扣减库存(支付成功)
* @param orderId 订单ID
* @return 确认结果
*/
boolean confirmStock(String orderId);
/**
* 释放预扣库存(支付失败/超时)
* @param orderId 订单ID
* @return 释放结果
*/
boolean releaseStock(String orderId);
/**
* 获取商品可用库存
* @param productId 商品ID
* @return 可用库存数量
*/
int getAvailableStock(String productId);
}
java
// Redis库存服务实现
@Service
@Slf4j
public class RedisInventoryService implements InventoryService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
@Value("${inventory.reserve-timeout:600}")
private int reserveTimeout;
// Redis Key 常量
private static final String STOCK_KEY_PREFIX = "stock:product:";
private static final String RESERVED_STOCK_KEY_PREFIX = "stock:reserved:";
private static final String ORDER_RESERVATION_KEY = "order:reservation:";
private static final String STOCK_LOCK_KEY_PREFIX = "lock:stock:";
@Override
public boolean reserveStock(String productId, int quantity, String userId, String orderId) {
if (quantity <= 0) {
throw new IllegalArgumentException("扣减数量必须大于0");
}
String lockKey = STOCK_LOCK_KEY_PREFIX + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 获取分布式锁,避免超卖
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
String stockKey = STOCK_KEY_PREFIX + productId;
String reservedKey = RESERVED_STOCK_KEY_PREFIX + productId;
String orderReservationKey = ORDER_RESERVATION_KEY + orderId;
// 获取当前可用库存
Integer availableStock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (availableStock == null) {
log.warn("商品 {} 库存信息不存在", productId);
return false;
}
// 检查库存是否充足
if (availableStock < quantity) {
log.info("商品 {} 库存不足,需要 {},实际 {}", productId, quantity, availableStock);
return false;
}
// 使用 Lua 脚本保证原子性操作
String luaScript = """
local stockKey = KEYS[1]
local reservedKey = KEYS[2]
local orderKey = KEYS[3]
local quantity = tonumber(ARGV[1])
local timeout = tonumber(ARGV[2])
local orderInfo = ARGV[3]
-- 检查库存
local availableStock = tonumber(redis.call('get', stockKey))
if availableStock == nil or availableStock < quantity then
return 0
end
-- 扣减可用库存
redis.call('decrby', stockKey, quantity)
-- 增加预扣库存
redis.call('incrby', reservedKey, quantity)
-- 记录订单预扣信息
redis.call('setex', orderKey, timeout, orderInfo)
return 1
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
String orderInfo = String.format("%s:%s:%d", productId, userId, quantity);
List<String> keys = Arrays.asList(stockKey, reservedKey, orderReservationKey);
Long result = redisTemplate.execute(redisScript, keys, quantity, reserveTimeout, orderInfo);
boolean success = result != null && result == 1;
if (success) {
log.info("预扣库存成功 - 商品: {}, 数量: {}, 订单: {}", productId, quantity, orderId);
// 发送延时消息,用于超时释放
scheduleStockRelease(orderId);
}
return success;
} else {
log.warn("获取库存锁失败 - 商品: {}", productId);
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("预扣库存中断异常", e);
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@Override
public boolean confirmStock(String orderId) {
String orderReservationKey = ORDER_RESERVATION_KEY + orderId;
String reservationInfo = (String) redisTemplate.opsForValue().get(orderReservationKey);
if (reservationInfo == null) {
log.warn("订单 {} 预扣信息不存在,可能已超时释放", orderId);
return false;
}
// 解析预扣信息: productId:userId:quantity
String[] parts = reservationInfo.split(":");
if (parts.length != 3) {
log.error("订单 {} 预扣信息格式错误: {}", orderId, reservationInfo);
return false;
}
String productId = parts[0];
int quantity = Integer.parseInt(parts[2]);
String lockKey = STOCK_LOCK_KEY_PREFIX + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
String reservedKey = RESERVED_STOCK_KEY_PREFIX + productId;
String luaScript = """
local reservedKey = KEYS[1]
local orderKey = KEYS[2]
local quantity = tonumber(ARGV[1])
-- 检查预扣记录是否存在
if not redis.call('exists', orderKey) then
return 0
end
-- 减少预扣库存
local currentReserved = tonumber(redis.call('get', reservedKey))
if currentReserved == nil or currentReserved < quantity then
return -1
end
redis.call('decrby', reservedKey, quantity)
-- 删除订单预扣记录
redis.call('del', orderKey)
return 1
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
List<String> keys = Arrays.asList(reservedKey, orderReservationKey);
Long result = redisTemplate.execute(redisScript, keys, quantity);
boolean success = result != null && result == 1;
if (success) {
log.info("确认库存成功 - 订单: {}, 商品: {}, 数量: {}", orderId, productId, quantity);
} else if (result != null && result == -1) {
log.error("预扣库存数据不一致 - 订单: {}", orderId);
}
return success;
} else {
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@Override
public boolean releaseStock(String orderId) {
String orderReservationKey = ORDER_RESERVATION_KEY + orderId;
String reservationInfo = (String) redisTemplate.opsForValue().get(orderReservationKey);
if (reservationInfo == null) {
log.info("订单 {} 预扣信息已不存在", orderId);
return true; // 视为释放成功
}
String[] parts = reservationInfo.split(":");
if (parts.length != 3) {
log.error("订单 {} 预扣信息格式错误: {}", orderId, reservationInfo);
return false;
}
String productId = parts[0];
int quantity = Integer.parseInt(parts[2]);
String lockKey = STOCK_LOCK_KEY_PREFIX + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
String stockKey = STOCK_KEY_PREFIX + productId;
String reservedKey = RESERVED_STOCK_KEY_PREFIX + productId;
String luaScript = """
local stockKey = KEYS[1]
local reservedKey = KEYS[2]
local orderKey = KEYS[3]
local quantity = tonumber(ARGV[1])
-- 检查预扣记录是否存在
if not redis.call('exists', orderKey) then
return 0
end
-- 恢复可用库存
redis.call('incrby', stockKey, quantity)
-- 减少预扣库存
local currentReserved = tonumber(redis.call('get', reservedKey))
if currentReserved ~= nil and currentReserved >= quantity then
redis.call('decrby', reservedKey, quantity)
end
-- 删除订单预扣记录
redis.call('del', orderKey)
return 1
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
List<String> keys = Arrays.asList(stockKey, reservedKey, orderReservationKey);
Long result = redisTemplate.execute(redisScript, keys, quantity);
boolean success = result != null && result == 1;
if (success) {
log.info("释放库存成功 - 订单: {}, 商品: {}, 数量: {}", orderId, productId, quantity);
}
return success;
} else {
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@Override
public int getAvailableStock(String productId) {
String stockKey = STOCK_KEY_PREFIX + productId;
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
return stock != null ? stock : 0;
}
/**
* 调度库存释放任务(模拟延时队列)
*/
private void scheduleStockRelease(String orderId) {
// 实际项目中可以使用 RocketMQ/RabbitMQ 的延迟消息
// 这里使用简单的线程池模拟
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
try {
// 检查订单是否已支付
if (isOrderUnpaid(orderId)) {
releaseStock(orderId);
log.info("订单 {} 预扣超时,库存已自动释放", orderId);
}
} catch (Exception e) {
log.error("释放库存任务执行失败 - 订单: {}", orderId, e);
}
}, reserveTimeout, TimeUnit.SECONDS);
}
/**
* 检查订单是否未支付(模拟)
*/
private boolean isOrderUnpaid(String orderId) {
// 实际项目中需要查询订单服务
// 这里简单返回true模拟未支付状态
return true;
}
/**
* 初始化商品库存(测试用)
*/
public void initProductStock(String productId, int stock) {
String stockKey = STOCK_KEY_PREFIX + productId;
String reservedKey = RESERVED_STOCK_KEY_PREFIX + productId;
redisTemplate.opsForValue().set(stockKey, stock);
redisTemplate.opsForValue().set(reservedKey, 0);
log.info("初始化商品库存 - 商品: {}, 库存: {}", productId, stock);
}
}
3. 订单服务控制器
java
@RestController
@RequestMapping("/api/order")
@Slf4j
public class OrderController {
@Autowired
private InventoryService inventoryService;
/**
* 创建订单(预扣库存)
*/
@PostMapping("/create")
public ResponseEntity<OrderResult> createOrder(@RequestBody CreateOrderRequest request) {
try {
boolean success = inventoryService.reserveStock(
request.getProductId(),
request.getQuantity(),
request.getUserId(),
request.getOrderId()
);
if (success) {
return ResponseEntity.ok(new OrderResult(true, "订单创建成功", request.getOrderId()));
} else {
return ResponseEntity.ok(new OrderResult(false, "库存不足", null));
}
} catch (Exception e) {
log.error("创建订单失败", e);
return ResponseEntity.status(500)
.body(new OrderResult(false, "系统错误", null));
}
}
/**
* 支付订单(确认库存)
*/
@PostMapping("/pay")
public ResponseEntity<OrderResult> payOrder(@RequestParam String orderId) {
try {
boolean success = inventoryService.confirmStock(orderId);
if (success) {
return ResponseEntity.ok(new OrderResult(true, "支付成功", orderId));
} else {
return ResponseEntity.ok(new OrderResult(false, "库存确认失败,可能已超时", null));
}
} catch (Exception e) {
log.error("支付订单失败", e);
return ResponseEntity.status(500)
.body(new OrderResult(false, "系统错误", null));
}
}
/**
* 取消订单(释放库存)
*/
@PostMapping("/cancel")
public ResponseEntity<OrderResult> cancelOrder(@RequestParam String orderId) {
try {
boolean success = inventoryService.releaseStock(orderId);
if (success) {
return ResponseEntity.ok(new OrderResult(true, "订单取消成功", orderId));
} else {
return ResponseEntity.ok(new OrderResult(false, "取消订单失败", null));
}
} catch (Exception e) {
log.error("取消订单失败", e);
return ResponseEntity.status(500)
.body(new OrderResult(false, "系统错误", null));
}
}
}
// 请求和响应对象
@Data
class CreateOrderRequest {
private String productId;
private Integer quantity;
private String userId;
private String orderId;
}
@Data
@AllArgsConstructor
class OrderResult {
private Boolean success;
private String message;
private String orderId;
}
4. 库存监控和管理
java
@Service
@Slf4j
public class InventoryMonitorService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private InventoryService inventoryService;
/**
* 获取库存概览
*/
public InventoryOverview getInventoryOverview(String productId) {
int availableStock = inventoryService.getAvailableStock(productId);
String reservedKey = "stock:reserved:" + productId;
Integer reservedStock = (Integer) redisTemplate.opsForValue().get(reservedKey);
return new InventoryOverview(productId, availableStock,
reservedStock != null ? reservedStock : 0);
}
/**
* 手动释放所有超时库存(补偿机制)
*/
public int releaseExpiredReservations() {
int releasedCount = 0;
// 扫描所有订单预扣记录(实际项目中应该用更高效的方式)
Set<String> keys = redisTemplate.keys("order:reservation:*");
if (keys != null) {
for (String key : keys) {
String orderId = key.replace("order:reservation:", "");
// 检查订单状态,如果未支付则释放
if (isOrderExpiredAndUnpaid(orderId)) {
if (inventoryService.releaseStock(orderId)) {
releasedCount++;
}
}
}
}
log.info("手动释放超时库存完成,共释放 {} 个订单", releasedCount);
return releasedCount;
}
private boolean isOrderExpiredAndUnpaid(String orderId) {
// 实际项目中需要查询订单服务
// 这里简化实现
return true;
}
}
@Data
@AllArgsConstructor
class InventoryOverview {
private String productId;
private int availableStock;
private int reservedStock;
public int getTotalStock() {
return availableStock + reservedStock;
}
}
5. 测试用例
java
@SpringBootTest
class InventoryServiceTest {
@Autowired
private InventoryService inventoryService;
@Autowired
private RedisInventoryService redisInventoryService;
@Test
void testInventoryFlow() {
String productId = "test_product_001";
String userId = "user_001";
String orderId = "order_001";
int initialStock = 100;
int purchaseQuantity = 2;
// 初始化库存
redisInventoryService.initProductStock(productId, initialStock);
// 测试预扣库存
boolean reserveSuccess = inventoryService.reserveStock(productId, purchaseQuantity, userId, orderId);
assertTrue(reserveSuccess);
// 验证库存减少
int availableStock = inventoryService.getAvailableStock(productId);
assertEquals(initialStock - purchaseQuantity, availableStock);
// 测试确认库存
boolean confirmSuccess = inventoryService.confirmStock(orderId);
assertTrue(confirmSuccess);
// 测试重复确认(应该失败)
boolean duplicateConfirm = inventoryService.confirmStock(orderId);
assertFalse(duplicateConfirm);
}
@Test
void testConcurrentInventory() throws InterruptedException {
String productId = "test_product_002";
int initialStock = 10;
int threadCount = 20;
redisInventoryService.initProductStock(productId, initialStock);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
boolean success = inventoryService.reserveStock(
productId, 1, "user_" + Thread.currentThread().getId(),
"order_" + Thread.currentThread().getId()
);
if (success) {
successCount.incrementAndGet();
}
} finally {
latch.countDown();
}
}).start();
}
latch.await(10, TimeUnit.SECONDS);
// 只有10个应该成功
assertEquals(initialStock, successCount.get());
int finalStock = inventoryService.getAvailableStock(productId);
assertEquals(0, finalStock);
}
}
核心特性说明
原子性操作:使用 Redis Lua 脚本确保库存操作的原子性
分布式锁:使用 Redisson 防止并发问题
预扣超时:自动释放超时未支付的库存
补偿机制:提供手动释放超时库存的接口
监控支持:可以查看库存状态和预扣情况
异常处理:完善的错误处理和日志记录


















暂无评论内容