📦 设计一个电商库存系统:仓库管理员的智慧!

47 阅读9分钟

📖 开场:超市的库存管理

想象你在超市买东西 🛒:

没有库存管理(混乱)

顾客A:我要买可乐 🥤
店员:好的!
    ↓
顾客B:我也要买可乐 🥤
店员:好的!
    ↓
...
顾客Z:我也要买可乐 🥤
店员:好的!
    ↓
去仓库取货:
只有10瓶可乐 💀
但卖了100瓶 💀💀

结果:
- 超卖了90瓶 ❌
- 赔钱 ❌
- 信誉受损 ❌

有库存管理(有序)

顾客A:我要买可乐 🥤
店员:库存检查... 有货 ✅
    ↓
扣减库存:10 → 9
    ↓
顾客B:我要买可乐 🥤
店员:库存检查... 有货 ✅
    ↓
扣减库存:9 → 8
    ↓
...
顾客K(第11个):我要买可乐 🥤
店员:库存检查... 没货了 ❌
    ↓
抱歉,已售罄!

结果:
- 不超卖 ✅
- 精准控制 ✅

这就是库存系统:防止超卖的利器!


🤔 核心挑战

挑战1:超卖问题 💀

问题:
库存:10个
    ↓
用户A:下单1个(库存109)
用户B:下单1个(库存109)并发!💀
    ↓
实际卖了2个,库存变成8?还是9?

结果:
数据不一致 ❌
可能超卖 ❌

挑战2:高并发 🔥

双11秒杀:
库存:100个
    ↓
10万人同时下单
    ↓
QPS:10万 💀
    ↓
如何保证不超卖?
如何保证高性能?

挑战3:分布式场景 🌐

订单服务:3台服务器
    ↓
同时扣减库存
    ↓
如何保证一致性?

解决:
- 分布式锁 ✅
- 数据库锁 ✅
- Redis扣减 ✅

🎯 核心设计

设计1:库存模型 📊

数据库设计

-- ⭐ 商品库存表
CREATE TABLE t_product_stock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL COMMENT '商品ID',
    total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存',
    available_stock INT NOT NULL DEFAULT 0 COMMENT '可用库存(真实库存)',
    locked_stock INT NOT NULL DEFAULT 0 COMMENT '锁定库存(预扣库存)',
    sold_stock INT NOT NULL DEFAULT 0 COMMENT '已售库存',
    version INT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
    update_time DATETIME COMMENT '更新时间',
    
    UNIQUE KEY uk_product (product_id),
    INDEX idx_available (available_stock)
) COMMENT '商品库存表';

-- 库存关系:
-- total_stock = available_stock + locked_stock + sold_stock
-- 例如:总库存100 = 可用70 + 锁定20 + 已售10

为什么要分成三种库存?

1. 可用库存(available_stock):
   - 用户可以下单的库存
   - 下单时扣减

2. 锁定库存(locked_stock):
   - 用户下单后,支付前
   - 暂时锁定,防止其他人买

3. 已售库存(sold_stock):
   - 用户支付完成
   - 真正卖出去的

设计2:库存扣减(乐观锁)⭐⭐⭐

@Service
public class StockService {
    
    @Autowired
    private ProductStockMapper stockMapper;
    
    /**
     * ⭐ 预扣库存(下单时)
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean lockStock(Long productId, int quantity) {
        // 1. 查询库存
        ProductStock stock = stockMapper.selectByProductId(productId);
        
        if (stock == null) {
            throw new StockNotFoundException("库存不存在");
        }
        
        // 2. 检查库存是否充足
        if (stock.getAvailableStock() < quantity) {
            return false;  // 库存不足
        }
        
        // ⭐ 3. 预扣库存(乐观锁)
        int rows = stockMapper.lockStock(productId, quantity, stock.getVersion());
        
        if (rows == 0) {
            // 更新失败(版本号不匹配),说明有并发修改
            throw new ConcurrentUpdateException("库存扣减失败,请重试");
        }
        
        return true;
    }
    
    /**
     * ⭐ 扣减库存(支付完成时)
     */
    @Transactional(rollbackFor = Exception.class)
    public void deductStock(Long orderId) {
        // 查询订单商品
        List<OrderItem> orderItems = orderItemMapper.selectByOrderId(orderId);
        
        for (OrderItem item : orderItems) {
            // ⭐ 扣减库存:locked_stock → sold_stock
            stockMapper.deductStock(item.getProductId(), item.getQuantity());
        }
    }
    
    /**
     * ⭐ 释放库存(订单取消时)
     */
    @Transactional(rollbackFor = Exception.class)
    public void releaseStock(Long orderId) {
        List<OrderItem> orderItems = orderItemMapper.selectByOrderId(orderId);
        
        for (OrderItem item : orderItems) {
            // ⭐ 释放库存:locked_stock → available_stock
            stockMapper.releaseStock(item.getProductId(), item.getQuantity());
        }
    }
}

Mapper实现

@Mapper
public interface ProductStockMapper {
    
    /**
     * ⭐ 预扣库存(乐观锁)
     */
    @Update("UPDATE t_product_stock " +
            "SET available_stock = available_stock - #{quantity}, " +
            "    locked_stock = locked_stock + #{quantity}, " +
            "    version = version + 1, " +
            "    update_time = NOW() " +
            "WHERE product_id = #{productId} " +
            "  AND version = #{version} " +
            "  AND available_stock >= #{quantity}")
    int lockStock(@Param("productId") Long productId, 
                  @Param("quantity") int quantity, 
                  @Param("version") int version);
    
    /**
     * ⭐ 扣减库存(支付完成)
     */
    @Update("UPDATE t_product_stock " +
            "SET locked_stock = locked_stock - #{quantity}, " +
            "    sold_stock = sold_stock + #{quantity}, " +
            "    version = version + 1, " +
            "    update_time = NOW() " +
            "WHERE product_id = #{productId} " +
            "  AND locked_stock >= #{quantity}")
    int deductStock(@Param("productId") Long productId, 
                    @Param("quantity") int quantity);
    
    /**
     * ⭐ 释放库存(订单取消)
     */
    @Update("UPDATE t_product_stock " +
            "SET locked_stock = locked_stock - #{quantity}, " +
            "    available_stock = available_stock + #{quantity}, " +
            "    version = version + 1, " +
            "    update_time = NOW() " +
            "WHERE product_id = #{productId} " +
            "  AND locked_stock >= #{quantity}")
    int releaseStock(@Param("productId") Long productId, 
                     @Param("quantity") int quantity);
}

设计3:Redis库存扣减(高性能)⭐⭐⭐

@Service
public class RedisStockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String STOCK_KEY = "product:stock:";
    
    /**
     * ⭐ 初始化Redis库存
     */
    public void initStock(Long productId, int stock) {
        String key = STOCK_KEY + productId;
        redisTemplate.opsForValue().set(key, String.valueOf(stock));
    }
    
    /**
     * ⭐ 预扣库存(Redis + Lua)
     */
    public boolean lockStock(Long productId, int quantity) {
        String key = STOCK_KEY + productId;
        
        // Lua脚本(原子操作)
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local quantity = tonumber(ARGV[1])\n" +
            "local stock = tonumber(redis.call('get', key))\n" +
            "\n" +
            "if stock == nil then\n" +
            "    return -1  -- 库存不存在\n" +
            "end\n" +
            "\n" +
            "if stock < quantity then\n" +
            "    return 0  -- 库存不足\n" +
            "end\n" +
            "\n" +
            "redis.call('decrby', key, quantity)\n" +
            "return 1  -- 成功\n";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            String.valueOf(quantity)
        );
        
        return result != null && result == 1;
    }
    
    /**
     * ⭐ 释放库存(订单取消)
     */
    public void releaseStock(Long productId, int quantity) {
        String key = STOCK_KEY + productId;
        redisTemplate.opsForValue().increment(key, quantity);
    }
    
    /**
     * 查询库存
     */
    public int getStock(Long productId) {
        String key = STOCK_KEY + productId;
        String stock = redisTemplate.opsForValue().get(key);
        return stock != null ? Integer.parseInt(stock) : 0;
    }
}

设计4:分布式锁(悲观锁)🔒

@Service
public class StockServiceWithLock {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private ProductStockMapper stockMapper;
    
    /**
     * ⭐ 预扣库存(分布式锁)
     */
    public boolean lockStock(Long productId, int quantity) {
        // 获取分布式锁
        String lockKey = "stock:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // ⭐ 尝试获取锁(等待10秒,持有30秒)
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            
            if (!locked) {
                throw new LockException("获取锁失败");
            }
            
            // 查询库存
            ProductStock stock = stockMapper.selectByProductId(productId);
            
            if (stock.getAvailableStock() < quantity) {
                return false;  // 库存不足
            }
            
            // 扣减库存
            stockMapper.lockStock(productId, quantity, stock.getVersion());
            
            return true;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        } finally {
            // ⭐ 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

设计5:库存同步(Redis → MySQL)

@Service
public class StockSyncService {
    
    @Autowired
    private RedisStockService redisStockService;
    
    @Autowired
    private ProductStockMapper stockMapper;
    
    /**
     * ⭐ 定时同步库存(Redis → MySQL)
     */
    @Scheduled(fixedRate = 60000)  // 每分钟同步一次
    public void syncStock() {
        // 获取所有商品ID
        List<Long> productIds = stockMapper.selectAllProductIds();
        
        for (Long productId : productIds) {
            try {
                // 1. 获取Redis库存
                int redisStock = redisStockService.getStock(productId);
                
                // 2. 获取MySQL库存
                ProductStock stock = stockMapper.selectByProductId(productId);
                int mysqlStock = stock.getAvailableStock();
                
                // 3. 如果不一致,以MySQL为准
                if (redisStock != mysqlStock) {
                    log.warn("⭐ 库存不一致,商品ID:{},Redis:{},MySQL:{}", 
                            productId, redisStock, mysqlStock);
                    
                    // 更新Redis
                    redisStockService.initStock(productId, mysqlStock);
                }
            } catch (Exception e) {
                log.error("⭐ 同步库存失败,商品ID:{}", productId, e);
            }
        }
    }
}

设计6:超时释放库存

@Service
public class StockReleaseService {
    
    @Autowired
    private StockService stockService;
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * ⭐ 定时释放超时订单的库存
     */
    @Scheduled(fixedRate = 60000)  // 每分钟执行一次
    public void releaseTimeoutStock() {
        // 查询30分钟前创建的待支付订单
        Date thirtyMinutesAgo = new Date(System.currentTimeMillis() - 30 * 60 * 1000);
        List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(thirtyMinutesAgo);
        
        for (Order order : timeoutOrders) {
            try {
                // ⭐ 释放库存
                stockService.releaseStock(order.getId());
                
                // 更新订单状态为已取消
                order.setStatus(OrderStatus.CANCELLED);
                order.setCancelReason("超时未支付");
                orderMapper.updateById(order);
                
                log.info("⭐ 释放超时订单库存:{}", order.getId());
                
            } catch (Exception e) {
                log.error("⭐ 释放库存失败,订单ID:{}", order.getId(), e);
            }
        }
    }
}

🎓 面试题速答

Q1: 如何防止超卖?

A: 乐观锁 + version字段

UPDATE t_product_stock 
SET available_stock = available_stock - 1,
    locked_stock = locked_stock + 1,
    version = version + 1
WHERE product_id = 123
  AND version = 5
  AND available_stock >= 1

关键

  • WHERE条件检查version和库存
  • 并发时只有一个UPDATE成功

Q2: 数据库乐观锁 vs Redis扣减?

A: 性能对比

方案QPS优点缺点
数据库乐观锁1000强一致性性能低 ❌
Redis扣减10万 ⭐高性能 ✅需要同步

推荐

  • 秒杀场景:Redis扣减
  • 普通场景:数据库乐观锁

Q3: 预扣库存的作用?

A: 防止超卖

流程:
1. 下单:预扣库存(锁定)
   available_stock → locked_stock

2. 支付:扣减库存
   locked_stock → sold_stock

3. 取消:释放库存
   locked_stock → available_stock

好处

  • 下单时立即锁定,防止超卖 ✅
  • 支付前库存已扣,保证有货 ✅

Q4: 分布式锁如何使用?

A: Redisson实现

// 获取分布式锁
RLock lock = redissonClient.getLock("stock:lock:" + productId);

try {
    // 尝试获取锁
    boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
    
    if (locked) {
        // 扣减库存
        stockMapper.lockStock(productId, quantity);
    }
} finally {
    // 释放锁
    lock.unlock();
}

注意

  • 必须在finally释放锁
  • 检查是否持有锁再释放

Q5: Redis库存如何同步到MySQL?

A: 定时同步

@Scheduled(fixedRate = 60000)  // 每分钟
public void syncStock() {
    // 获取Redis库存
    int redisStock = redisTemplate.get(key);
    
    // 获取MySQL库存
    int mysqlStock = stockMapper.selectStock(productId);
    
    // 不一致,以MySQL为准
    if (redisStock != mysqlStock) {
        redisTemplate.set(key, mysqlStock);
    }
}

Q6: 超时订单如何处理?

A: 定时任务释放库存

@Scheduled(fixedRate = 60000)
public void releaseTimeoutStock() {
    // 查询30分钟前的待支付订单
    List<Order> orders = orderMapper.selectTimeout(30);
    
    for (Order order : orders) {
        // 释放库存
        stockService.releaseStock(order.getId());
        
        // 取消订单
        orderService.cancel(order.getId());
    }
}

🎬 总结

       电商库存系统核心

┌────────────────────────────────────┐
│ 1. 三种库存                        │
│    - 可用库存(available)         │
│    - 锁定库存(locked)            │
│    - 已售库存(sold)              │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 2. 乐观锁扣减 ⭐                    │
│    - version字段                   │
│    - WHERE检查库存和版本           │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 3. Redis扣减(高性能)⭐⭐           │
│    - Lua脚本原子操作               │
│    - QPS 10万+                     │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 4. 分布式锁                        │
│    - Redisson实现                  │
│    - 防止并发冲突                  │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 5. 库存同步                        │
│    - Redis → MySQL                 │
│    - 定时同步(1分钟)             │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 6. 超时释放                        │
│    - 30分钟未支付                  │
│    - 自动释放库存                  │
└────────────────────────────────────┘

🎉 恭喜你!

你已经完全掌握了电商库存系统的设计!🎊

核心要点

  1. 三种库存:可用、锁定、已售
  2. 乐观锁:version字段防止并发
  3. Redis扣减:Lua脚本原子操作,高性能
  4. 分布式锁:Redisson实现
  5. 定时同步:Redis → MySQL
  6. 超时释放:30分钟未支付释放库存

下次面试,这样回答

"电商库存系统将库存分为三种:可用库存、锁定库存和已售库存。用户下单时扣减可用库存并增加锁定库存,支付完成后扣减锁定库存并增加已售库存,订单取消时将锁定库存释放回可用库存。这样保证下单时库存已锁定,不会超卖。

防止超卖使用乐观锁实现。库存表增加version字段,更新时WHERE条件检查version和可用库存是否充足。并发更新时只有一个请求的version匹配成功,其他请求version不匹配更新失败,需要重试。这样保证了库存扣减的原子性。

秒杀场景使用Redis扣减库存提升性能。将库存预热到Redis,使用Lua脚本原子性扣减。Lua脚本检查库存充足后执行decrby操作,整个过程是原子的。Redis内存操作QPS可达10万以上,远超数据库的1000 QPS。扣减成功后异步更新MySQL库存。

分布式场景使用Redisson分布式锁。获取锁key为'stock:lock:商品ID',tryLock方法设置等待10秒、持有30秒。获取锁后查询库存并扣减,finally块释放锁。分布式锁保证同一时刻只有一个线程操作库存,避免并发冲突。

Redis库存通过定时任务同步到MySQL。每分钟对比Redis和MySQL的库存,如果不一致以MySQL为准更新Redis。这样保证Redis库存的准确性。超时订单的库存释放也通过定时任务实现,查询30分钟前创建的待支付订单,释放其锁定库存并取消订单。"

面试官:👍 "很好!你对库存系统的设计理解很深刻!"


本文完 🎬

上一篇: 220-设计一个监控告警系统.md
下一篇: 222-设计一个地理位置附近的人功能.md

作者注:写完这篇,我觉得库存管理太重要了!📦
如果这篇文章对你有帮助,请给我一个Star⭐!