预扣库存模式问题与解决方案分析

预扣库存模式存在的问题

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。预扣时,执行 
DECR
 命令扣减可用库存,同时在一个独立的集合(Set)或哈希(Hash)中记录用户预扣的记录。超时或付款后,再执行 
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 防止并发问题

预扣超时:自动释放超时未支付的库存

补偿机制:提供手动释放超时库存的接口

监控支持:可以查看库存状态和预扣情况

异常处理:完善的错误处理和日志记录

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容