📖 开场:超市的库存管理
想象你在超市买东西 🛒:
没有库存管理(混乱):
顾客A:我要买可乐 🥤
店员:好的!
↓
顾客B:我也要买可乐 🥤
店员:好的!
↓
...
顾客Z:我也要买可乐 🥤
店员:好的!
↓
去仓库取货:
只有10瓶可乐 💀
但卖了100瓶 💀💀
结果:
- 超卖了90瓶 ❌
- 赔钱 ❌
- 信誉受损 ❌
有库存管理(有序):
顾客A:我要买可乐 🥤
店员:库存检查... 有货 ✅
↓
扣减库存:10 → 9
↓
顾客B:我要买可乐 🥤
店员:库存检查... 有货 ✅
↓
扣减库存:9 → 8
↓
...
顾客K(第11个):我要买可乐 🥤
店员:库存检查... 没货了 ❌
↓
抱歉,已售罄!
结果:
- 不超卖 ✅
- 精准控制 ✅
这就是库存系统:防止超卖的利器!
🤔 核心挑战
挑战1:超卖问题 💀
问题:
库存:10个
↓
用户A:下单1个(库存10 → 9)
用户B:下单1个(库存10 → 9)并发!💀
↓
实际卖了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分钟未支付 │
│ - 自动释放库存 │
└────────────────────────────────────┘
🎉 恭喜你!
你已经完全掌握了电商库存系统的设计!🎊
核心要点:
- 三种库存:可用、锁定、已售
- 乐观锁:version字段防止并发
- Redis扣减:Lua脚本原子操作,高性能
- 分布式锁:Redisson实现
- 定时同步:Redis → MySQL
- 超时释放: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⭐!