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

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


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

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

相关推荐
陌路204 小时前
RPC分布式通信(5)--发布 RPC 服务、处理客户端调用请求
分布式·qt·rpc
LDG_AGI4 小时前
【机器学习】深度学习推荐系统(三十):X 推荐算法Phoenix rerank机制
人工智能·分布式·深度学习·算法·机器学习·推荐算法
秋雨雁南飞4 小时前
C# 分布式消息框架
分布式
ZePingPingZe5 小时前
TCC—最终一致性分布式事务方案及【案例】
分布式·微服务
alonewolf_995 小时前
RabbitMQ高级功能全面解析:队列选型、死信队列与消息分片实战指南
分布式·消息队列·rabbitmq·ruby
hellojackjiang20116 小时前
如何保障分布式IM聊天系统的消息有序性(即消息不乱)
分布式·架构·即时通讯·im开发
burning_maple7 小时前
设计数据密集型应用阅读笔记
分布式·后端·中间件
alonewolf_997 小时前
RabbitMQ快速上手与核心概念详解
分布式·消息队列·rabbitmq
陌路208 小时前
RPC分布式通信(4)--Zookeeper
分布式·zookeeper·rpc
陌路208 小时前
RPC分布式通信(6)---调用方自动封装请求数据、从 ZK 获取服务地址、建立 TCP 连接发送请求、接收并解析响应
分布式·tcp/ip·rpc