数据库CAS实战:如何实现永不超卖的库存系统?
本文探讨数据库CAS技术在库存系统中的应用,通过单条UPDATE语句实现原子性库存操作。文章对比MySQL与PostgreSQL的实现差异,分析事务的必要性场景,并指出FOR UPDATE锁的使用条件。作者推荐:简单库存扣减使用纯CAS方案;秒杀场景采用CAS+重试;复杂业务需结合事务。MySQL通过ROW_COUNT()检查结果,PostgreSQL利用RETURNING子句获取更新数据。典型代
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,
16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
🤝商务合作:请搜索或扫码关注微信公众号 “心海云图”


数据库CAS实战:如何实现永不超卖的库存系统?
引言:库存更新的核心挑战
在高并发场景下,商品库存的准确更新是电商系统的关键挑战。多个用户同时下单可能导致超卖,后台补货也可能出现数据不一致。本文将深入探讨如何仅通过数据库层面实现安全、准确的库存更新。
一、CAS方案:最简洁的安全更新
1.1 基本原理
Compare And Set (CAS) 通过单条UPDATE语句的WHERE条件实现原子性操作:
-- 减库存核心语句
UPDATE products SET stock = stock - 1
WHERE id = ? AND stock >= 1;
-- 加库存
UPDATE products SET stock = stock + 1
WHERE id = ?;
1.2 安全性分析
- 原子性保证:UPDATE语句在数据库内部是原子操作
- 行级锁定:MySQL和PostgreSQL都会自动对更新的行加锁
- 条件过滤:WHERE条件防止超卖
1.3 实现示例
// Java实现 - 无事务版本
public boolean safeDecreaseStock(Long productId, int quantity) {
String sql = "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?";
int affectedRows = jdbcTemplate.update(sql, quantity, productId, quantity);
return affectedRows > 0;
}
二、MySQL与PostgreSQL的实现差异
2.1 MySQL实现方案
-- 基础更新
UPDATE products
SET stock = stock - 1,
version = version + 1,
updated_at = NOW()
WHERE id = 100
AND stock >= 1
AND status = 'active';
-- 检查是否成功
SELECT ROW_COUNT(); -- 返回受影响行数
关键技术点:
ROW_COUNT()获取更新结果- 默认
REPEATABLE READ隔离级别已足够 innodb_lock_wait_timeout控制锁等待
2.2 PostgreSQL实现方案
-- 使用RETURNING获取更新结果
UPDATE products
SET stock = stock - 1,
updated_at = CURRENT_TIMESTAMP
WHERE id = 100
AND stock >= 1
AND status = 'active'
RETURNING id, stock, name;
-- 使用CTE进行复杂操作
WITH updated AS (
UPDATE products
SET stock = stock - 1
WHERE id = 100 AND stock >= 1
RETURNING *
)
SELECT * FROM updated;
关键技术点:
RETURNING子句获取更新后数据- MVCC机制下的并发控制
FOR UPDATE SKIP LOCKED避免死锁
三、事务的必要性分析
3.1 单操作用例(无需事务)
当业务逻辑仅包含单个UPDATE操作时,不需要显式事务:
// 秒杀场景:只扣减库存
public boolean seckill(Long productId) {
String sql = "UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0";
return jdbcTemplate.update(sql, productId) > 0;
}
3.2 多操作用例(需要事务)
当涉及多个数据库操作时,必须使用事务保证原子性:
@Transactional
public OrderResult createOrder(OrderRequest request) {
// 1. 扣减库存
boolean stockSuccess = decreaseStock(request.getProductId(), request.getQuantity());
if (!stockSuccess) throw new BusinessException("库存不足");
// 2. 创建订单
Order order = createOrderRecord(request);
// 3. 扣减余额
deductBalance(request.getUserId(), order.getTotalAmount());
// 这些操作必须全部成功或全部失败
return OrderResult.success(order);
}
四、FOR UPDATE的使用场景
4.1 绝大多数情况不需要
-- 不需要:UPDATE本身已经加锁
UPDATE products SET stock = stock - 1 WHERE id = 100;
-- 不需要:即使高并发也不需要
UPDATE products SET stock = stock - 1 WHERE id = 100 AND stock >= 1;
4.2 需要使用的情况
当业务需要"先查询后更新"的复杂逻辑时:
START TRANSACTION;
-- 需要:先锁定行,然后执行复杂业务逻辑
SELECT * FROM products WHERE id = 100 FOR UPDATE;
-- 应用层复杂计算
-- if (库存 > 100) 打9折
-- if (用户是VIP) 额外扣减
UPDATE products SET stock = stock - 1 WHERE id = 100;
COMMIT;
五、完整技术方案对比
5.1 方案选择矩阵
| 场景 | 推荐方案 | 是否需要事务 | 是否需要FOR UPDATE |
|---|---|---|---|
| 简单库存扣减 | 纯CAS | 否 | 否 |
| 秒杀抢购 | 纯CAS + 重试 | 否 | 否 |
| 下单流程 | CAS + 事务 | 是 | 否 |
| 复杂库存计算 | 事务 + FOR UPDATE | 是 | 是 |
| 批量库存调整 | 批量UPDATE | 视情况 | 否 |
5.2 MySQL最佳实践
public class MySQLInventoryService {
// 方案1:简单扣减(无事务)
public boolean decreaseStockSimple(Long productId, int quantity) {
String sql = "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?";
return jdbcTemplate.update(sql, quantity, productId, quantity) > 0;
}
// 方案2:完整业务流程(有事务)
@Transactional(isolation = Isolation.READ_COMMITTED)
public OrderResult processOrder(OrderRequest request) {
// 使用CAS更新
int affected = jdbcTemplate.update(
"UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?",
request.getQuantity(), request.getProductId(), request.getQuantity()
);
if (affected == 0) {
// 提供详细错误信息
return getFailureReason(request.getProductId(), request.getQuantity());
}
// 其他业务操作...
return OrderResult.success();
}
}
5.3 PostgreSQL最佳实践
public class PostgresInventoryService {
// 使用RETURNING获取完整结果
public StockResult decreaseStock(Long productId, int quantity) {
String sql = """
UPDATE products
SET stock = stock - ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND stock >= ?
RETURNING id, stock, name
""";
try {
Map<String, Object> result = jdbcTemplate.queryForMap(sql, quantity, productId, quantity);
return StockResult.success(result);
} catch (EmptyResultDataAccessException e) {
return StockResult.failed("库存不足");
}
}
// 使用SKIP LOCKED避免死锁
public boolean decreaseStockSkipLocked(Long productId) {
String sql = """
WITH locked_row AS (
SELECT id FROM products
WHERE id = ? AND stock > 0
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE products p
SET stock = stock - 1
FROM locked_row lr
WHERE p.id = lr.id
""";
return jdbcTemplate.update(sql, productId) > 0;
}
}
六、性能优化建议
6.1 索引优化
-- 必须的索引
CREATE INDEX idx_products_id_stock ON products(id, stock);
CREATE INDEX idx_products_status ON products(status) WHERE status = 'active';
6.2 超时设置
-- MySQL
SET innodb_lock_wait_timeout = 3; -- 3秒锁等待超时
-- PostgreSQL
SET lock_timeout = '3s'; -- 3秒锁等待超时
SET statement_timeout = '10s'; -- 10秒语句超时
6.3 批量处理
-- 批量扣减
UPDATE products
SET stock = stock - 1
WHERE id IN (100, 101, 102)
AND stock >= 1;
-- 批量回滚(订单取消)
UPDATE products p
JOIN order_items oi ON p.id = oi.product_id
SET p.stock = p.stock + oi.quantity
WHERE oi.order_id = 1001;
七、监控与故障处理
7.1 关键指标监控
- 更新成功率
- 锁等待时间
- 死锁发生率
- 库存异常报警
7.2 失败处理策略
public class InventoryService {
private static final int MAX_RETRIES = 3;
public boolean decreaseStockWithRetry(Long productId, int quantity) {
for (int i = 0; i < MAX_RETRIES; i++) {
try {
return decreaseStock(productId, quantity);
} catch (DeadlockLoserDataAccessException e) {
// 死锁重试
waitExponentialBackoff(i);
continue;
} catch (DataAccessException e) {
if (e.getMessage().contains("lock wait timeout")) {
// 锁等待超时重试
continue;
}
throw e;
}
}
return false;
}
private void waitExponentialBackoff(int retryCount) {
try {
Thread.sleep((long) Math.pow(2, retryCount) * 100);
} catch (InterruptedException ignored) {}
}
}
结论
在库存更新场景中,纯CAS方案(单条UPDATE语句) 在大多数情况下已经足够安全,不需要额外的事务或FOR UPDATE。只有在涉及多个关联操作的复杂业务场景中,才需要引入事务管理。
关键要点总结:
- UPDATE语句本身提供原子性和行级锁
- WHERE条件防止超卖(
stock >= 1) - 检查受影响行数判断是否成功
- 根据业务复杂度决定是否需要事务
- 优先使用无事务的简单方案,需要时再引入复杂方案
更多推荐



所有评论(0)