概述

视频链接: [IT老齐063] 大型电商整点秒杀业务场景下,商品库存如何预防超卖现象产生
视频地址: https://www.bilibili.com/video/BV1Qv411u7K7

什么是库存超卖?

库存超卖是指在高并发场景下,由于多个用户同时购买同一件商品,导致实际销售数量超过库存数量的现象。在秒杀场景中,这个问题尤为突出。

超卖产生的根本原因

  1. 并发访问:大量用户同时访问商品库存
  2. 数据竞争:读取-判断-扣减库存的非原子操作
  3. 网络延迟:请求处理时间差异造成的时序问题

传统方案及其局限性

1. 数据库悲观锁方案

-- 伪代码示例
SELECT * FROM goods_stock WHERE id = ? FOR UPDATE;
-- 检查库存是否充足
-- 扣减库存

缺点

  • 性能较差,串行化执行
  • 容易造成死锁
  • 数据库压力大

2. 数据库乐观锁方案

-- 伪代码示例
UPDATE goods_stock SET stock = stock - 1 WHERE id = ? AND stock > 0;

缺点

  • 高并发下失败率高
  • 用户体验差
  • 需要重试机制

分布式锁解决方案

Redis分布式锁实现

public boolean tryDeductStock(String productId, int quantity) {
    String lockKey = "stock_lock:" + productId;
    String lockValue = UUID.randomUUID().toString();
    
    try {
        // 获取分布式锁
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
        
        if (Boolean.TRUE.equals(acquired)) {
            // 检查库存
            Integer currentStock = getCurrentStock(productId);
            if (currentStock >= quantity) {
                // 扣减库存
                return deductStock(productId, quantity);
            }
        }
        return false;
    } finally {
        // 释放锁
        releaseLock(lockKey, lockValue);
    }
}

Redission分布式锁

RLock lock = redissonClient.getLock("stock_lock:" + productId);
try {
    if (lock.tryLock(10, TimeUnit.SECONDS)) {
        // 业务逻辑:检查库存、扣减库存
        return processOrder(productId, quantity);
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redis原子操作方案

Lua脚本实现原子性扣减

-- Redis Lua脚本
local key = KEYS[1]
local quantity = tonumber(ARGV[1])

local current_stock = redis.call('GET', key)
if current_stock == false then
    return -1  -- 商品不存在
end

current_stock = tonumber(current_stock)
if current_stock < quantity then
    return 0   -- 库存不足
end

local new_stock = current_stock - quantity
redis.call('SET', key, new_stock)
return new_stock

Java调用示例

public boolean deductStockWithLua(String productId, int quantity) {
    String luaScript = 
        "local current_stock = redis.call('GET', KEYS[1]) " +
        "if current_stock == false then return -1 end " +
        "current_stock = tonumber(current_stock) " +
        "if current_stock < tonumber(ARGV[1]) then return 0 end " +
        "local new_stock = current_stock - tonumber(ARGV[1]) " +
        "redis.call('SET', KEYS[1], new_stock) " +
        "return new_stock";
    
    Object result = redisTemplate.execute(
        new DefaultRedisScript<>(luaScript, Long.class),
        Collections.singletonList("stock:" + productId),
        String.valueOf(quantity)
    );
    
    return Long.valueOf(0).compareTo((Long) result) < 0;
}

预扣库存方案

两阶段提交思想

  1. 预扣阶段:预先冻结库存
  2. 确认阶段:确认支付后真正扣减
public class StockService {
    
    public boolean preDeductStock(String productId, String orderId, int quantity) {
        // 检查可用库存
        String availableKey = "stock_available:" + productId;
        String frozenKey = "stock_frozen:" + productId;
        
        // 使用Lua脚本保证原子性
        String luaScript = 
            "local available = redis.call('GET', KEYS[1]) " +
            "if available == false then available = 0 end " +
            "if tonumber(available) < tonumber(ARGV[1]) then return 0 end " +
            "redis.call('DECRBY', KEYS[1], ARGV[1]) " +
            "redis.call('INCRBY', KEYS[2], ARGV[1]) " +
            "redis.call('SETEX', ARGV[2], 300, ARGV[1]) " +  -- 订单冻结信息
            "return 1";
        
        Object result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Arrays.asList(availableKey, frozenKey),
            String.valueOf(quantity), "order_frozen:" + orderId
        );
        
        return Long.valueOf(1).equals(result);
    }
    
    public void confirmOrder(String productId, String orderId) {
        // 确认订单,正式扣减库存
        String frozenKey = "stock_frozen:" + productId;
        String confirmedKey = "stock_confirmed:" + productId;
        
        redisTemplate.opsForValue().decrement(frozenKey);
        redisTemplate.opsForValue().increment(confirmedKey);
        
        // 删除订单冻结信息
        redisTemplate.delete("order_frozen:" + orderId);
    }
    
    public void cancelOrder(String productId, String orderId) {
        // 取消订单,释放冻结库存
        String availableKey = "stock_available:" + productId;
        String frozenKey = "stock_frozen:" + productId;
        
        redisTemplate.opsForValue().increment(availableKey);
        redisTemplate.opsForValue().decrement(frozenKey);
        
        // 删除订单冻结信息
        redisTemplate.delete("order_frozen:" + orderId);
    }
}

消息队列异步处理方案

基于RabbitMQ的异步库存处理

@Component
public class AsyncStockService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void asyncDeductStock(String productId, int quantity) {
        StockMessage message = new StockMessage(productId, quantity, System.currentTimeMillis());
        rabbitTemplate.convertAndSend("stock.deduct.queue", message);
    }
    
    @RabbitListener(queues = "stock.deduct.queue")
    public void handleStockDeduction(StockMessage message) {
        // 使用Redis单线程特性保证顺序执行
        String luaScript = 
            "local current_stock = redis.call('GET', KEYS[1]) " +
            "if current_stock == false then return -1 end " +
            "current_stock = tonumber(current_stock) " +
            "if current_stock < tonumber(ARGV[1]) then return 0 end " +
            "local new_stock = current_stock - tonumber(ARGV[1]) " +
            "redis.call('SET', KEYS[1], new_stock) " +
            "return new_stock";
        
        Object result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList("stock:" + message.getProductId()),
            String.valueOf(message.getQuantity())
        );
        
        if (Long.valueOf(-1).equals(result)) {
            // 库存扣减失败,发送失败消息
            rabbitTemplate.convertAndSend("stock.fail.queue", message);
        }
    }
}

数据库层面优化

行级锁优化

-- 使用行级锁,而不是表级锁
UPDATE goods_stock 
SET stock = stock - 1, version = version + 1 
WHERE id = ? AND stock > 0 AND version = ?

分库分表策略

// 根据商品ID进行分库分表
public String getStockTable(int productId) {
    int dbIndex = productId % dbCount;
    int tableIndex = productId % tableCount;
    return "goods_stock_" + dbIndex + "_" + tableIndex;
}

综合解决方案

秒杀系统架构设计

用户请求
    ↓
限流层 (令牌桶/漏桶算法)
    ↓
缓存层 (Redis预热库存)
    ↓
分布式锁 (Redisson)
    ↓
库存扣减 (Lua脚本原子操作)
    ↓
异步处理 (消息队列)
    ↓
数据库持久化

完整实现示例

@Service
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private OrderService orderService;
    
    public SeckillResult seckill(String productId, String userId) {
        String lockKey = "seckill_lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                // 1. 检查是否还有库存
                String stockKey = "seckill_stock:" + productId;
                Integer currentStock = (Integer) redisTemplate.opsForValue().get(stockKey);
                
                if (currentStock != null && currentStock > 0) {
                    // 2. 扣减Redis库存
                    redisTemplate.opsForValue().decrement(stockKey);
                    
                    // 3. 创建订单
                    Order order = orderService.createOrder(productId, userId);
                    
                    // 4. 异步扣减数据库库存
                    asyncDeductDatabaseStock(productId, 1);
                    
                    return SeckillResult.success(order.getId());
                } else {
                    return SeckillResult.failure("库存不足");
                }
            } else {
                return SeckillResult.failure("系统繁忙,请稍后再试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return SeckillResult.failure("系统异常");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    private void asyncDeductDatabaseStock(String productId, int quantity) {
        // 异步扣减数据库库存,保证最终一致性
        CompletableFuture.runAsync(() -> {
            // 执行数据库库存扣减操作
        });
    }
}

性能优化建议

1. 缓存预热

  • 秒杀开始前预先加载库存到Redis
  • 预先分配库存到多个key,减少热点问题

2. 限流控制

  • 使用令牌桶算法控制请求速率
  • 对同一用户进行请求频率限制

3. 降级策略

  • 库存不足时快速失败
  • 熔断机制防止系统雪崩

总结

库存超卖问题是电商系统中的经典难题,需要从多个层面综合考虑:

  1. 缓存层面:使用Redis实现高性能库存管理
  2. 锁机制:合理使用分布式锁保证数据一致性
  3. 异步处理:解耦核心业务逻辑
  4. 限流降级:保证系统稳定性
  5. 监控告警:及时发现和处理异常情况

通过以上方案的组合使用,可以有效解决秒杀场景下的库存超卖问题。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐