秒杀业务中的库存扣减为什么不加分布式锁?

前言

说到秒杀业务的库存扣减,就还是得先确认我们的扣减基本方案。

秒杀场景的库存扣减方案

一般的做法是,先在Redis中做扣减,然后发送一个MQ消息,消费者在接到消息之后做数据库中库存的真正扣减及业务逻辑操作。

如何解决数据一致性问题:

Redis中库存成功扣减了,但是后续发送MQ消息失败,或者后面的消费过程中消息丢了或者失败了等情况。
就会导致Redis中的库存被扣减了,但是数据库库存没扣减,业务的实际操作没发生。这时候的结果就是Redis中发生了多扣,那么带来的业务问题就是少卖。

对账机制:
想要解决这类数据不一致的情况,就需要引入一些对账的机制,做一些准实时的核对,处理这种数据不一致的情况。

常见的对账方案有,用zset在redis中添加流水记录,然后定时拉一段时间内的所有记录,和数据库比对,发现不一致,则进行补偿处理。

一般在成熟的电商公司中,不管前面的方案做的多么完善,这个核对系统都是必不可少的。及时的核对出超卖、少卖等问题。

库存扣减为什么不用分布式锁

实现秒杀的库存扣减,最重要的是两个点:

  1. 抗更高的并发。
  2. 避免超卖
    尤其重要的就是这个防止超卖,这也是我们加锁的目的。
    用lua脚本的优势:
  3. lua脚本具有原子性:可以直接用利用他的原子性特性,在一个脚本中实现库存的检查、扣减等动作,避免超卖!
  4. 效率高:所有操作可以在一次脚本执行中完成,减少了网络传输时间和通信次数。
  5. 更加简洁,易于维护:所有操作可以在一次脚本执行中完成并且使得应用层的代码。

用分布式锁的缺点:
如果用了 tryLock,不

<think>我们正在讨论的是在秒杀库存扣减场景中,使用Redis的INCR命令扣减库存,但在拿到库存号后不使用SETNX加分布式锁的后果。 首先,明确一下流程: 1. 使用INCR命令扣减库存:这个步骤是原子的,可以保证库存扣减的原子性。例如,库存key为`stock:sku123`,初始值为100。每个请求通过`INCR stock:sku123 -1`(或者用`DECR`)来扣减库存,并返回扣减后的值。注意,INCR是原子操作,所以多个请求同时执行时,不会出现超卖(即不会扣减到负值?注意:INCR可以扣减到负数,所以我们一般不会直接用INCR扣减到负数,而是通过判断扣减后的值来避免负数)。 但是,通常我们不会直接使用INCR来扣减库存,而是使用一个Lua脚本来保证原子性,比如: ```lua local current = redis.call('GET', KEYS[1]) if current and tonumber(current) > 0 then return redis.call('DECR', KEYS[1]) else return -1 end ``` 或者更常见的做法是使用DECR命令,然后判断返回值。但是DECR命令本身不会阻止扣减到负数,所以我们需要在扣减前判断库存是否大于0,或者扣减后判断是否小于0(但扣减后小于0说明已经超卖了,所以应该在扣减前判断)。 实际上,在引用[5]中提到,Redis实现高并发扣减库存,会使用Lua脚本保证原子性,避免超卖。 2. 拿到库存号:这里可能是扣减成功后,返回给用户一个库存凭证(比如一个订单号),然后用户需要完成后续的订单创建、支付等流程。 3. 不使用SETNX加分布式锁:这里注意,问题描述是“拿到库存号后”不使用SETNX加分布式锁。也就是说,在库存扣减成功后,后续的订单处理流程没有加锁。 那么,这里讨论的后果并不是库存扣减本身的原子性问题(因为INCR是原子的,或者Lua脚本保证了原子性),而是后续订单处理流程中可能存在的问题。 具体后果: 1. **重复订单创建**:同一个用户可能在极短时间内多次点击,导致同一个库存被多次扣减(虽然库存扣减了一次,但是后续流程可能会创建多个订单)。但是,库存扣减是一次性的,所以这里的问题不是库存超卖,而是订单重复。 2. **库存号被多次使用**:如果库存扣减成功,返回一个库存号(比如一个序号),这个库存号代表用户获得了购买资格。但是,如果后续流程(如下单)没有加锁,同一个用户的多次请求(或者不同用户的恶意请求)可能使用同一个库存号多次下单,导致一个库存号对应多个订单,从而出现少卖(即实际库存被占用,但只应该卖出一件)。 3. **订单处理不一致**:在后续订单处理中,可能涉及多个步骤(创建订单、扣减积分、生成物流单等),如果没有分布式锁,多个线程或进程同时处理同一个库存号,可能导致数据不一致。例如,订单创建了两次,但是库存扣减了一次,导致超卖(这里超卖是指实际多卖了,因为库存只有一件,但创建了两个订单)。 注意:这里有一个关键点,库存扣减已经完成,所以库存不会超卖。但是,后续的订单处理如果出现多次,那么就会导致一个库存对应多个订单,这就是超卖(多卖了商品)。 为什么会出现这种情况? 因为在库存扣减成功后,我们通常需要将库存号与订单绑定。如果没有锁,可能出现: - 线程A和线程B同时拿到同一个库存号(比如库存号是100,表示第100件商品),然后都去创建订单。 - 两个线程都成功创建了订单,这样就出现了两个订单都对应同一个库存号100。 那么,实际仓库中只有100件商品,但卖出了101件(因为100件商品对应100个订单,但这里有两个订单都用了100号?实际上库存号100只代表一件商品,所以两个订单都用了100号,意味着两个订单都要求出库同一件商品,这就导致了超卖(实际仓库中这件商品只能给一个订单))。 因此,后果是:虽然库存扣减是原子性的(只扣减了一次),但是后续订单创建流程没有加锁,导致同一个库存号被多次使用,从而产生多个订单,造成超卖。 如何避免? 在后续订单创建流程中,对库存号加锁(使用SETNX),确保同一个库存号在同一时间只能被一个请求使用,从而保证一个库存号只创建一个订单。 另外,在引用[1]中提到了分布式锁的实现,可以使用SETNX(现在推荐使用SET命令的NX选项)来获取锁,并设置过期时间,避免死锁。 所以,总结后果: - 同一个库存号被多次使用,导致超卖(实际仓库中一件商品被多个订单占用)。 - 重复下单,造成数据混乱。 引用[4]中也提到,需要保证整个链路的数据一致性。如果后续订单处理没有锁,那么即使库存扣减正确,也会因为订单重复导致实际超卖。 因此,在秒杀系统中,我们通常在库存扣减后的订单创建环节也需要加分布式锁,锁的粒度可以是库存号(或者用户ID+商品ID等),确保一个库存号只能被一个订单使用。 相关问题: 1. 在秒杀系统中,除了库存扣减需要原子性,订单创建环节为什么需要分布式锁? 2. 如何设计分布式锁来防止同一个库存号被多次使用? 3. 如果订单创建环节不使用分布式锁,还有哪些方法可以避免重复下单? 4. 库存扣减和订单创建两个步骤如何通过事务保证一致性? </think>### 秒杀库存扣减场景中 `INCR` 扣减不加 `SETNX` 锁的后果分析 在秒杀库存扣减场景中,使用 Redis 的 `INCR` 命令进行原子性库存扣减(如 `INCR stock:sku123 -1`)能有效避免超卖问题[^5]。但若在**拿到库存号后不使用 `SETNX` 加分布式锁**,会导致以下严重后果: --- #### **1. 订单重复创建与超卖风险** - **问题本质** `INCR` 保证了库存扣减的原子性,但后续创建订单、支付等业务操作**非原子**。若不加锁,多个线程可能同时操作同一个库存号: ```mermaid graph TD A[线程A:扣减库存成功] --> B[创建订单] C[线程B:扣减库存成功] --> D[创建订单] B & D --> E[同一库存号创建多个订单] ``` - **后果** 实际库存扣减一次(如库存号100),但多个线程基于库存号100创建多个订单,导致**仓库实际商品被重复售卖**(超卖)[^2][^4]。 --- #### **2. 状态不一致与数据错乱** - **库存与订单状态断裂** 库存扣减成功后,业务系统需完成订单创建、支付状态更新、物流单生成等操作。不加锁时: - 线程A创建订单但支付失败 - 线程B同时操作同一库存号,重复创建订单 - 结果:库存扣减,但支付状态混乱,需人工介入修复[^3] - **典型错误场景** ```python # 伪代码:不加锁的订单创建流程 stock_id = redis.incr("stock:sku123", -1) # 原子扣减库存 if stock_id >= 0: order = create_order(stock_id) # 无锁并发创建订单 → 重复订单风险 process_payment(order) # 支付状态冲突 ``` --- #### **3. 消息丢失与少卖风险** - **库存回滚失效** 若后续操作(如发送MQ消息)失败,需回滚库存不加锁时: - 线程A:订单创建失败,触发库存回滚(`INCR stock:sku123 +1`) - 线程B:同时操作同一库存号,导致回滚后重复扣减 - 结果:Redis库存正确,但**实际售卖量少于库存**(少卖)[^3][^5] --- #### **4. 分布式事务中断** - **最终一致性破坏** 库存扣减(Redis)与订单创建(DB)需跨系统事务保障。不加锁时: - 线程A:Redis扣减成功 → 创建DB订单(进行中) - 线程B:抢占同一库存号 → DB订单创建冲突 - 结果:Redis与DB数据不一致,需复杂补偿机制[^4] --- ### 解决方案:`INCR` + 分布式锁工作流 ```mermaid graph LR A[请求到达] --> B[INCR原子扣减库存] B --> C{库存号≥0?} C -- 是 --> D[SETNX lock:库存号 加锁] D --> E[创建订单+支付] E --> F[释放锁] C -- 否 --> G[返回库存不足] ``` #### **关键代码实现** ```lua -- Lua脚本:原子扣减库存+加锁 local stock_key = KEYS[1] local lock_key = "lock:" .. stock_key local lock_ttl = 10 -- 锁超时时间(秒) -- 原子扣减库存 local stock_id = redis.call('INCRBY', stock_key, -1) if stock_id < 0 then redis.call('INCRBY', stock_key, 1) -- 回滚库存 return {-1} -- 库存不足 end -- 尝试加锁(SETNX + EXPIRE) local lock_ok = redis.call('SET', lock_key, '1', 'NX', 'EX', lock_ttl) if not lock_ok then redis.call('INCRBY', stock_key, 1) -- 回滚库存 return {-2} -- 锁竞争失败 end return {stock_id} -- 返回库存号(已加锁) ``` --- #### **设计要点** 1. **锁粒度优化** 锁的 Key 精确到库存号(如 `lock:stock:1001`),避免全局锁性能瓶颈[^1]。 2. **锁超时机制** 设置锁过期时间(如10秒),防止死锁[^1][^4]。 3. **操作幂等性** 订单创建需幂等设计(如唯一ID),即使重试也不产生重复数据[^4]。 4. **库存回滚补偿** 监听MQ消息失败事件,触发库存回滚(需核对订单状态)[^5]。 --- ### 结论 在秒杀系统中,`INCR` 扣减库存不加 `SETNX` 锁会导致: 1. ✅ 库存扣减原子性保障(通过`INCR`) 2. ❌ 订单创建/支付等后续操作**非原子** 3. ❌ 同一库存号被多次使用 → **超卖/少卖** 4. ❌ 跨系统数据不一致 → **人工修复成本高** 必须通过分布式锁(或事务消息)保证 **“库存扣减-订单创建-支付”全链路原子性**,这是秒杀系统设计的核心原则之一[^4][^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

提前退休的java猿

兄弟们能否给口饭吃

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值