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

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


一、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 故障时切换至数据库乐观锁,保障基础功能可用。

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

相关推荐
java_nnnn1 小时前
Redis 初识:从单体架构到分布式架构的演进
redis·分布式·架构
Sayai2 小时前
Kafka 2.8→3.9.1 跨大版本升级实战:ZooKeeper 同步升级、数据目录迁移与回滚预案全流程
大数据·运维·分布式·zookeeper·kafka
敲上瘾2 小时前
分布式系统——Redis哨兵:自动故障转移与选举原理
linux·数据库·redis·分布式·缓存·微服务
微三云 - 廖会灵 (私域系统开发)17 小时前
供应链系统实战:多渠道订单聚合与分布式库存一致性方案总结
开发语言·分布式·php
她说可以呀18 小时前
Docker 镜像的两种创建方式
java·分布式·docker
杭州领祺科技1 天前
分布式光伏『四可』接入改造:绿电直连场景下的二次系统通信与安全架构(27 号令 4 区落位 + 7 产品边界)
分布式·安全·安全架构·光伏·电力·绿电直连·二次系统
SparklingTheo1 天前
【Zookeeper】源码阅读
分布式·zookeeper·云原生
棣廷1 天前
初识Hadoop
大数据·hadoop·分布式
2603_954708311 天前
如何进一步优化微能网协调控制箱的架构优势
大数据·数据库·人工智能·分布式·架构