🧑 博主简介CSDN博客专家历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,16年工作经验,精通Java编程高并发设计分布式系统架构设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生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。只有在涉及多个关联操作的复杂业务场景中,才需要引入事务管理。

关键要点总结:

  1. UPDATE语句本身提供原子性和行级锁
  2. WHERE条件防止超卖(stock >= 1
  3. 检查受影响行数判断是否成功
  4. 根据业务复杂度决定是否需要事务
  5. 优先使用无事务的简单方案,需要时再引入复杂方案
Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐