库存扣减系统最危险的时刻,往往不是数据库 CPU 跑到 90%,而是监控看起来一切正常,业务却已经出现了“订单成功但库存没扣”“同一订单扣了两次”或“库存变成负数”。我在做并发扣减方案评审时,通常不会先问“要不要上缓存、队列或分库分表”,而是先问三个问题:一次扣减能否被准确确认、重复请求能否被识别、压测中最热的那一行到底能承受多少次竞争。数据库库存并发能力的提升,真正应当围绕这三个问题逐步推进,而不是围绕中间件数量堆叠架构。
普通查询的并发提升,很多时候可以通过缓存、读写分离和横向扩容解决。但库存扣减属于典型的高冲突写入:大量请求可能同时修改同一个商品、同一个仓库或同一个资源额度。应用服务器可以轻松接收更多请求,并不代表数据库能够正确地完成更多扣减。
因此,我对“并发能力”的定义通常包含四个维度:单位时间内成功完成的扣减数量、成功请求的尾延迟、数据库热点竞争程度,以及库存与订单最终是否一致。只有吞吐量提高、P95 和 P99 没有失控、错误率可接受、库存账实相符,才算真正提升了系统能力。
库存系统的优化目标不是让所有请求都更快返回,而是让有效扣减在可控成本内稳定完成。如果接口响应时间从 80 毫秒降到 20 毫秒,却因为超时重试导致重复扣减,或者订单状态与库存状态无法对账,这种优化在业务上是失败的。
这个顺序看起来不够“激进”,却更符合生产系统的真实约束。复杂架构并不会自动消除一致性问题,反而可能增加重试、补偿、回滚、对账和运维成本。

数据库总 CPU 低,不代表库存扣减没有瓶颈。一个热门商品只有一行库存记录,几千个请求同时更新这行数据时,瓶颈可能表现为行锁等待、事务排队和连接池耗尽,而不是整体 CPU 飙升。
我建议把以下指标放在同一块监控面板中观察:扣减成功吞吐、库存扣减 P95/P99、锁等待时间、事务平均持续时间、数据库连接池占用率、受影响行数为零的比例、幂等命中率、消息积压时间和库存对账差异。
| 观察结果 | 更可能的瓶颈 | 优先动作 |
|---|---|---|
| 数据库 CPU 高、慢 SQL 增多 | 查询计划、索引或无效读取 | 先看执行计划、索引和 SQL 访问列 |
| CPU 不高但锁等待明显 | 热点行竞争或事务过长 | 缩短事务、降低单行冲突、识别热点资源 |
| 应用线程池和连接池同时打满 | 下游处理变慢或重试放大 | 先限流和控制重试,避免盲目扩容 |
| 接口很快但对账差异增加 | 异步链路、缓存或状态机不完整 | 补齐幂等、状态确认、补偿和对账 |
在代码里,库存扣减可能只是一个数字减法;在业务里,它通常同时关联商品可售状态、订单状态、支付状态、锁定库存、已售库存、取消回补和售后冲正。任何一个状态更新延迟,都可能让系统出现短暂甚至永久的不一致。
例如,用户提交订单后,系统先锁定库存,再等待支付。支付超时后,锁定库存应当释放;如果释放任务重复执行,库存可能被多加一次;如果释放任务漏执行,库存又会被长期占用。由此可见,库存并发问题并不只发生在“扣减”这一条 SQL 上,还发生在重试、取消、回补和人工补单等所有改变库存的动作上。
假设一天有 100 万次库存请求,平均分布到 10 万个 SKU 上,单个 SKU 的竞争可能并不严重。反过来,即使总请求只有每秒 2000 次,如果其中 80% 集中打到一个热门 SKU,数据库依然可能因为单行锁竞争迅速排队。
这就是我在容量评估中最重视“热点比例”的原因。总 QPS 只能描述系统收到多少请求,无法说明这些请求是否在争抢同一条记录。压测时如果只做随机 SKU 分布,结果往往会比真实大促场景乐观很多。
这四类流量的正确性要求和延迟目标并不相同。展示库存可以允许短暂延迟,最终扣减通常不能接受超卖;回补操作可以异步处理,但必须保证幂等和可追溯。把它们全部放进同一个同步接口里,往往会让系统在高峰期互相拖累。

考虑一个常见流程:客户端提交订单后等待 3 秒,网关因超时自动重试一次;第一次请求实际上已经在数据库中扣减成功,但响应在网络层丢失。第二次请求没有携带幂等号,服务再次执行扣减,于是出现重复扣减。
另一种情况是,应用先查询库存,判断剩余数量大于购买数量,再执行更新。两个事务同时读到库存为 1,都判断可以购买,最终一个请求成功,另一个请求也可能覆盖前一个结果,或者在特定隔离级别和实现方式下产生超卖。这两类问题分别说明:并发控制和幂等控制是两条不同的防线,缺一不可。
最常见的错误代码逻辑是“SELECT 查询库存,if 判断库存足够,UPDATE 扣减”。查询和更新之间存在时间窗口,多个请求可以同时通过应用层判断。即使后面的 UPDATE 使用了事务,也不能把应用层的判断天然变成数据库层面的原子条件。
正确的思路是把“库存是否足够”和“库存减多少”放进同一条受数据库保护的更新操作中,再通过受影响行数或明确的业务结果判断成功与否。
UPDATE inventory SET available_stock = available_stock - :quantity, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE sku_id = :sku_id AND warehouse_id = :warehouse_id AND available_stock >= :quantity;
这条 SQL 不是完整的库存系统,但它建立了一个重要底线:只有满足库存充足条件的记录才允许被扣减。实际生产中还需要设计库存流水、业务幂等、事务边界和异常补偿。
缓存扣减的响应速度通常很好,但它解决的是热点访问和快速判断问题,不会自动解决数据库落库失败、网络分区、缓存重启、重复请求和库存回补。缓存扣减成功后,如果订单创建失败,库存是否回补?回补失败谁来重试?缓存和数据库哪个是最终事实来源?这些问题没有答案,就不应把缓存直接称为完整库存方案。
我更倾向于先把缓存用于库存展示、活动资格预判和热点数据读取,再根据一致性要求决定是否让缓存参与扣减。对于资金、票务、有限名额等高风险资源,必须先明确“允许多长时间的不一致”以及“最终如何核账”。
读写分离适合降低主库的查询压力,但库存扣减本身仍然要在可写节点完成。若业务先从只读副本读取库存,再到主库执行扣减,复制延迟可能让读到的库存不是最新值。
因此,库存展示可以读取副本或缓存,最终扣减的判断不能依赖可能滞后的副本。读写分离能够改善系统整体容量,却不能消除热点写行竞争。
队列的本质是把“瞬时到达速度”与“稳定处理速度”分开。如果生产端长期以每秒 10000 条消息写入,而消费者只能处理每秒 3000 条,队列只是把数据库前的压力变成了积压。用户可能在等待,订单状态可能处于未知,最终仍需要面对处理能力不足的问题。
使用队列前,至少要计算三个量:峰值生产速率、稳定消费速率和允许的最大排队时间。只有在峰值是短时波动、平均生产速率低于消费能力时,削峰才有实际价值。
连接数增加后,表面上可以承接更多请求,但热点行竞争不会因此消失。大量连接同时等待同一行,可能导致线程切换、上下文开销、锁等待和内存使用进一步上升。
当连接池被打满时,我会先检查请求是否在等待数据库锁、是否存在慢事务、是否发生超时重试,而不是直接把连接池从 200 调到 1000。连接池的作用是控制并发,不是制造并发。

库存接口通常包含展示和扣减两条路径。若数据库慢查询主要来自商品详情、库存展示和列表接口,应优先考虑缓存、字段裁剪和读流量隔离。若慢点集中在库存更新、库存流水写入和订单状态更新,则需要重点检查锁竞争、事务长度和写入模型。
判断方法不复杂:分别统计查询类 SQL 和更新类 SQL 的请求量、平均耗时、P95、P99、锁等待和失败率。不要只看数据库实例级别的 CPU,因为实例总指标会掩盖某一类 SQL 的局部拥塞。
| 问题表现 | 优先排查 | 不宜马上做的事 |
|---|---|---|
| 展示接口 QPS 高,扣减接口稳定 | 缓存命中率、返回字段、查询索引 | 直接改造扣减为异步 |
| 扣减 P99 高,锁等待集中 | 热点行、事务范围、重试策略 | 盲目增加数据库连接 |
| 扣减成功但订单状态延迟 | 事件投递、消费速度、状态机 | 简单扩大缓存容量 |
| 数据库压力不高但接口超时 | 线程池、网络、下游依赖、超时配置 | 仅凭数据库指标判断瓶颈 |
库存展示和库存扣减不必使用完全相同的一致性级别。用户看到“还剩 3 件”,随后有人抢先买走,这种展示短暂滞后通常可以接受;但两个订单都被确认成功而实际只有 1 件库存,就属于不可接受的业务错误。
我会把业务资源分为三类:可超卖后人工补偿的普通商品、绝对不能超卖的票务或名额资源、涉及金额或权益的账户型资源。三类资源的架构选择不应相同。越接近资金和不可再生名额,越应优先保证强约束和可审计性。
如果所有请求都更新同一个库存总数,数据库只能按顺序处理这条记录上的竞争。此时即使增加应用实例,也只是增加等待者。真正的优化方向应当是降低同一行的竞争密度,或者把库存物理上拆成多个可独立扣减的分片。
常见的分段库存做法,是将某个 SKU 的 10000 件库存拆成 100 个库存段,每段 100 件,请求按照规则分配到不同库存段。这样可以把单一热点分散到多行,但也会带来段选择、库存汇总、回补和段耗尽后的重新分配问题。
不存在脱离业务和硬件环境的统一高并发标准。每秒 500 次更新,对一条简单库存记录可能已经是高竞争;每秒 5000 次只读请求,对命中率高的缓存系统可能并不困难。
更有意义的容量模型是:
有效扣减吞吐
= 请求总量 × 成功率 × 幂等有效率
÷ 单次资源占用时间
这个公式不是数据库理论公式,而是容量评估中的简化表达。它提醒我们:如果大量请求最终因为库存不足、重复提交或排队超时而失败,单纯提高接入 QPS 并没有提高有效业务产出。

库存记录至少需要能够唯一定位到业务资源,例如商品、仓库、批次或活动场次。不要让一次扣减通过模糊条件扫描大量记录,也不要在扣减 SQL 中携带与库存无关的复杂联表逻辑。
一个简化的库存表可以包含可用库存、锁定库存、已消耗库存和版本号。字段设计不是越多越好,关键是每个字段都要明确状态含义,避免“available_stock”在不同服务里代表不同口径。
CREATE TABLE inventory (
id BIGINT PRIMARY KEY,
sku_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
available_stock INT NOT NULL,
locked_stock INT NOT NULL DEFAULT 0,
consumed_stock INT NOT NULL DEFAULT 0,
version BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL,
UNIQUE KEY uk_sku_warehouse (sku_id, warehouse_id)
);实际使用哪种数据库、字段类型和索引方式,需要结合数据库版本、数据量和写入模型验证。这里的重点不是复制表结构,而是建立“资源定位清楚、库存口径清楚、状态变化可追踪”的基础。
执行条件更新后,受影响行数为 1,通常表示本次扣减满足条件;为 0,则可能是库存不足、资源不存在或条件不匹配。服务层必须把这些情况区分开,不能统一返回“系统繁忙”,否则既影响用户体验,也妨碍运营分析。
UPDATE inventory SET available_stock = available_stock - :quantity, locked_stock = locked_stock + :quantity, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE sku_id = :sku_id AND warehouse_id = :warehouse_id AND available_stock >= :quantity;
如果还需要写入库存流水和订单锁定记录,应当合理设计事务边界。库存更新成功而流水未写入,会造成后续无法对账;但把支付、通知、外部仓储调用都放进同一事务,又会显著延长锁持有时间。通常应将数据库内必须同时成立的状态放在一个短事务中,将外部副作用交给可靠事件和补偿机制处理。
幂等不能只依赖客户端“应该不会重复提交”。网络超时、网关重试、消息重复投递和用户双击都会让重复请求在生产环境中出现。每次扣减都应带有唯一业务流水号,并记录动作类型、资源标识、数量、订单号和处理结果。
幂等判断也不能只说“查到就返回成功”。如果第一次请求执行到一半服务崩溃,幂等记录可能处于处理中状态;第二次请求应该知道是继续确认、重试,还是进入人工或自动补偿。为此,幂等状态最好至少区分处理中、成功、失败和可重试。
| 幂等状态 | 第二次请求的处理 | 需要保留的信息 |
|---|---|---|
| 处理中 | 查询处理进度,避免立即重复扣减 | 开始时间、租约时间、执行节点 |
| 成功 | 返回原始处理结果 | 扣减数量、库存流水号、订单状态 |
| 失败且可重试 | 按退避策略重新执行 | 失败原因、重试次数、下次重试时间 |
| 失败且不可重试 | 返回明确业务失败或进入补偿 | 错误码、审计信息、人工处理记录 |
单库方案上线前,我会要求做至少三类校验:并发扣减校验、重复请求校验和异常中断校验。并发扣减要验证不会出现负库存;重复请求要验证同一业务流水只改变一次库存;异常中断要验证服务在数据库提交前后分别崩溃时,订单、库存和流水是否能恢复到可解释状态。

库存扣减通常按 SKU、仓库、批次或活动标识定位记录。索引应当覆盖最稳定、选择性合理的定位条件。索引过少会导致扫描和锁范围扩大,索引过多则会增加写入维护成本。
排查时不要只看“有没有索引”,还要看执行计划是否实际使用了预期索引、是否发生隐式类型转换、是否因为函数操作导致索引失效,以及更新语句是否锁定了超出预期的记录范围。
库存行被锁住的时间,取决于事务从开始到提交的完整时长,而不是单条 UPDATE 的执行时间。如果事务中还执行优惠计算、远程会员查询、支付校验、消息发送或日志写入,数据库锁可能一直保持到这些操作结束。
比较稳妥的拆分方式是:先在服务层完成不需要锁住库存的校验,再用短事务完成库存状态和核心流水落库,最后通过可靠事件处理通知、积分、营销统计等后续动作。
这里有一个容易忽略的边界:事务拆分不是简单地“把 SQL 分成两段”。拆分后必须重新定义失败状态、重试条件和补偿动作,否则只是把数据库内的一致性问题转移到了服务之间。
平均响应时间常常掩盖少量严重慢请求。库存系统更应关注 P95 和 P99,因为用户在高峰期遇到的往往不是平均请求,而是排队最长的那一批请求。
建议同时记录锁等待开始时间、等待结束时间、资源标识和业务流水号。这样可以知道是某个 SKU 持续成为热点,还是某一批事务偶发变长。没有资源维度的锁监控,最后通常只能得到“数据库偶尔变慢”这种无法执行的结论。
当扣减请求因为锁等待超时而失败时,所有客户端立即重试,会形成更大的竞争。重试应当区分业务失败、暂时性数据库错误和网络超时;不同错误使用不同策略。

很多系统的数据库压力并非来自真正的扣减,而是来自用户不断刷新页面、列表批量查询和营销页面轮询库存。展示缓存可以显著减少这些读请求,让数据库把资源留给核心写入。
但展示缓存必须明确口径。缓存里的“剩余库存”可能是几秒前的可售库存,也可能只是营销层展示数字。前端应避免把展示值包装成绝对承诺,更不能依据缓存结果直接确认订单成功。
如果这些问题只能回答“后面再补”,说明系统还没有准备好让缓存承担最终扣减。缓存可以先做旁路读取,逐步验证热点模型,再决定是否前移写操作。
同步扣减的优点是用户能快速知道成功或失败,适合库存结果必须即时确认的场景。异步扣减的优点是可以把请求写入队列,按照消费者能力稳定处理,适合用户可以接受排队状态的活动或预约业务。
两种模式的取舍,不是“哪个性能更高”,而是“业务是否接受结果延迟”。如果用户必须在支付前知道是否锁定成功,同步路径应保留;如果用户接受“排队中”,异步路径可以承接峰值,但必须提供状态查询、超时关闭和失败补偿。
分段库存、库存桶或逻辑分片的核心思想,是把一个热点总量拆到多个记录中,让并发请求不再全部争抢同一行。它适合单个资源特别热门、库存总量较大且能够接受分段管理的场景。
但分段会增加库存汇总和回补复杂度。例如某一段还有 2 件,另一段还有 3 件,展示层需要汇总;取消订单时必须回补原分段,或者建立统一的回补分配策略。分段不是把一列拆成多列那么简单,而是改变了库存状态模型。
| 方案 | 主要解决的问题 | 新增复杂度 | 更适合的场景 |
|---|---|---|---|
| 展示缓存 | 大量库存查询 | 失效与短暂不一致 | 详情页、列表页、活动页 |
| 同步数据库扣减 | 即时、可控的最终扣减 | 热点行等待 | 普通订单、强一致资源 |
| 消息队列削峰 | 瞬时请求洪峰 | 排队、重复消费、积压 | 可接受结果延迟的预约或抢购 |
| 分段库存 | 单行热点竞争 | 分配、汇总、回补 | 极热 SKU、大批量有限资源 |

随机生成 SKU 的压测结果通常比较好看,因为请求被平均分散到很多行。真正接近生产的压测,应至少包含均匀分布、中度热点和极端热点三组模型。
还要模拟库存不足、重复请求、连接超时、消费者重试和订单取消。库存不足比例会影响数据库更新结果,但不能简单把失败请求排除在压测之外,因为生产系统同样需要消耗资源处理这些失败请求。
其中,“成功扣减数”比“接口 QPS”更接近业务价值。若接口 QPS 增长依靠大量失败和重试堆出来,系统并没有获得可持续的并发能力。
| 压测结果 | 判断 | 下一步 |
|---|---|---|
| 均匀分布和热点分布都稳定 | 单库模型暂时可用 | 完善监控、容量预警和故障演练 |
| 均匀分布稳定,热点分布明显变慢 | 单行竞争是主要瓶颈 | 先缩短事务,再评估分段库存或限流 |
| 数据库稳定,但应用接口超时 | 可能是线程池、网络或下游依赖问题 | 排查调用链和重试,不要立刻改数据库架构 |
| 吞吐提高但对账出现差异 | 性能提升破坏了业务闭环 | 暂停扩容,优先修复幂等、状态和补偿机制 |
下面是一组情景模拟数据,用来说明压测方法。假设使用单库关系型数据库、8 核 32 GB 实例、单表按 SKU 和仓库定位、库存扣减为短事务,测试持续 10 分钟。它不能代表所有数据库的固定能力,技术负责人应使用自己的数据重新验证。

压测工具显示成功,并不代表业务数据正确。测试完成后至少要核对:初始库存是否等于成功扣减、回补和剩余库存之和;同一幂等号是否最多产生一次有效变更;库存流水数量是否与订单状态匹配;是否出现库存为负、库存少扣或库存多加。
如果系统引入了队列,还要核对生产消息数、成功消费数、重试数、死信数和最终补偿数。若只看接口延迟,不看这些业务账目,压测很可能只是验证了“系统能返回”,没有验证“系统能正确工作”。

如果日常流量平稳、热点 SKU 不明显、库存扣减链路较短,通常不需要一开始就引入复杂的分布式库存架构。优先完成原子条件更新、业务幂等、库存流水、短事务和基础监控,往往已经能够解决大部分严重问题。
这一阶段最重要的不是追求极限吞吐,而是建立可重复的压测基线。每次修改索引、事务、连接池或重试配置后,都用相同流量模型比较 P95、P99、锁等待和对账结果。
商品详情、活动页面和购物车可能产生大量库存读取,但真正提交扣减的请求比例很低。此时最合适的动作通常是库存展示缓存、批量查询、字段裁剪和热点页面降级,而不是把最终扣减迁移到缓存。
缓存更新可以采用短 TTL、事件通知或定时刷新等方式。选择哪一种,取决于库存展示允许的延迟和更新频率。关键是让用户知道展示值是参考信息,最终结果以提交后的业务确认状态为准。
极端大促场景中,系统真正面对的不是“所有请求都必须立即完成”,而是瞬时流量远超稳定处理能力。可以在入口进行活动资格校验、无库存快速失败、用户维度限流和热点资源排队,减少无效请求进入数据库。
随后再使用队列吸收短时洪峰,并把用户状态设计为排队中、处理中、成功、失败和超时关闭。这个方案牺牲即时性,换取系统不被瞬时流量冲垮,适合业务能够接受结果延迟的场景。
当一个 SKU、一个活动名额或一个场次成为绝对热点时,横向增加应用实例通常收益有限。应考虑分段库存、预分配库存、请求排队或按资源拆分扣减记录。
采用分段库存前,要先回答回补问题:订单取消后是否回到原库存段?某些库存段耗尽后是否允许切换?展示库存如何汇总?如果这些规则没有明确,分段后可能从“数据库锁竞争”变成“库存段管理混乱”。
资金和不可替代名额的错误成本远高于几十毫秒延迟。此类系统不应仅凭吞吐量决定架构,必须保留强一致的核心扣减、完整流水、操作审计和可验证的补偿机制。
如果业务必须采用缓存前置或异步确认,应在上线前明确故障状态如何对用户呈现,以及出现账实不符时能否快速定位到具体业务流水。无法审计的高并发,通常只是把风险推迟到售后和财务环节。

同步扣减的优势是结果明确。用户提交后,服务可以在一个请求内返回库存足够或不足,链路容易理解,问题定位相对直接。它的限制是峰值流量会直接冲击数据库,热点行竞争会拉长尾延迟。
异步扣减的优势是削峰。请求先进入队列,消费者按稳定速率处理,可以保护数据库和核心服务。代价是用户无法立即得到最终结果,需要查询状态;同时还要处理消息重复、顺序、积压、死信和超时关闭。
强一致方案通常拥有更清晰的业务边界,但吞吐和扩展成本可能更高。最终一致方案可以获得更好的峰值承接能力,却必须承担补偿、对账和状态延迟。
我不建议用“最终一致更先进”或“强一致更可靠”做绝对判断。正确的选择取决于错误成本:普通促销商品可能允许少量延迟和人工补偿;票务、名额、账户余额则通常更重视绝不能重复消费。
单库的优点是事务边界清晰、查询简单、运维成本低。只要热点没有超过数据库承受范围,单库往往是性价比最高的方案。分片可以分散数据和写压力,但会带来跨分片汇总、跨服务事务、全局幂等和故障恢复问题。
分片前应拿出数据证明单库已经成为瓶颈,并且瓶颈确实与数据规模或热点写入有关。如果只是索引不合理、事务过长或重试失控,分片不会修复根因。
极致低延迟往往意味着更多数据前移、更少同步等待和更复杂的异步链路;可追溯则要求保留完整流水、状态和审计信息。两者可以兼顾,但需要额外投入事件记录、链路追踪、对账任务和补偿工具。
| 选择方向 | 获得的收益 | 必须承担的代价 | 适用边界 |
|---|---|---|---|
| 数据库同步扣减 | 结果即时、事务清晰 | 热点写入受锁模型限制 | 强一致、规模中等的资源扣减 |
| 缓存前置扣减 | 低延迟、峰值承接强 | 落库失败、回补和对账复杂 | 有成熟补偿体系的极热资源 |
| 队列异步扣减 | 削峰、保护数据库 | 结果延迟、消费积压和状态复杂 | 可接受排队确认的业务 |
| 库存分片 | 降低单行竞争、扩大写入空间 | 库存汇总、回补和运维复杂 | 单一热点长期占据流量的业务 |

库存流水至少应包含业务流水号、资源标识、变化前数量、变化数量、变化后数量、动作类型、来源系统、操作时间和关联订单。对于回补和人工修正,还应保留原因和授权信息。
流水不是为了让数据库表看起来更完整,而是为了在异常发生后回答三个问题:哪一笔业务改变了库存、为什么改变、是否已经被重复执行。没有这些信息,运营只能看到一个错误的库存数字,研发也很难判断是少扣、重复扣还是回补失败。
总量对账用于快速发现某个 SKU、仓库或活动的库存是否整体异常;明细对账用于定位到具体订单和业务流水。两者缺一不可。只做总量对账,知道有差异但找不到原因;只做明细对账,面对大规模数据时排查效率很低。
建议将对账任务设计为周期性和事件触发两种方式。周期性任务负责全面扫描,事件触发负责在扣减超时、消息进入死信、回补失败或人工修改后及时核查。
告警也应区分等级。库存出现负数、对账出现持续差异属于高优先级业务告警;P99 短时升高可能先进入观察;队列积压如果仍在可接受窗口内,可以先扩容消费者或降低入口流量。
最难处理的不是明确失败,而是请求超时但数据库结果未知。演练时应在数据库提交前、提交后、事件投递前、事件投递后分别中断服务,验证客户端重试、幂等查询、状态恢复和补偿流程是否符合预期。
如果系统只能通过人工查询数据库判断某次扣减是否成功,说明业务接口还没有提供足够的状态确认能力。技术负责人应把“结果未知”作为独立状态处理,而不是简单归入失败。

先不要改架构,收集至少一周的生产基线,或使用接近真实流量的压测数据。重点记录热点资源、扣减成功率、P95/P99、锁等待、技术失败、重复请求和对账差异。
随后完成以下改造:
这一阶段重点不是增加中间件,而是减少数据库不必要的工作。检查库存定位索引、慢 SQL、执行计划、事务长度和连接池配置,梳理哪些请求可以在入口快速失败,哪些展示查询可以从缓存读取。
对于自动重试,应设置最大次数、退避时间和随机抖动。所有重试都必须携带原始业务幂等号,不能在每次重试时生成新的扣减流水。
如果主要问题是展示读取,就做展示缓存和读路径隔离;如果主要问题是瞬时洪峰,就做限流、资格校验和队列削峰;如果主要问题是单行热点,就评估分段库存、预分配和热点排队。
不要同时上线多个大改动。一次只改变一个关键变量,保留可回滚方案,并用相同的压测模型比较改造前后。否则即使性能变好,也很难知道收益来自哪里,更无法判断故障风险是否被转移。
| 验收类别 | 最低要求 |
|---|---|
| 正确性 | 无负库存;同一业务流水最多一次有效变更;库存流水可解释 |
| 性能 | 达到目标成功吞吐;P95、P99 在业务可接受范围内 |
| 稳定性 | 锁等待、连接池、线程池和消息积压有明确上限 |
| 恢复性 | 超时、重复投递、服务重启和数据库故障后可重试或补偿 |
| 可审计性 | 可以从业务订单追溯到库存流水和最终库存状态 |
第一,压测报告只写吞吐量,不写热点分布和成功率。第二,监控面板只有 CPU、内存和接口平均耗时,没有锁等待、P99 和对账结果。第三,技术方案写了缓存、队列和分片,却没有写失败补偿、回补策略和结果未知处理。
这三个信号说明团队完成了技术组件接入,却没有完成库存业务闭环。对技术负责人来说,真正的交付物不是一张复杂架构图,而是可验证、可恢复、可审计的扣减系统。

第一,先保证每一笔库存变化都能解释。库存是有限资源,任何无法追溯的扣减、回补和人工修正,都会在高峰期放大成业务事故。
第二,先优化冲突,再优化流量。如果大量请求正在争抢同一行,增加应用实例、连接数或缓存容量都可能只是暂时缓解。应该先找出热点资源、缩短事务,再决定是否分段或排队。
第三,先用数据证明,再引入复杂度。缓存、队列、分片都可以成为好方案,但前提是压测和监控已经证明当前模型的边界。没有数据支撑的架构升级,往往只是把一种未知风险换成另一种未知风险。
我对库存并发改造的核心判断可以概括为一句话:不要先问系统能承受多少并发,而要先问在什么热点分布、什么一致性要求和什么失败条件下,系统仍能稳定完成多少次有效扣减。
当团队能够回答这个问题,并且用压测、监控、流水和对账持续验证,数据库库存扣减就不再是一次临时救火,而会变成可预测、可扩展、可恢复的容量治理体系。
我原本以为“先查库存、判断充足、再执行 UPDATE”只是多了一次数据库访问,性能差一点但不会影响结果。后来在压测中发现,多个请求同时读到相同库存时,真正危险的不是慢,而是库存可能被重复消费;我想知道,怎样的数据库写法才能先守住不超卖这条底线?
库存扣减首先要解决的是正确性,而不是把 QPS 做到最高。最容易踩坑的写法是先查询库存,再由应用层判断是否充足,最后执行扣减。这个过程包含两个独立步骤,两个请求可能在同一时间读到库存 1,随后都判断成功并完成扣减。
更稳妥的基础写法是把判断和扣减合并成一次带条件的更新:
UPDATE inventory SET available_stock = available_stock - :quantity WHERE sku_id = :sku_id AND available_stock >= :quantity;执行后通过受影响行数判断结果:受影响行数为 1,表示扣减成功;为 0,表示库存不足或目标记录不存在。这里的关键不是 SQL 看起来简短,而是让“库存是否充足”和“库存减法”由数据库在同一个原子操作中完成,避免应用层判断与数据库写入之间出现竞争窗口。
我在一次商品抢购压测中对比过两种实现:库存初始化为 10,使用 100 个并发请求,每个请求扣减 1 件。先查询再更新的版本虽然接口成功返回较快,但在并发日志和最终库存核对中出现了重复成功记录;改成条件更新后,数据库库存不会低于 0,但业务记录仍然需要额外做幂等控制。
实现方式主要风险适用判断 先查询再更新存在并发竞争窗口,可能超卖不建议用于扣减核心链路 带条件原子更新热点行仍可能产生锁等待适合作为单库阶段的正确性底线 缓存或队列前置扣减需要处理落库失败、重试和回补规模较大且具备补偿体系时采用 还要注意事务边界。
库存扣减、扣减流水和订单状态之间不能简单地假设“调用成功就一定全部成功”。如果库存更新成功后服务进程崩溃,后续订单记录可能没有写入,因此应使用业务幂等号、扣减流水、状态查询和补偿任务,把一次扣减变成可追踪、可恢复的业务过程。
我负责过一个商品数量并不算大的库存库,表里只有几百万条记录,数据库 CPU 也没有打满,但活动一开始接口延迟却突然升高。后来我怀疑瓶颈不在表的规模,而在少数热门 SKU 被大量请求同时更新;这种判断应该通过哪些指标验证,分段库存是否真的有用?
库存系统的瓶颈经常不是“表里有多少数据”,而是“同一时刻有多少请求争抢同一行”。一张包含数百万 SKU 的表,如果流量均匀分布,数据库可能运行平稳;反过来,只有一个热门 SKU 被持续扣减,也可能让单行锁竞争成为整个链路的上限。
我在一次热点压测中采用了两组流量模型:第一组让请求均匀访问 10 万个 SKU,第二组让 80% 的请求集中到 20 个热门 SKU。两组测试使用相同的应用实例和数据库配置,第二组的数据库整体 CPU 并未明显翻倍,但锁等待时间、P99 延迟和连接池占用都快速恶化。
这说明只看 CPU 和平均响应时间,很容易错过热点行问题。
观察指标热点竞争时的典型表现判断价值 锁等待时间明显上升,且集中在少数库存记录直接判断行级竞争 P95/P99 延迟尾延迟远高于平均值反映排队请求的真实体验 数据库 CPU可能只有中等水平不能单独作为扩容依据 连接池使用率长时间接近上限说明请求在等待数据库完成 当确认是热点行后,可以按风险逐步处理。
第一步是缩短事务,避免在持有库存锁期间调用支付、订单或营销服务;第二步是给展示库存和最终扣减分离路径,减少不必要的读写;第三步才考虑分段库存,把一个逻辑库存拆成多个可独立竞争的库存段。分段库存并不是简单把库存字段复制几份。
系统必须明确库存分配、扣减失败、订单取消回补和各分段汇总规则,否则会把一个数据库热点问题变成跨记录的一致性问题。我的判断标准是:如果热点流量短、可限流,优先排队和限流;如果热点持续且单行竞争已经成为稳定瓶颈,再评估分段库存或按资源分片。
我经常看到方案一遇到高并发就直接把库存放进缓存,再用消息队列异步落库,听起来吞吐量会更高。但我担心缓存扣成功、数据库落失败,或者消息重复消费后库存被扣两次;从技术负责人的角度,应该用哪些前置条件判断这类改造是否成熟?
缓存和消息队列能缓解压力,但它们本身不是库存正确性组件。缓存更适合承接库存展示、商品详情和热点读取;消息队列更适合削平瞬时峰值。最终扣减采用哪一个事实来源,必须在架构设计中明确,否则系统只是把数据库竞争转移成缓存、消息或补偿任务的复杂性。
在实际改造中,我通常先保留数据库作为最终库存事实来源,只把“可售库存展示”放到缓存。这样即使缓存短暂过期,影响通常是展示延迟,而不是直接造成扣减错误。缓存更新可以通过事务事件、可靠消息或定时校准完成,但不能把缓存读取结果直接当作最终扣减依据。
方案优势必须补齐的机制 数据库同步扣减链路直观,结果及时原子更新、幂等、热点治理 缓存预扣减后落库峰值吞吐潜力较高落库重试、回补、对账、故障切换 消息队列异步扣减可以吸收突发流量重复消费、积压、死信、状态查询 是否引入缓存前置扣减,我会先问五个问题:缓存扣减成功但服务宕机怎么办?
数据库落库失败如何重试?消息重复投递如何保证幂等?订单取消如何回补?每天如何核对库存、订单和扣减流水?只要其中一个问题没有明确答案,就不建议在核心链路上直接采用缓存预扣。消息队列也有一个容易被忽略的边界:它只能把瞬时流量变成排队流量,不能无限提升长期处理能力。
如果生产速度持续高于消费速度,消息积压最终会转化为用户等待、订单超时和库存状态延迟。因此,队列上线后必须同时监控消费速率、积压量、最大等待时间和失败重试次数。我的实施顺序通常是“先数据库原子扣减,再缓存展示,最后根据压测结果引入队列或缓存预扣”。
这样做虽然不一定是理论上最激进的方案,却更容易定位问题,也能避免团队在还没有幂等、对账和补偿能力时,过早承担分布式库存的维护成本。
我以前做压测时只关注平均响应时间和接口 QPS,结果上线后却在热点活动中遇到大量超时和数据库连接耗尽。现在我想建立一套更接近真实业务的评估方法,除了吞吐量,还应该验证哪些技术和业务指标,怎样确定一次压测是否真的通过?
库存系统的并发上限不能用单一 QPS 定义。真正有价值的容量结论应该同时满足三个条件:请求成功吞吐达到目标、尾延迟处在可接受范围、库存和订单数据经过核对没有业务错误。只要库存出现负数、重复扣减或成功订单没有库存流水,即使压测报告写着高 QPS,也不能认为系统具备可上线能力。
我会先把压测目标拆成业务指标和系统指标。业务指标包括成功扣减率、超卖数、重复扣减数、订单与库存差异数;系统指标包括 P95/P99 延迟、锁等待、慢 SQL、连接池使用率、数据库 I/O、应用线程池和消息积压。
指标类别建议关注项通过标准示例 吞吐每秒成功扣减数,而非仅请求数达到峰值目标且稳定运行 延迟平均值、P95、P99尾延迟不持续失控 数据库锁等待、慢 SQL、连接池无持续增长的排队和超时 正确性负库存、重复扣减、差异库存关键业务错误为零 异步链路消费速率、积压时间、重试量积压可在目标时间内恢复 压测流量不能只做均匀分布。
我会至少准备四种场景:热门 SKU 集中访问、突发流量、客户端重复提交、数据库或下游服务出现短暂抖动。以库存为 100 件的资源为例,可以让 1,000 个并发请求争抢同一 SKU,同时随机注入超时重试,验证幂等和锁竞争,而不是让每个请求访问不同商品来制造“看起来很漂亮”的吞吐量。
压测结束后要做业务账本核对。可以用下面的关系检查结果:初始库存 – 成功扣减总量 + 成功回补总量 = 当前库存。这个公式不能覆盖所有复杂状态,但足以快速发现扣减流水遗漏、重复消费和回补失败等问题。容量报告还应写清测试环境、数据库配置、并发模型、数据分布、事务耗时和瓶颈位置。
不要直接把某次测试得到的 5,000 QPS 宣称成系统固定能力;如果换成单热点 SKU、P99 延迟目标更严格,或者开启真实的订单写入,结果可能完全不同。技术负责人真正需要的不是一个最大数字,而是知道在什么流量形态和资源边界下,系统仍然能够正确工作。


读者评论
文章把库存并发问题拆成正确性、性能和扩展三个层次,比较符合生产实践。尤其是原子条件更新与业务幂等,确实应当先于缓存和队列建设。
对热点比例的强调很有价值。总QPS相同的情况下,流量是否集中到少数SKU,可能直接决定行锁等待和尾延迟,压测时不能只做随机分布。
文中对缓存和消息队列的边界说明比较客观。它们能缓解读压力或瞬时流量,但不能自动解决库存回补、重复消费和最终对账问题。
先查库存再更新的风险讲得很清楚。通过受影响行数判断扣减结果,同时配合幂等号和库存流水,才更容易处理超时重试等异常情况。
阶梯式改造思路较稳妥,不过文中的吞吐数据属于情景模拟,实际落地仍需结合数据库类型、热点分布、事务设计和压测结果评估。