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

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


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

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

相关推荐
嘉禾望岗50321 小时前
spark算子类型
大数据·分布式·spark
大厂技术总监下海21 小时前
来自美团生产环境的实战派:开源CAT监控,如何保障超大规模分布式系统可观测性?
分布式·开源
大厂技术总监下海1 天前
深入 Apache Dubbo 架构:解读一个开源高性能 RPC 框架的设计哲学与核心源码
分布式·微服务
前端不太难1 天前
不写 Socket,也能做远程任务?HarmonyOS 分布式任务同步实战
分布式·华为·harmonyos
回家路上绕了弯1 天前
Spring Retry框架实战指南:优雅处理分布式系统中的瞬时故障
分布式·后端
前端不太难1 天前
HarmonyOS 分布式开发第一课:设备间协同调试实战
分布式·华为·harmonyos
AutoMQ1 天前
当 Kafka 架构显露“疲态”:共享存储领域正迎来创新变革
分布式·架构·kafka
程序员阿鹏1 天前
RabbitMQ持久化到磁盘中有个节点断掉了怎么办?
java·开发语言·分布式·后端·spring·缓存·rabbitmq
独自破碎E1 天前
Kafka的索引设计有什么亮点?
数据库·分布式·kafka
武子康1 天前
Java-218 RocketMQ Java API 实战:同步/异步 Producer 与 Pull/Push Consumer
java·大数据·分布式·消息队列·rocketmq·java-rocketmq·mq