字节一面:库存超卖怎么办?
前言:一道让你与大厂失之交臂的题
“面字节电商岗,被问库存超卖,我只说了加锁,面试官直接摇头…”
“阿里二面,被追问‘超卖怎么兼顾并发和一致性’,我当场卡壳…”
这些令人窒息的场景,可能就是你面试的真实写照。库存超卖,这道看似简单的题目,早已不是纯粹的面试题,而是电商、秒杀、促销等高并发业务场景下的“生死线”。
去年一位朋友的公司,就因为618活动没防住超卖,多卖了2000单,最终导致了十几万的直接经济损失。可见,能否在技术层面优雅地解决超卖问题,已经成为衡量一个工程师技术深度和广度的重要标准。
今天,我们将通过一个7层方案模型,从最基础的数据库防御到大厂级别的高阶架构,层层递进,彻底讲透库存超卖的解决方案。掌握这套逻辑,下次面试再遇到它,你将游刃有余,让面试官对你刮目相看。
第一层:数据库无符号字段(基础防御)
这是最基础,却也最容易被忽略的一道防线。将数据库中存储库存的字段(如stock)类型设置为无符号整数(UNSIGNED INT)。

- 原理:当库存扣减到0后,如果再有扣减操作(
stock = stock - 1),数据库会直接抛出异常,阻止该SQL语句的执行,从而在数据库层面杜绝了库存变为负数的可能。
-- 库存字段设为无符号整型
CREATE TABLE goods (
id INT PRIMARY KEY,
stock INT UNSIGNED NOT NULL DEFAULT 0 -- 关键:UNSIGNED
);
-- 扣减库存时,若stock-1<0,直接报错
UPDATE goods SET stock = stock - 1 WHERE id = 1;面试加分点: 当面试官问:“这能防住超卖吗?”
标准回答:“它能作为最后一道防线,防止库存出现负数,保证数据的最终正确性。但它属于‘被动防御’,无法解决高并发下的业务逻辑问题。例如,100个请求同时查询到库存为1,它们都会通过业务代码的判断逻辑,最终只有一个请求能成功执行SQL,其余99个都会收到数据库异常。这会导致大量请求失败,用户体验极差,因此这只是一个基础保障,不能作为主要的并发控制方案。”
第二层:数据库悲观锁(串行控制)
悲观锁的核心思想是“先锁定,再操作”,它假设并发冲突一定会发生,所以每次操作数据前都会先加锁,阻止其他事务的修改。

- 实现:在SQL查询时使用
FOR UPDATE关键字。
-- 开启事务
BEGIN;
-- 查询并锁定行,其他事务将被阻塞,直到本事务提交
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 业务逻辑判断库存...
UPDATE products SET stock = stock - 1 WHERE id = 1;
-- 提交事务,释放锁
COMMIT;- 优点:实现简单,强一致性,能有效防止超卖。
- 缺点:性能极差。在高并发场景下,大量请求会因为等待锁而被阻塞,导致事务积压、响应超时,数据库吞吐量急剧下降。这在秒杀等场景下是绝对无法接受的。
面试加分点: 清晰地指出其适用场景:“这种方案适合并发量不大、且对数据一致性要求极高的场景,比如后台管理系统手动修改少量库存。对于C端高并发场景,悲观锁会成为性能瓶颈。”
第三层:乐观锁(CAS机制)
与悲观锁相反,乐观锁假设冲突很少发生,它不会预先加锁,而是在更新数据时检查在此期间数据是否被其他事务修改过。

- 实现:通常通过版本号(
version)或时间戳实现。
-- 1. 查询出商品信息,包含版本号
SELECT stock, version FROM products WHERE id = 1;
-- 2. 业务代码判断库存后,执行更新
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = #{current_version};如果UPDATE语句影响的行数为0,说明在你更新期间,version已经被其他线程修改,本次更新失败。
- 优点:不加锁,性能远高于悲观锁。
- 缺点:在高并发下,冲突会变多,导致大量更新失败和重试,增加了CPU开销。
面试加分点: 当面试官追问:“高并发下重试次数过多怎么办?”
标准回答:“可以引入重试机制,但必须设置合理的重试次数上限(例如3次)。如果超过上限仍然失败,应操作失败并向用户返回‘当前繁忙,请稍后再试’的提示,避免无限重试导致CPU空转和死循环。”
第四层:Redis队列(削峰填谷)
利用Redis单线程执行命令的特性,将请求序列化,变并行处理为串行处理。

- 实现:在秒杀活动开始前,根据库存数量(如1000件),在Redis的一个List中
LPUSH1000个标识(例如"1")。用户每下一个单,就执行一次LPOP操作。能LPOP成功的用户才能继续后续的下单流程,如果返回nil,则表示库存已空。 - 优点:
- 性能极高:Redis内存操作,速度飞快。
- 无需加锁:天然的原子性操作,将并发压力转移到Redis。
- 削峰填谷:平滑处理瞬时流量洪峰。
- 缺点:
- 功能受限:不适用于复杂的SKU场景或购物车批量购买。
- 数据一致性:Redis中的库存与数据库中的库存需要额外机制来保证最终一致性。
面试加分点: 指出该方案的局限性:“此方案适合单品秒杀,能扛住极高的瞬时并发。但需要考虑Redis故障或数据同步延迟可能导致的库存不一致问题,需要有后续的补偿和对账机制。”
第五层:Redis Lua脚本(原子操作)
虽然Redis命令是原子的,但“查询库存”+“扣减库存”是两个独立的操作,在并发下依然存在原子性问题。Lua脚本可以将多个命令打包,作为一个原子单元执行。

- 实现:编写一个Lua脚本,将“读取库存”和“判断并扣减”两个步骤合并。
-- check_and_deduct.lua
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0通过 EVAL 命令执行此脚本,Redis会保证脚本在执行期间不会被其他命令打断。
- 优点:保证了在Redis层面的原子性,代码逻辑清晰,性能优异。
- 缺点:如果业务逻辑复杂,Lua脚本会变得难以维护。同时,它依然面临Redis与数据库数据一致性的挑战。
面试加fen点: 当面试官问:“Lua脚本为什么能保证原子性?”
标准回答:“因为Redis在执行Lua脚本时,会将其作为一个整体执行,期间不会插入执行其他客户端的命令,这类似于一个‘单线程’的执行模型,从而天然地保证了原子性。但同时也要考虑到,如果脚本过于耗时,会阻塞整个Redis服务。此外,需要配置合理的Redis持久化策略(如AOF),防止服务器宕机导致库存数据丢失。”
第六层:“一锁二判三更新”(强一致性模式)
这是一个在分布式环境下保证强一致性的经典模式,常被大厂面试官考察。

- 流程:
- 一锁:通过分布式锁(如Redisson、ZooKeeper)锁定商品ID。
- 二判:获取锁成功后,再次从数据库或缓存中查询库存,判断是否充足。
- 三更新:如果库存充足,则执行扣减库存的操作,然后释放锁。
- 优点:逻辑严谨,能够确保在分布式环境下的数据强一致性,完全杜绝超卖。
- 缺点:性能瓶颈。由于引入了分布式锁,所有对同一商品的请求都变成了串行执行,吞吐量受限于锁的性能,并发能力较差。
面试加分点: 强调其适用场景:“这个方案以牺牲性能为代价,换取了最高级别的一致性。它非常适合那些绝对不能超卖的场景,比如限量版奢侈品、高价值商品的秒杀,这些场景下用户对‘慢’的容忍度高于对‘超卖’的容忍度。”
第七层:分布式锁 + 分段缓存(高性能模式)
这是对第六层方案的优化,也是字节、拼多多等公司处理大流量秒杀的常用方案,核心思想是“分而治之”。

- 实现:
- 库存分段:将总库存(如10000件)拆分成多个分片(Segment),存储在Redis中。例如,分成10片,每片1000件库存,存为
goods:stock:1到goods:stock:10。 - 分流请求:用户请求到来时,通过某种哈希算法(如
userId % 10)将其路由到某个库存分片上。 - 分片扣减:在分片上使用分布式锁或Lua脚本进行库存扣减。如果当前分片库存不足,可以尝试在下一个分片上扣减(需要设计好流转机制)。
- 优点:
- 并发倍增:将对单个库存热点的竞争,分散到多个分片上,并发能力理论上可提升N倍(N为分片数量)。
- 热点分散:有效避免了单一热点Key的问题。
- 缺点:
- 实现复杂:需要精心设计分片逻辑、跨分片扣减逻辑以及总库存的统计。
- 库存碎片:可能出现总库存充足,但每个分片剩余少量库存,导致用户请求失败的情况。
面试加分点: 清晰地指出方案的难点:“该方案最大的挑战在于如何处理‘库存碎片’和跨分片扣减的逻辑。例如,需要一个额外的计数器来大致跟踪总库存,或者在某个分片库存告急时,能平滑地将流量切换到其他分片,这对系统设计提出了更高的要求。”
总结:没有银弹,只有权衡
在面试和实际工作中,切忌背诵方案。面试官更想考察的是你对不同方案优劣的理解,以及在特定业务场景下进行技术选型的能力。
- 中小企业/低并发:乐观锁 或 Redis队列 是性价比极高的选择。
- 大型企业/高并发秒杀:Redis Lua脚本 + 分段缓存 是兼顾性能和一致性的主流方案。
- 金融级/强一致性要求:“一锁二判三更新” 模式是最后的保障。
下次再被问到库存超卖,请务必按照这个逻辑,从场景出发,分析利弊,给出你的权衡。这不仅能展现你的技术深度,更能体现你的架构思维。
最后,留个思考题: 如果使用Redis方案,如何解决Redis库存与数据库库存的最终一致性问题?欢迎在评论区留下你的见解!