数据库存:电商企业最佳实践:性能优化怎样稳步实现降低超卖风险
库存只剩 1 件时,100 个请求同时到达,系统最终却创建了 3 笔成功订单,这并不一定是数据库“太慢”造成的。更常见的根因是:系统先查询库存,再执行扣减,查询与更新之间存在并发窗口。我的判断是,电商企业降低超卖风险,不能从“加 Redis、加消息队列”开始,而应先用数据库原子扣减守住正确性,再根据锁等待、热点 SKU、事务耗时和流量峰值,逐步完成性能优化。
本文不把库存系统写成中间件清单,而是沿着一条可以落地的路径展开:先识别超卖发生在哪里,再拆分一致性与性能目标,接着比较条件更新、行锁、乐观锁、缓存预扣和队列削峰的适用边界,最后给出压测、监控、对账和上线回滚方法。文中出现的性能数字,除特别注明外,均为情景模拟或建议基准,不代表任何企业的生产数据。
在库存系统中,“快”与“准”是两个不同目标。接口响应从 800 毫秒降到 80 毫秒,只能说明请求处理得更快;它不能证明同一件商品没有被多个请求重复占用。
典型的危险流程是:请求 A 查询库存为 1,请求 B 也查询库存为 1;随后 A 扣减一次,B 再扣减一次。如果数据库允许第二次更新继续执行,系统就可能出现负库存;即使数据库没有负数,两个订单也可能都进入“待支付”,直到仓库拣货时才发现无法履约。
真正需要守住的约束是:同一库存单元在同一时刻只能被有效占用一次,并且所有扣减、释放、回补都必须留下可追溯记录。
数据库索引、连接池、读写分离、缓存和消息队列,主要解决的是查询压力、写入压力、锁竞争和流量峰值。它们能够让系统承载更多请求,却不会天然保证库存不会被重复扣减。
例如,把库存数量放进缓存后,读库存的速度可能明显提升,但缓存失效、服务重启、消息重复消费、订单取消回补等问题仍然存在。如果缓存中的库存已经减掉,而数据库扣减失败,系统还必须知道如何恢复这次占用。
因此,我通常把库存架构拆成四层目标:
如果企业目前连第一层和第二层都没有建立,直接引入缓存和队列,往往只是把一个容易定位的数据库问题,改造成更难排查的分布式一致性问题。

对于大多数电商企业,我建议采用以下顺序,而不是一次性重构整个库存中心:
假设某 SKU 的可售库存为 1。用户甲和用户乙几乎同时点击购买,两个请求分别进入订单服务。库存服务采用以下流程:
如果步骤 1 和步骤 4 不在同一个有效的并发控制范围内,两个请求都可能通过第一步。即使最终数据库库存变成 0,系统也可能已经生成两笔订单。
有些团队会说:“数据库最终库存没有变成负数,所以没有超卖。”这是一种危险的误判。库存数字不为负,只代表某个字段看起来正常,不代表订单占用数、支付成功数和仓库可履约数保持一致。
我在设计库存排查表时,不会只检查 available_qty 是否小于 0,而会把超卖拆成四类,因为它们的修复方式完全不同。
| 表现 | 表面现象 | 实际风险 | 优先检查位置 |
|---|---|---|---|
| 负库存 | 可售库存小于 0 | 数据库约束或更新条件失效 | 扣减 SQL、事务隔离级别、并发更新 |
| 订单超占 | 库存显示正常,但成功订单多于可售数量 | 订单状态与库存状态不一致 | 订单创建、库存锁定、支付确认 |
| 回补重复 | 取消订单后库存增加两次 | 重试或重复消息造成虚增库存 | 取消接口、消费幂等、库存流水 |
| 缓存假有货 | 页面显示有货,但下单时数据库无货 | 用户体验和转化率下降 | 缓存刷新、预扣失败、缓存回源 |
在平峰期,一个热点 SKU 可能只有每秒几十次请求,数据库行锁很快释放,问题不容易暴露。到了大促开始的前几秒,大量请求集中到相同的 SKU 行,数据库更新开始排队,事务持锁时间变长,连接池逐渐占满。
这时,系统会出现一种很容易误判的现象:接口耗时上升,客户端开始重试;重试请求又进入库存服务,进一步放大热点行竞争。如果幂等机制不完整,第一次请求虽然已经扣减成功,但响应在网络中丢失,第二次请求可能再次扣减。
因此,压测不能只测“正常下单成功率”,还要模拟超时、重试、重复消息和取消回补。否则得到的只是理想环境下的吞吐量,不是大促环境下的库存安全性。

库存数据库至少要同时回答三个问题:现在还能卖多少、哪些订单已经占用、每一次数量变化由谁造成。如果只有一个库存总数,没有库存流水和业务状态,出了差异后很难判断是重复扣减、取消未回补、支付超时未释放,还是人工调整造成的。
更稳妥的库存模型,通常会区分可售库存、锁定库存、已售库存和在途库存。不同企业的字段命名可以不同,但必须明确每个数字的业务含义,以及状态迁移是否允许逆向回滚。
缓存非常适合读取商品详情、活动规则和库存展示值,但“展示库存”与“扣减库存”不是同一个动作。页面上显示还有 3 件,只能作为用户提示;最终能否获得这 3 件,仍必须由具备并发控制能力的扣减链路决定。
如果系统只在缓存中执行 decrement,却没有明确的持久化、失败重试和回补机制,就可能出现两类相反问题:缓存已经减到 0,但数据库仍有库存;或者数据库扣减失败,缓存却已经减少,导致可售库存被错误隐藏。
我的建议是:把缓存看作流量闸门,不要把它默认当成唯一库存账本。除非企业已经为缓存持久化、故障恢复、幂等和对账建立了完整机制,否则最终库存事实仍应有可追溯的持久化来源。
悲观锁能够让并发请求排队处理,逻辑直观,也适合库存极少、扣减必须同步确认的场景。但锁并不是免费的。一个事务如果持锁期间还调用营销服务、风控服务或支付服务,锁就会被这些外部依赖的耗时放大。
高峰期的锁竞争会带来连锁反应:库存行等待时间增加,数据库连接不释放,连接池被占满,其他不相关 SKU 的请求也开始排队,最终表现为整个订单系统变慢。
悲观锁更适合短事务、低热点和强同步确认场景。对于单个爆款 SKU 每秒产生大量竞争的情况,仅仅把锁加得更重,通常不能获得稳定吞吐。
乐观锁通常通过 version 字段控制并发,例如更新时要求 version 仍然等于读取时的版本。如果影响行数为 0,就说明版本已变化,需要重试或返回失败。
问题在于,很多实现只判断 version,却没有同时判断 available_qty 是否足够;或者重试逻辑没有次数上限,导致热点 SKU 在高峰期形成大量无效重试。重试越多,数据库压力越大,最终可能从“少量失败”演变成“全链路超时”。
库存扣减的条件应同时覆盖数量约束和并发版本,且必须为每个请求设置明确的失败策略。库存不足时,不能无限重试;数据库暂时冲突时,也不能把所有请求都重新打回数据库。
队列可以把瞬时写入峰值转变为平滑消费,但它会把同步问题转化为异步状态管理问题。用户点击购买后,如果库存只是进入队列,还没有真正锁定,系统就不能把订单直接展示为“购买成功”。
如果消息已经消费但响应没有返回,客户端再次提交,就会产生重复请求;如果消费者处理成功但确认消息失败,同一消息可能再次投递。因此,队列方案必须配套业务幂等、消费状态、重试上限和死信处理。
更准确的说法是:消息队列适合削峰和解耦,不是库存一致性的替代品。
平均响应时间很容易掩盖热点请求。假设 99% 的请求耗时 30 毫秒,1% 的请求耗时 8 秒,平均值可能仍然不算夸张,但这 1% 恰恰可能是库存最紧张、最容易触发重试的请求。
库存服务至少要同时观察 P95、P99、锁等待、死锁、事务回滚、连接池占用和库存操作失败原因。一个方案如果平均耗时下降,但 P99 和回滚率上升,未必是优化成功。

不同库存业务不能使用同一套默认方案。普通现货商品、预售商品、秒杀商品、组合商品和多仓库存,虽然都叫“库存”,但扣减时点和回补规则完全不同。
| 业务类型 | 扣减时点 | 主要一致性要求 | 优先方案 |
|---|---|---|---|
| 普通现货 | 下单锁定或支付后扣减 | 订单占用与可售数量一致 | 条件更新加短事务 |
| 秒杀商品 | 请求进入后快速预扣 | 高并发下只能成功有限次数 | 限流、缓存闸门、原子扣减、异步后续处理 |
| 预售商品 | 定金或支付阶段锁定 | 锁定周期和释放规则明确 | 状态机加定时释放任务 |
| 多仓库存 | 按仓或履约区域扣减 | 不能把不同仓库存混为一笔 | 仓库维度原子扣减加分配策略 |
| 组合商品 | 多个子 SKU 同时占用 | 避免只扣减部分组件 | 资源校验、短事务和失败回滚 |
如果企业没有先明确这些规则,数据库层面越复杂,业务风险越大。例如,组合商品需要同时扣减多个子 SKU,单行条件更新只能保证单个 SKU 的安全,不能自动保证整套商品的原子性。
并发量大不等于一定需要分库分表。真正需要优先观察的是:流量是否集中在少数 SKU,单个库存记录是否成为所有请求争用的热点。
如果 80% 的请求分布在几万件商品上,单行库存更新可能仍然足够稳定;如果 70% 的请求集中到一个爆款 SKU,即使数据库整体 CPU 只有 40%,这一行也可能成为系统瓶颈。
我会按 SKU 维度统计以下数据:
对许多中小型电商来说,第一版安全方案并不需要复杂的分布式组件。一条设计正确的条件更新,可以先解决最危险的“查到库存后被其他请求抢走”的问题。
UPDATE inventory SET available_qty = available_qty - :quantity, locked_qty = locked_qty + :quantity, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE sku_id = :sku_id AND warehouse_id = :warehouse_id AND available_qty >= :quantity AND status = 'ON_SALE';
执行后必须检查影响行数。影响行数为 1,说明本次扣减成功;影响行数为 0,可能是库存不足、仓库不可售、SKU 状态变化或并发版本不匹配。应用层不能把所有 0 行都简单返回“库存不足”,否则会掩盖数据库异常和状态错误。
还要注意,条件更新本身只解决“数量扣减”这一动作。它不自动解决库存流水写入、订单创建失败、支付超时释放和取消订单回补,这些动作仍然需要明确的事务和幂等设计。
| 判断维度 | 悲观锁 | 乐观锁 | 条件更新 |
|---|---|---|---|
| 实现直观性 | 较直观,先锁后改 | 需要版本与重试逻辑 | 实现相对简单 |
| 低并发适配性 | 较好 | 较好 | 很好 |
| 热点 SKU 表现 | 容易排队 | 冲突和重试增加 | 失败请求可快速返回 |
| 业务复杂度 | 事务边界要求高 | 重试与幂等要求高 | 需要补充流水和状态管理 |
| 适用建议 | 短事务、强同步确认 | 冲突率可控的更新 | 大多数单 SKU 扣减起点 |
我的默认判断不是“哪个锁更先进”,而是先看冲突率。如果库存请求分散且事务很短,悲观锁可以提供清晰的行为;如果冲突率较低,乐观锁能减少无意义的等待;如果业务主要是单 SKU 数量扣减,带条件的原子更新通常是更低成本的起点。

库存扣减经常按照 SKU、仓库、销售渠道、批次等字段定位记录。索引设计必须与实际更新条件匹配,而不是只给 sku_id 建一个单列索引就结束。
如果同一个 SKU 在多个仓库有库存,使用 sku_id 查询可能命中多行;应用层再挑选仓库,会增加锁定范围和业务不确定性。此时应根据实际库存模型设计联合索引,例如(sku_id,warehouse_id,status),并通过执行计划确认更新是否扫描了不必要的记录。
索引并非越多越好。每增加一个索引,库存更新都可能增加维护成本。库存表属于高频写表,索引过多会让写入放大,导致查询优化带来的收益被写入成本抵消。
一个健康的库存事务,应该只完成必须同步完成的动作,例如校验扣减条件、更新库存数量、写入库存流水和记录业务幂等状态。商品推荐、优惠券发放、短信通知、埋点和报表统计,不应在持有库存锁时执行。
特别需要避免在事务中调用外部 HTTP 服务。外部服务的网络抖动、连接超时和重试,会直接延长数据库锁的持有时间。库存锁等待一旦扩散,就会把一个外部依赖的问题变成数据库连接池问题。
如果订单创建必须跨多个服务,建议明确“库存锁定成功”和“订单后续处理成功”是两个状态,不要为了追求一次完成而把所有逻辑塞进一个超长事务。
常见的低效写法是:先执行条件更新,再重新查询库存,确认是否扣减成功。这个查询不仅增加一次数据库访问,还可能在高并发下让开发人员误读结果。
对于单次扣减,应用层应优先使用更新语句返回的影响行数作为本次操作结果。需要展示库存时,再根据页面需求读取展示值,而不是让展示查询参与核心扣减判断。
连接池不是越大越好。连接数超过数据库实际处理能力后,只会让更多请求在数据库内部排队。尤其在热点 SKU 场景下,增加连接数可能会让同一行的竞争更激烈。
我建议同时观察应用连接池等待时间、数据库活动连接数、锁等待时间和事务耗时。如果应用连接池已经满,但数据库 CPU 不高,可能是锁竞争;如果数据库 CPU 持续高位且没有明显锁等待,才更可能是 SQL 执行、索引或计算资源问题。

库存不足时,让请求在数据库中继续等待没有价值;库存已经被其他请求锁定时,长时间等待也不一定能提高成功率。对于秒杀和限量商品,系统应尽快返回明确结果,把失败请求挡在数据库之外。
可采用活动开始前预热、请求限流、用户维度防重复提交、商品维度并发闸门等方式,减少无效请求。需要强调的是,这些措施用于削峰和降低压力,最终扣减仍必须依赖可靠的原子操作。
当商品页面访问量很大,而真正进入扣减的请求相对较少时,缓存可以承担商品信息、活动规则和库存展示等读请求。对于预热库存,也可以通过原子计数操作快速过滤明显超出库存数量的请求。
但是,缓存预扣后必须明确库存状态。例如,可以把一次成功预扣标记为“待数据库确认”,并为它绑定订单号或库存操作号。数据库写入失败时,系统需要将预扣数量释放;如果释放消息重复到达,必须通过幂等记录避免重复回补。
缓存方案的关键不是 decrement 命令本身,而是“预扣成功、持久化成功、订单状态变化、取消释放”四个节点是否能够被同一条业务流水串起来。
如果业务允许“先申请,再排队确认”,队列可以明显降低数据库瞬时写入压力。用户提交申请后进入排队状态,消费者按照既定规则执行库存锁定和订单处理。
如果业务要求用户在点击后立即知道是否抢到库存,那么库存资格判断不能完全异步化。至少要在同步链路中完成一个可验证的占用动作,后续的订单详情、通知、积分和营销权益再进入异步流程。
| 组件 | 能够解决的问题 | 不能自动解决的问题 | 必须补充的机制 |
|---|---|---|---|
| 缓存 | 热点读、库存展示、流量过滤 | 最终账本一致性 | 失效处理、回源、持久化、对账 |
| 消息队列 | 削峰、异步化、服务解耦 | 重复消费和用户即时确认 | 幂等键、重试、死信、补偿 |
| 库存分片 | 降低单个热点行写入竞争 | 跨分片汇总和回补复杂度 | 分片策略、聚合规则、对账 |
| 读写分离 | 分担部分查询压力 | 写入一致性和扣减安全 | 读延迟容忍、主库扣减、路由规则 |
对于单个爆款 SKU,如果所有请求都更新同一行,可以把库存拆成多个逻辑桶,例如将可售数量分散到多个库存分片。请求先选择一个分片,再执行原子扣减,从而降低单行锁竞争。
但分片会带来新问题:某个分片已经没有库存,其他分片仍有库存;订单取消时要回补哪个分片;多个仓库或多个销售渠道之间怎样汇总。分片并不是简单地把一行拆成十行,而是把“库存一致性”从单点约束变成集合约束。
如果企业没有成熟的库存流水和对账能力,我不建议仅因为某个接口变慢就贸然进行库存分片。先确认瓶颈确实是单行热点,并通过压测证明分片能够改善 P99 和锁等待,再进入设计阶段。
在库存优化中,数据库监控能够告诉我们锁等待和 SQL 耗时,但不一定能直接解释业务异常发生在哪个 SKU、仓库或活动。此时可以引入某数据分析平台,将订单、库存流水、取消订单和支付结果按照业务主键关联起来。
例如,使用九数云这类数据分析工具,可以将库存流水表、订单表和售后回补表汇总成按 SKU、仓库、活动场次的分析看板。这里的价值不是让分析工具参与实时扣减,而是帮助技术和运营发现异常:哪些 SKU 的库存差异持续扩大,哪些活动的取消回补最集中,哪些仓库的锁定库存长期没有释放。
分析平台应当位于监控和决策层,而不是实时库存扣减的核心事务层。把报表查询直接放进库存事务,或者让分析接口直接读取并修改库存,都是边界混乱的表现。

数据库监控通常能看到 SQL 慢、锁等待、死锁和连接池使用率,却不一定能回答业务团队最关心的问题:某次活动到底是哪些 SKU 出现异常?差异发生在下单、支付还是取消回补?同一个用户重复提交是否造成了额外占用?
这些问题需要把多张业务表放到同一个分析口径中。至少要关联订单明细、库存变更流水、支付状态、取消订单和仓库履约结果。若各系统只保留自己的局部日志,技术团队往往只能凭时间线猜测问题。
下面以某电商企业一次限量活动为例。该案例中的数据为情景模拟,目的是展示排查方法,不代表九数云或任何企业的生产结果。
活动设置为:3 个仓库、12 个热点 SKU、总可售库存 18,000 件,活动持续 30 分钟。系统在活动开始后出现订单接口 P99 延迟上升,但数据库 CPU 只有 55%,初步看并不像整体算力不足。
技术团队将以下字段作为统一分析键:
通过九数云这类数据分析平台制作交叉分析后,团队发现:异常并不平均分布在 12 个 SKU 上,其中 2 个 SKU 占全部库存差异的 74%;这两个 SKU 又集中在同一个仓库。继续查看数据库监控,发现数据库整体 CPU 不高,但该仓库对应的库存记录锁等待明显高于其他仓库。
这类观察改变了优化方向。团队没有马上扩容数据库,而是先检查库存更新条件是否包含仓库字段、是否存在多个线程同时争用同一库存行、取消订单回补是否在同一仓库重复执行。
| 分析指标 | 计算方式 | 能发现的问题 | 建议刷新频率 |
|---|---|---|---|
| 库存差异数量 | 账面库存减去流水推导库存 | 扣减、回补或人工调整未闭合 | 活动期间 1-5 分钟 |
| 重复库存操作率 | 重复业务操作号除以操作总量 | 接口重试或消息重复消费 | 活动期间 5 分钟 |
| 取消回补及时率 | 规定时间内完成回补的订单数占比 | 锁定库存长期未释放 | 活动期间 5-15 分钟 |
| 热点 SKU 锁等待占比 | 热点 SKU 锁等待时长占总事务时长 | 单行竞争而非整体资源不足 | 活动期间 1 分钟 |
| 订单库存闭合率 | 订单占用与库存流水成功关联的比例 | 订单成功但库存记录缺失 | 活动结束后核算 |
在情景模拟中,团队先将库存更新从“查询后更新”改为条件更新,再把库存流水写入和幂等记录纳入同一短事务;随后将通知、积分和营销权益移到异步链路。模拟压测中,库存差异从每万笔请求 23 次下降到 2 次,P99 延迟从 1.8 秒下降到 420 毫秒。
这组数据只用于说明优化顺序,不能理解为九数云的产品效果或某家企业的实际成绩。真正上线时,应以企业自己的压测、生产监控和对账结果为准。
值得注意的是,P99 下降并不意味着风险归零。团队仍需验证取消回补、消息重放、服务重启和数据库主从切换后的数据状态。库存系统的验收标准不能只有“接口更快”,还必须包括“异常之后能否恢复”。

如果企业已经使用九数云进行经营分析,可以将库存治理拆成三个看板,而不是制作一个只展示“当前库存”的大屏。
重点展示请求量、库存扣减成功率、库存不足率、P95/P99 延迟、锁等待和消息积压。这个看板用于活动期间快速判断是否需要限流、降级或暂停某个活动入口。
重点展示可售库存、锁定库存、已售库存、取消回补和人工调整。各字段需要能够按 SKU、仓库、活动和时间段下钻,不能只看全局汇总。
重点展示重复操作号、无订单库存流水、无流水订单占用、超过释放时限的锁定库存和对账差异。每一条异常最好能够回到订单号、库存操作号和消息号。
九数云在这里承担的是跨表分析和异常定位职责,而不是替代交易数据库。这个边界非常重要:看板可以帮助你发现“哪里不对”,但不能承担“扣减是否成功”的最终判断。
在压测前,团队必须定义库存守恒关系。一个常见的基础公式是:
期末可用库存
= 期初可用库存
有效锁定数量
已售出数量
+ 有效释放数量
+ 有效回补数量
+ 合法人工调整数量
不同企业的库存模型可能会把锁定库存和已售库存放在不同表中,但逻辑必须能够闭合。只要公式无法成立,压测得到的接口吞吐量就没有业务意义。
很多压测脚本只是不断提交正常订单,无法覆盖真实异常。库存系统的压测至少要设置以下场景:
| 指标类别 | 指标 | 建议观察内容 | 不合格信号 |
|---|---|---|---|
| 正确性 | 负库存次数 | 压测和活动期间是否出现负值 | 任何未授权负库存都应阻断上线 |
| 正确性 | 重复扣减次数 | 同一订单或操作号是否出现两次有效扣减 | 重复扣减无法自动识别和修复 |
| 一致性 | 库存闭合率 | 订单、库存流水、回补记录是否完整关联 | 存在无法解释的差异记录 |
| 性能 | P99 延迟 | 尾部请求是否持续超时 | 重试率随延迟上升 |
| 性能 | 锁等待时间 | 热点 SKU 是否形成排队 | 锁等待占事务耗时比例过高 |
| 稳定性 | 消息积压量 | 异步链路是否能够在峰值后恢复 | 积压持续增加且没有降速措施 |
库存为 1 的场景最容易暴露并发控制问题,因为理论上无论进入多少请求,最终只能有一个请求有效占用库存。这个测试不依赖复杂业务数据,却能够快速检查原子扣减、幂等和回补是否真正生效。
建议在压测结束后自动校验:成功锁定订单数是否等于成功扣减数;成功扣减数是否不大于初始库存;重复请求是否只产生一条有效操作;取消后回补是否只执行一次。

如果每日订单量不高、热点 SKU 不明显、库存模型相对简单,建议优先采用单库事务和条件更新。重点不是引入更多组件,而是确保库存表、订单表和库存流水表之间有清晰的状态关系。
这一阶段至少完成以下工作:
小规模企业最常见的浪费,是在没有明确瓶颈时引入缓存、队列和分布式锁。复杂度增加后,团队可能没有足够的监控和排障能力,最终问题比原来更难定位。
当订单量增长、活动频率提高,企业应重点观察热点 SKU、数据库锁等待和连接池排队。不要仅按日订单量判断架构压力,因为库存系统往往承受的是秒级峰值,而不是平均流量。
中等规模阶段可以逐步增加:
如果某几个 SKU 的异常占比很高,可以先进行局部热点治理,而不是立即对所有 SKU 实施分片。局部问题使用全局重构,通常会带来不必要的开发、测试和运营成本。
对于秒杀、直播带货和大型促销活动,流量峰值可能在几秒内集中爆发。此时可以采用缓存闸门、限流、队列削峰和数据库原子确认的组合方案。
关键是把链路分成不同层次:
大型活动还应准备降级策略。例如,暂时关闭实时库存展示、限制同一用户的重试次数、降低非核心接口优先级、暂停低优先级异步任务。降级的目标不是让所有请求都成功,而是优先保证库存和订单主链路不失控。
多仓库存的难点通常不是数据库更新速度,而是库存归属规则。订单究竟扣哪个仓的库存?仓库分配失败后是否允许切换?切换过程中是否会重复锁定?这些规则不清晰时,即使数据库性能很高,也可能造成跨仓重复占用。
建议为每一笔库存操作记录 SKU、仓库、批次、渠道和订单号。仓库分配成功后再执行对应维度的原子扣减;如果分配失败,必须释放已经产生的临时锁定,而不是简单把订单标记为失败。
条件更新的优势是实现简单、数据库语义明确,适合库存按 SKU 或 SKU 加仓库维度管理的业务。它可以在数据库层面直接约束可用数量,失败请求也能通过影响行数快速返回。
它的局限是:所有竞争最终仍然落在相应库存记录上。热点 SKU 极高并发时,单行更新能力可能成为上限;组合商品和复杂预占模型也需要额外的事务设计。
悲观锁适合“库存必须立即确认,且事务内操作很少”的场景。它的优点是排他行为直观,出现冲突时不会产生大量应用层重试。
它的代价是锁等待。只要事务中存在慢查询、外部调用或不必要的业务逻辑,锁竞争就会快速放大。使用悲观锁时,必须建立锁等待和死锁监控,不能只依赖数据库默认日志。
乐观锁适合并发冲突率相对可控、失败可以快速返回或有限重试的业务。它不要求请求长时间等待锁,但需要正确处理版本冲突。
如果重试没有上限,或者每次重试都重新读取大量数据,乐观锁在热点 SKU 上可能变成“高频失败加高频重试”。因此,乐观锁的核心不是 version 字段,而是冲突后的业务策略。
缓存预扣能够把部分高峰流量挡在数据库之外,适合库存量有限、请求高度集中的活动。但它要求企业具备故障恢复和库存对账能力。
如果业务对库存绝对一致性要求极高,且活动峰值并不大,直接使用数据库条件更新可能更容易控制。如果业务更看重峰值吞吐,且能够接受“先锁定、后异步确认”的状态模式,缓存预扣才更有价值。
分片能够把单行竞争分散到多个库存桶,在热点极强时有明显价值。但它会增加库存聚合、回补、跨仓分配和对账的复杂度。
我不会把分片作为默认方案。只有当监控证明单行热点已经成为主要瓶颈,并且团队已经具备库存流水、补偿和对账能力时,才建议进入分片设计。

在修改库存链路之前,应至少保存一段时间的基线数据,包括库存扣减成功率、库存不足率、P95/P99 延迟、锁等待、死锁、事务回滚和消息积压。
没有基线,就无法判断优化是否真的有效。比如接口延迟下降,可能只是流量下降;库存差异减少,可能只是活动规模变小。基线必须包含相同业务场景下的可比数据。
库存优化不建议一次覆盖所有 SKU。可以按照活动、仓库、商品类别或用户流量比例灰度,优先选择库存模型简单、可对账的业务进行验证。
灰度期间,新旧链路都应保留必要的操作流水,但不要让两套链路同时真实扣减同一份库存。可以通过影子计算、只读比对或旁路校验验证新逻辑,而不是拿真实库存做不可逆实验。
回滚条件不能只写“出现严重问题”。应把它转化为可观测指标,例如:
不同企业阈值不同,但必须在上线前约定。活动开始后才临时讨论是否回滚,通常已经错过最佳处理时间。
所有差异都自动修复并不安全。若差异来源尚未确定,直接调整库存可能把一个重复扣减问题改成虚增库存问题。
建议把差异分成三类:
库存对账的价值不是让报表上的数字看起来相等,而是让每一次差异都有原因、有责任边界、有处理结果。

第一周不要急于改代码。先确认库存表、订单表、库存流水表、支付状态和取消回补记录之间如何关联,找出唯一的业务操作号。
同时拉取最近一段时间的监控数据,按 SKU、仓库和活动统计请求量、扣减成功率、P99、锁等待、回补失败和对账差异。没有这些数据,后续所有架构讨论都容易停留在感觉层面。
第二周优先修复最危险的并发窗口:取消“先查询、后更新”的扣减方式,改为条件更新,并根据影响行数判断结果。
同时补充订单级幂等、库存操作流水和重复请求测试。对于已经存在的库存差异,不要一边改链路一边直接修数字,应先保留异常样本,避免新旧问题混在一起。
第三周围绕数据库实际瓶颈开展优化:查看执行计划、清理无效索引、确认联合索引、缩短事务、移除外部调用和非核心写入。
如果数据库整体资源并不紧张,但热点 SKU 的锁等待很高,应优先做热点治理;如果查询扫描量大、CPU 高,则先优化 SQL 和数据访问;如果数据库写入能力已经达到瓶颈,再评估缓存和队列。
第四周进行库存为 1 的极端并发测试、重复提交测试、消息重复消费测试和取消回补测试。性能指标和正确性指标分别验收,不要因为吞吐量达标就忽略库存差异。
灰度上线后,用业务分析看板持续观察热点 SKU、仓库差异、库存闭合率和回补及时率。九数云等分析平台可以用于跨表聚合和异常下钻,但实时交易仍应由库存服务和数据库负责。
如果企业只能先做三件事,我建议按以下优先级执行:
只有当这三步完成后,企业仍然受到明显的热点流量和数据库写入瓶颈影响,才需要进一步考虑缓存预扣、队列削峰、库存分桶或分片。
电商库存系统最容易犯的错误,是把所有问题都归因于数据库性能。实际上,超卖通常发生在并发窗口、事务边界、重复请求、消息重试和库存回补之间;数据库慢只是让这些问题更容易集中暴露。
更稳妥的路径是:用条件更新或合理锁机制守住库存数量约束,用短事务减少锁竞争,用幂等键防止重复扣减,用缓存和队列承接峰值,用库存流水记录每一次变化,再通过对账分析发现长链路中的异常。
我最想强调的独特判断是:库存系统的优化目标,不是把所有请求都处理得更快,而是让“成功、失败、重试、取消和回补”都变成可解释、可验证、可恢复的状态。
下一步可以先选一个库存为 1 的热点 SKU,跑一轮并发压测;再用订单号、库存操作号和消息号核对库存是否闭合。只要这个最小场景仍无法稳定通过,就不要急着引入更多中间件。先修复正确性,再根据真实监控数据释放性能,才是电商企业降低超卖风险、控制改造成本的可持续方法。
我负责过一次限量商品压测,最初方案是先查询库存,再调用更新语句,结果在库存只剩 1 件时,多个并发请求都拿到了“有库存”的结果。我想知道,为什么查询速度已经很快,系统仍然会超卖,以及数据库原子扣减到底解决了哪一层问题?
超卖的根因通常不是查询慢,而是“判断库存”和“扣减库存”之间存在时间间隔。请求 A 和请求 B 先后查询到同一个库存值,随后分别执行扣减,即使每条 SQL 都很快,也可能把同一件商品分配给多个订单。
更稳妥的基础写法是把库存条件放进更新语句,让数据库在一次原子操作中完成“库存足够”和“扣减”两个动作:
UPDATE inventory SET available_qty = available_qty - :qty WHERE sku_id = :sku_id AND available_qty >= :qty;然后根据影响行数判断结果:影响 1 行代表扣减成功,影响 0 行代表库存不足或条件不满足。这里的关键不是 SQL 看起来简短,而是数据库会对符合条件的记录执行并发控制,避免应用层把“读到有货”误当成“已经占有库存”。一次示例压测中,库存设置为 100 件,并发请求逐步从 50 提升到 1000。
对比结果如下,数据用于说明测试方法,不代表某个企业的线上指标: 方案库存为负重复扣减风险主要瓶颈 先查后扣可能出现高应用层竞态 条件更新可避免需配合幂等热点行锁竞争 条件更新+流水可避免可追溯控制事务设计与写入吞吐 需要特别注意,原子扣减只解决“同一库存记录不能被无条件重复扣减”,并不自动解决订单创建失败、支付超时、取消订单回补和重复请求。
因此,生产方案还应增加订单号或库存操作号作为幂等键,并记录扣减前后数量、操作类型和关联订单。
我曾经见过一个库存接口,功能上已经不再超卖,但大促一开始 P99 延迟就从几十毫秒升到数秒,数据库连接池也很快被占满。我不想简单地把数据库扩容或继续加锁,应该先看哪些指标,怎样定位真正的锁竞争来源?
加锁能提高正确性,但锁本身不是性能优化。库存更新变慢,通常要先区分三类问题:热点 SKU 集中竞争同一行、更新没有命中合适索引、事务持锁时间过长。排查时,建议同时观察锁等待时间、死锁次数、事务持续时间、连接池活跃连接数、数据库 CPU 和执行计划。
只看接口平均响应时间很容易误判,因为平均值可能正常,但少量热点请求已经把 P99 拉高。一个可执行的排查顺序是:先确认更新条件是否命中 SKU、仓库和批次等关键索引;再检查事务中是否包含订单写入、优惠计算或外部服务调用;最后按 SKU 统计锁等待,判断是不是少数爆款商品占用了大部分数据库写入能力。
现象更可能的原因优先动作 锁等待高、单个 SKU 请求集中热点行竞争限流、分桶或预扣库存 CPU 高、更新扫描行数异常索引未命中检查执行计划和索引条件 事务持续时间长、连接池耗尽事务范围过大移除外部调用和非核心写入 死锁偶发、多个库存维度同时更新加锁顺序不一致统一更新顺序并设置重试上限 我更倾向于先缩短事务,再处理热点拆分。
事务内只保留库存条件更新和必要的库存流水写入,通知、营销权益发放、搜索索引更新等动作放到事务外异步处理。因为锁粒度再合理,如果事务持锁期间还在等待网络调用,吞吐也会被拖垮。如果确认是单个爆款 SKU 成为热点,可以把库存拆成多个逻辑桶,让请求分散到不同记录。
但这会带来库存汇总、回补和对账复杂度,所以不建议在没有监控和补偿能力时直接采用。
我在评估大促方案时,团队有人主张把库存全部放到缓存里,认为这样既快又不会锁数据库;也有人坚持所有请求都必须直接更新数据库。我比较担心缓存重启、消息重复和订单取消回补时出现账实不一致,究竟怎样划分两者的职责更稳妥?
缓存适合承接高并发流量,但不应被默认视为唯一库存账本。它可以快速完成可售资格判断、活动库存预热和请求削峰,却必须配合持久化记录、幂等机制、失败补偿以及最终对账。更实用的分工是:缓存负责“高峰期能否快速拦截请求”,数据库或库存服务负责“这次库存操作是否被正式记录”。
如果缓存预扣成功后,订单创建失败,就必须有明确的回补路径;如果消息重复消费,也不能再次扣减。可以把一次扣库存拆成以下链路:请求限流 → 缓存预扣 → 生成唯一库存操作号 → 写入消息或库存操作表 → 持久化扣减 → 创建订单 → 取消或超时时回补。
每一步都要定义成功、失败、超时和重试后的状态,而不是只关注接口返回成功。
组件适合承担的职责不应单独承担的职责 缓存预热、快速判断、削峰完整库存审计和永久账本 数据库库存流水、状态落库、对账依据无限承接爆发流量 消息队列异步解耦、削峰、重试直接保证业务幂等 库存操作表记录操作号和处理状态替代所有业务状态管理 判断方案是否可靠,不要只测缓存命中率和接口 QPS,还要故意制造缓存节点重启、消费者重复消费、订单创建超时、取消回补失败等异常。
最终要核对可售库存、锁定库存、已售库存和库存流水是否闭合。如果企业当前并发量还没有达到数据库瓶颈,优先做好条件更新、幂等和库存流水,往往比提前引入复杂的缓存预扣更稳。缓存方案的收益来自削峰,不来自技术名词本身;没有补偿闭环时,速度越快,错误库存扩散得也越快。
我以前参与过一次接口压测,报告里只有 QPS、平均响应时间和成功率,看起来指标都不错,但活动结束后对账才发现有重复占用和少量回补失败。我想建立一套更接近真实业务的验收标准,避免只证明系统“跑得快”,却没有证明库存“算得对”。
库存系统的压测必须同时验证性能和一致性。只看 QPS、平均响应时间或 HTTP 成功率,会遗漏重复提交、消息重放、订单取消和库存回补等真正容易出错的路径。建议至少准备五组场景:库存为 1 的极端抢购、多个用户争抢同一 SKU、多 SKU 混合购买、请求超时后的自动重试、订单取消后的库存回补。
若系统使用缓存和消息队列,还要加入缓存重启、消费者重复消费、消息延迟和死信处理场景。
验证维度必须观察的指标合格判断示例 接口性能QPS、P95、P99、错误率高峰期无持续性延迟恶化 数据库压力CPU、锁等待、死锁、连接池无持续锁堆积或连接池耗尽 库存正确性负库存、重复扣减、库存差异不出现负库存,流水可闭合 异常恢复重试、回补、消息积压、对账差异失败可重试,异常可定位和补偿 压测数据应使用独立的测试 SKU 和可追踪的订单号,测试前记录初始库存,测试后分别统计成功扣减、失败扣减、取消回补和重复请求结果。
不要只用“最终库存等于预期”作为结论,因为扣减和回补可能相互抵消,掩盖中间过程中的重复操作。我建议把库存守恒写成验收公式:期末可售库存 + 锁定库存 + 已售库存 = 期初库存 + 合法入库 – 报损或其他合法出库。与此同时,库存流水的操作数量、订单状态和幂等记录也必须能够逐笔关联。
如果压测发现 P99 上升但库存始终正确,应优先处理锁竞争和削峰;如果性能很好但出现重复扣减,则必须先修复正确性,不能用扩容或加缓存掩盖问题。上线前还应准备限流、关闭活动入口、暂停异步消费者和人工对账等降级及回滚动作。


读者评论
文章把超卖问题归因于并发一致性,而不是简单归咎于数据库性能,这个判断比较准确。条件更新、幂等和库存流水确实应先于缓存、队列等扩展方案落地。
对“库存不为负不代表没有超卖”的解释很有价值。订单占用、支付成功和仓库履约之间可能存在差异,排查时只看库存字段容易遗漏真实问题。
文中对缓存和消息队列的边界说明较客观,尤其提醒了重复消费、失败回补和幂等处理。实际采用异步方案时,确实不能只关注吞吐量。
建议中的监控指标比较实用,除了平均响应时间,还应关注P99、锁等待、连接池排队和事务耗时,这些指标更能反映大促期间的真实风险。
文章的不足是部分性能数字属于情景模拟,不能直接作为容量规划依据。企业上线前仍需结合自身数据库版本、业务模型和压测结果验证方案。