# 对商品拆分库存行后,现在 1 单要买 2 个商品,这两个库存分别在 2 个库存行怎么处理? 这是一个非常硬核的场景设计题,通常出现在**高并发电商库存扣减**(如秒杀、热点商品)的面试中。 背景是:为了解决单行记录(Single Row)更新时的数据库锁竞争问题,我们采用了\*\*分段库存(Inventory Sharding)\*\*方案,把 `Total: 100` 拆成了 `Sub1: 20`, `Sub2: 20`... 等多个“子库存行”。 **问题核心:** 当用户下单购买的数量(N=2)超过了任意单个子库存行的剩余数量(比如每个行只剩1个),必须跨行扣减。这时候面临两大挑战: 1. **原子性(Atomicity):** 要么两行都扣成功,要么都不扣。不能出现扣了 A 行,B 行失败的情况。 2. **死锁(Deadlock):** 并发场景下,多线程同时锁多行,极易产生死锁。 以下是具体的解决方案,按推荐程度从高到低排列: ----- ## 方案一:Redis Lua 脚本原子扣减(业界主流推荐) 在极高并发下,库存通常不在 MySQL 直接扣,而是在 Redis 扣。 **核心思路:** Redis 的 Lua 脚本执行具有**原子性**(脚本执行期间不会插入其他命令)。我们可以把“寻找足够库存的 bucket”和“扣减”逻辑都写在 Lua 里。 **执行流程:** 1. **传入参数:** 商品ID,购买数量(2个)。 2. **Lua 内部逻辑:** * 遍历该商品所有的库存分桶 Key(例如 `item:1001:stock:0`, `item:1001:stock:1` ...)。 * **贪心策略:** 优先找一个剩余库存 \>= 2 的桶。如果有,直接扣减,返回成功。 * **跨桶策略:** 如果单桶不够,开始凑单。 * 记录需要扣减的桶和对应的数量(如:Bucket A 扣 1 个,Bucket B 扣 1 个)。 * 检查这些桶的总库存是否满足需求。 * 如果满足,**批量执行扣减**(在内存中操作,极快)。 * 如果总数不够,返回库存不足。 **优点:** \* **无死锁:** Redis 是单线程执行 Lua,不会出现线程A锁了Key1等Key2,线程B锁了Key2等Key1的情况。 * **高性能:** 纯内存操作。 ----- ## 方案二:数据库层面的多行事务 + 锁排序(兜底方案) 如果你必须在 MySQL 层面解决,或者作为 Redis 的强一致性兜底,必须遵循\*\*“锁排序”\*\*原则来避免死锁。 **场景复现(死锁陷阱):** * 线程 A 需要扣减 Bucket 1 和 Bucket 2。 * 线程 B 需要扣减 Bucket 2 和 Bucket 1。 * 如果 A 锁了 1,B 锁了 2,两者互相等待,造成死锁。 **解决方案(按照 ID 排序加锁):** 不管业务逻辑如何凑单,在执行 `UPDATE` 语句之前,必须将涉及到的库存行 ID 进行**从小到大排序**。 **执行步骤:** 1. **计算分配:** 在应用层(Java/Go代码中)先查出所有库存行,在内存中计算好需要扣减哪几行。 * 结果:`Target = { Row_ID_5: 扣1个, Row_ID_2: 扣1个 }` 2. **排序资源:** 将结果按 ID 排序 -\> `[{ID:2, count:1}, {ID:5, count:1}]`。 3. **开启事务:** ```sql START TRANSACTION; -- 1. 先锁 ID 小的 UPDATE stock_table SET num = num - 1 WHERE id = 2 AND num >= 1; -- 检查上一条 update 的 affected_rows,如果为0,回滚并报错(乐观锁失败) -- 2. 再锁 ID 大的 UPDATE stock_table SET num = num - 1 WHERE id = 5 AND num >= 1; -- 检查 affected_rows COMMIT; ``` **优点:** 强一致性,严格遵循 ACID。 **缺点:** 性能较差,锁竞争依然存在,只是避免了死锁。 ----- ## 方案三:应用层“尽量单行”策略(优化策略) 这是对上述方案的补充优化。在进行拆行扣减之前,算法应该尽量避免跨行。 **算法逻辑:** 1. **Try Best Fit:** 先遍历所有分桶,看有没有哪个桶 `stock >= 2`。如果有,锁定该单行扣减(性能最高)。 2. **Fallback to Multi-Row:** 如果所有桶都不够 2 个,再执行上述的“跨桶扣减”逻辑。 ----- ## 面试回答话术总结 你可以这样回答面试官: > "针对这个问题,核心难点在于**保证原子性**的同时**避免死锁**。 > > **第一,在分配逻辑上**,我会采用‘贪心算法’。应用层先判断是否存在单行库存充足的情况,优先进行单行扣减,因为单行事务性能最高。 > > **第二,如果必须跨行扣减(比如各剩1个)**,为了避免死锁,必须遵循\*\*‘资源有序分配’\*\*原则。 > > * 如果是在 **MySQL** 中做,我会开启一个事务,在代码里计算好要扣减的哪几行 ID,并**强制将这些 ID 从小到大排序**。然后依次执行 Update 语句。这样所有线程加锁顺序一致(都是先锁小ID,再锁大ID),彻底解决死锁问题。 > * 如果是在 **Redis** 中做(通常也是高并发的最佳实践),我会使用 **Lua 脚本**。在脚本内部遍历分桶 Key,凑够数量后一次性扣除。因为 Redis Lua 脚本的执行是原子的且单线程的,天然不存在死锁和部分失败的问题。"