在电商系统中,如何确保库存扣减的原子性

在电商系统中,确保库存扣减的原子性是防止超卖的核心问题。以下是基于不同技术方案的加锁实现方案及对比分析:


一、Redis 原子操作方案

1. Redis 事务 + WATCH 命令
  • 原理 :通过 WATCH监控库存键,若键被修改则事务回滚。

  • 代码示例

    复制代码
    // 伪代码示例
    String stockKey = "stock:product_1001";
    redisTemplate.watch(stockKey);
    int currentStock = Integer.parseInt(redisTemplate.opsForValue().get(stockKey));
    if (currentStock >= quantity) {
        redisTemplate.multi();
        redisTemplate.opsForValue().decrement(stockKey, quantity);
        redisTemplate.exec();
    }
  • 优点:简单易用,无需引入额外依赖。

  • 缺点WATCH机制在高并发下可能频繁触发重试,性能受限。


2. Lua 脚本原子性操作
  • 原理:将库存查询和扣减逻辑封装为 Lua 脚本,在 Redis 单线程中执行。

  • 脚本示例

    复制代码
    -- Lua 脚本(扣减库存)
    local key = KEYS[1]
    local quantity = tonumber(ARGV[1])
    local current = tonumber(redis.call('GET', key))
    if current >= quantity then
        redis.call('DECRBY', key, quantity)
        return current - quantity
    else
        return -1
    end
  • Java 调用

    复制代码
    DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
    Long result = redisTemplate.execute(script, Collections.singletonList(stockKey), quantity);
  • 优点:完全原子性,性能高(单次网络请求)。

  • 缺点:脚本复杂度高时需优化编译和网络开销。


二、分布式锁方案

1. Redisson 分布式锁
  • 原理:通过 Redisson 客户端实现可重入锁,自动续期防止死锁。

  • 代码示例

    复制代码
    RLock lock = redissonClient.getLock("lock:product_1001");
    try {
        if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
            // 执行库存扣减
            int currentStock = inventoryMapper.selectStock(productId);
            if (currentStock >= quantity) {
                inventoryMapper.updateStock(productId, quantity);
            }
        }
    } finally {
        lock.unlock();
    }
  • 优点:支持自动续期、可重入锁,适合复杂业务逻辑。

  • 缺点:依赖 Redis 稳定性,网络延迟可能影响锁释放。


2. Redis + SETNX 自定义锁
  • 原理 :通过 SETNX命令实现分布式锁,结合过期时间防死锁。

  • 代码示例

    复制代码
    String lockKey = "lock:product_1001";
    String requestId = UUID.randomUUID().toString();
    Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
    if (success != null && success) {
        try {
            // 执行库存扣减
        } finally {
            // 仅持有锁的客户端可删除
            if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
                redisTemplate.delete(lockKey);
            }
        }
    }
  • 优点:轻量级,灵活性高。

  • 缺点:需手动处理锁续期和释放逻辑。


三、数据库锁方案

1. 悲观锁(SELECT ... FOR UPDATE)
  • 原理:通过数据库行锁保证串行化操作。

  • SQL 示例

    复制代码
    BEGIN;
    SELECT stock FROM products WHERE id = 1001 FOR UPDATE;
    UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock > 0;
    COMMIT;
  • 优点:强一致性,直接保障数据正确性。

  • 缺点:高并发下数据库性能瓶颈明显。


2. 乐观锁(版本号机制)
  • 原理:通过版本号控制并发更新。

  • SQL 示例

    复制代码
    UPDATE products 
    SET stock = stock - 1, version = version + 1 
    WHERE id = 1001 AND version = current_version;
  • 优点:减少锁竞争,适合读多写少场景。

  • 缺点:需处理更新失败的重试逻辑。


四、混合方案(Redis + 数据库)

1. 预扣库存 + 异步持久化
  • 流程

    1. Redis 预扣:通过 Lua 脚本快速扣减 Redis 库存。

    2. 异步落库:通过 MQ 异步更新数据库,若失败则回滚 Redis。

  • 优点:高吞吐量,数据库压力小。

  • 缺点:需实现最终一致性逻辑。


五、方案对比与选型建议

方案 适用场景 性能 一致性 实现复杂度
Redis Lua 脚本 高并发、低延迟扣减 ★★★★★
Redisson 分布式锁 需业务逻辑串行化的复杂场景 ★★★★☆
数据库悲观锁 数据强一致性要求的核心交易 ★★☆☆☆
数据库乐观锁 读多写少、可接受少量重试的场景 ★★★☆☆ 最终一致

六、最佳实践建议

  1. 核心库存扣减 :优先使用 Redis Lua 脚本,兼顾原子性与性能。

  2. 复杂业务逻辑 :结合 Redisson 分布式锁,确保操作串行化。

  3. 最终一致性 :采用 预扣库存 + MQ 异步落库,提升系统吞吐量。

  4. 降级策略:Redis 故障时切换至数据库乐观锁,保障基础功能可用。

通过合理选择方案,可有效解决库存超卖问题,同时平衡性能与一致性需求。

相关推荐
Coder_Boy_13 小时前
基于SpringAI的在线考试系统-相关技术栈(分布式场景下事件机制)
java·spring boot·分布式·ddd
程序员泠零澪回家种桔子16 小时前
分布式事务核心解析与实战方案
分布式
凯子坚持 c16 小时前
CANN 生态中的分布式训练利器:深入 `collective-ops` 项目实现高效多卡协同
分布式
惊讶的猫17 小时前
rabbitmq实践小案例
分布式·rabbitmq
禁默18 小时前
打破集群通信“内存墙”:手把手教你用 CANN SHMEM 重构 AIGC 分布式算子
分布式·重构·aigc
惊讶的猫20 小时前
rabbitmq初步介绍
分布式·rabbitmq
小镇敲码人20 小时前
华为CANN框架中HCCL仓库的全面解析:分布式通信的引擎
分布式·华为
User_芊芊君子21 小时前
【分布式训练】CANN SHMEM跨设备内存通信库:构建高效多机多卡训练的关键组件
分布式·深度学习·神经网络·wpf
酷酷的崽79821 小时前
CANN 开源生态解析(四):`cann-dist-train` —— 构建高效可扩展的分布式训练引擎
分布式·开源
惊讶的猫1 天前
AMQP 与 RabbitMQ 四大模型
分布式·rabbitmq