《数据库存:仓储系统团队流程优化:高并发秒杀怎样减少锁等待严重》这个问题,最容易被误判成“数据库配置不够大”。我在排查仓储、库存和订单系统的大促故障时,反复遇到一种现象:数据库 CPU 只有六七成,磁盘也没有明显打满,但库存扣减接口的 P99 延迟已经从几十毫秒升到数秒,大量事务卡在等待状态。真正的瓶颈不是数据库不会执行,而是几千个请求同时争抢同一条库存记录,甚至还要等待一个包含远程调用的长事务提交。
因此,减少秒杀场景中的锁等待,不能从“把连接池调大”开始,也不能把“加缓存、上消息队列”当成万能答案。更可靠的顺序是:先确认等待对象,再找到持锁事务;随后缩短事务边界、优化库存更新条件,减少请求直接打到热点行;最后根据业务一致性要求,选择预扣库存、消息削峰、库存分片或链路解耦。技术方案之外,研发、测试、数据库、运维和业务团队还必须共用一套指标和故障流程。
普通高并发请求未必都会形成严重锁等待。真正危险的是,大量请求在极短时间内访问同一个可写资源。例如某个爆款 SKU 在一个仓库中只有一行可售库存记录,所有请求最终都执行同一条更新语句。此时,数据库即使拥有更多 CPU 和连接,也不能让同一行被多个事务同时安全修改。
可以把一次库存扣减抽象成一个竞争模型:请求数量不断增加,但可并行更新的库存记录数量没有增加。请求越多,等待队列越长;事务持锁时间越长,队列释放越慢。秒杀系统的关键不是让所有请求都进入数据库,而是让真正有机会成功的请求尽快完成,尽量阻止无效请求继续争抢热点资源。
我的判断是:如果等待事务集中在同一个 SKU、同一个仓库或同一个库存桶上,优先级最高的动作不是扩容数据库,而是降低热点集中度和持锁时长。
很多团队一发现锁等待,就直接引入缓存和消息队列。这样做有时有效,但也可能把一个可通过十几行代码解决的事务问题,升级成库存回补、消息重试、订单幂等和数据对账问题。
我更建议按照四层顺序处理:
这四层不是互相替代的关系。缓存不能修复错误的事务边界,消息队列不能自动解决重复消费,库存分片也不能替代幂等和补偿。每往外扩展一层,吞吐能力可能提升,但系统状态、故障类型和运维成本也会增加。

有些优化可以让接口变快,却让库存变得不可信。例如应用层先查询库存,再在稍后的事务中执行扣减;或者缓存层先把库存减掉,但数据库失败后没有可靠回补。这样的系统可能在监控上表现为延迟下降,却出现超卖、少卖、重复订单或库存长期对不上。
所以秒杀优化至少要同时观察三组结果:
只有三组指标都在可接受范围内,才能说优化真正有效。否则只是把故障从“接口超时”转移成了“事后对账困难”。
仓储系统里的库存通常不是一个简单的商品数量。实际扣减可能同时涉及商品、SKU、仓库、库区、批次、库位、库存状态和锁定状态。比如前台看到的是“某 SKU 还有 500 件”,但系统内部可能需要判断某仓库可售库存、某批次是否允许销售、是否已经被盘点锁定,以及库存是否被其他订单预占。
如果团队把所有库存都汇总在一条商品库存记录中,查询和维护看起来简单,但热点会非常集中。一个爆款商品的所有请求都更新同一行,锁等待很容易爆发。如果把库存拆到仓库或库存桶,竞争可能下降,但库存汇总、分配规则和回补逻辑会变复杂。
库存粒度不是越细越好,而是要与业务承诺相匹配。如果用户购买时必须指定仓库,按仓库拆分有明确价值;如果系统可以自动分配多个仓库,则拆分后必须增加库存路由和分配失败处理。
库存扣减并不只来自用户下单。仓储系统还可能有采购入库、移库、盘点调整、退货入库、订单取消回补和库存同步任务。平时这些写操作分散在不同时间,问题不明显;大促期间,秒杀扣减和批量同步恰好同时发生,就可能形成跨业务的锁竞争。
我曾经见过一种典型排查结果:秒杀接口本身的 SQL 只有一条更新,但阻塞它的持锁事务来自库存同步任务。同步任务一次处理数千条记录,事务迟迟不提交,前台请求就被迫等待。研发一开始只盯着秒杀代码,直到把阻塞链路和任务批次关联起来,才发现真正的持锁方在后台作业。
这说明锁等待不能只按接口排查,还要按“谁在写同一张表、同一条记录、同一个索引范围”排查。仓储团队若没有统一的写入登记和高峰期任务排班,数据库调优很容易陷入局部优化。
秒杀请求的目标通常是确认用户是否获得购买资格,而仓储系统的目标是完成拣货、分配、出库和配送。两者的时效要求不同,也不应该被强行放进一个同步事务中。
如果用户请求需要同步完成库存扣减、订单写入、仓库分配、波次生成、拣货任务创建甚至外部物流调用,那么库存锁的持有时间会被整条链路拖长。任何一个下游接口变慢,都会让前面的库存行继续被占用。
更合理的做法是把“是否占住库存”和“如何履约”拆开。前者需要快速、明确、可幂等;后者可以通过状态机和消息逐步推进。拆开之后,用户能够较快获得抢购结果,仓储任务也有独立的重试和补偿空间。

连接池解决的是“应用是否能拿到数据库连接”,锁等待解决的是“拿到连接后是否能获得目标资源”。如果 100 个请求争抢同一条库存记录,把连接池从 100 调到 300,通常不会让这条记录同时被 300 个事务安全更新。
相反,更多连接可能带来三个副作用:等待事务数量上升,数据库上下文切换增加,应用线程和连接被长时间占用。最终表现为接口线程池、连接池和数据库事务队列一起变满。
连接池应该根据数据库可承受并发、SQL 平均执行时间、事务持锁时间和应用实例数量共同计算,而不是单独追求一个更大的数字。增加连接前,必须先确认数据库当前瓶颈不是锁竞争。
索引确实可能减少扫描范围、缩短执行时间,也能避免更新操作影响过多记录。但如果所有请求本来就要更新同一条库存行,索引无法让这条行锁消失。
索引能够改善的是“找到目标记录的速度”,不能改变“目标记录只有一条”的事实。若 SQL 已经命中唯一索引,继续增加普通索引可能只会增加写入维护成本,甚至让更新更加复杂。
我的判断标准是:先看执行计划和实际锁对象,再决定索引。不要看到数据库慢,就把所有查询字段都建成索引。
“先查询可用库存,应用层判断大于零,再执行更新”在单线程测试中很直观,但并发下存在时间窗口。多个请求可能同时查到库存为 1,随后都进入扣减逻辑。如果没有正确的事务隔离和条件更新,系统就可能出现超卖或无效订单。
更安全的基础做法是让库存条件参与原子更新,并根据数据库返回的受影响行数判断结果。下面是抽象示例,字段和语法需要结合实际数据库版本调整:
UPDATE inventory SET available_quantity = available_quantity - 1, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE sku_id = ? AND warehouse_id = ? AND available_quantity > 0;
当受影响行数为 1,才说明本次扣减获得了库存;当受影响行数为 0,可能代表库存不足、记录不存在或条件没有命中。应用不能把“SQL 执行成功”误认为“库存扣减成功”。
消息队列能把突发请求转化为可控制的消费速度,但它不会自动提供库存一致性。消费者可能重复执行,消息可能重试,订单落库可能失败,库存扣减可能成功但确认消息发送失败。
因此,队列方案至少需要同时设计业务幂等键、消费状态、重试次数、死信处理、库存回补和对账任务。没有这些配套,队列只是把同步故障变成异步故障,问题发生得更晚,排查难度反而更高。

排查开始时,我不会先看平均响应时间。平均值会掩盖少数长事务,秒杀系统真正受影响的往往是 P99、超时率和等待队列长度。
可以先把问题拆成三个问题:
这三个问题经常同时出现,但治理顺序不同。先修复连接泄漏,再看锁;先确认 SQL 是否扫描过多,再判断是否需要架构分流。否则团队可能把连接池问题误认为数据库锁问题,也可能把锁竞争误认为慢查询。
锁等待分析最重要的信息不是“有多少事务在等”,而是“谁正在持有锁、为什么还没有提交”。等待者只是结果,持锁者才是根因候选。
排查时应记录以下信息:
不同数据库的系统视图和命令不同,不能直接套用一套查询语句。无论使用哪种数据库,团队都应该把“等待事务,持锁事务,业务请求”串成一条可追踪链路。只监控数据库层面的锁数量,不关联业务上下文,定位速度通常会很慢。
热点行是最直观的竞争对象,但范围扫描也可能扩大锁影响。更新条件如果没有命中合适索引,数据库可能扫描和锁定比预期更多的记录。尤其在事务隔离级别、范围条件和并发插入同时存在时,锁的范围可能比开发者想象得更大。
我通常会要求研发和数据库人员共同核对四件事:
需要强调的是,执行计划显示“使用索引”也不代表锁影响一定很小。还要结合实际扫描行数、事务持续时间和并发下的锁对象观察。
秒杀接口最危险的代码通常不是那条扣减 SQL,而是事务开始后还做了很多与扣库存无关的事情。例如先查价格、调用优惠服务、写营销日志、请求仓库分配、发送通知,最后才提交事务。
理想情况下,事务内只保留必须原子完成的动作。资格校验、商品展示、价格计算等读操作可以尽量放到事务外;远程调用必须放在事务外,或者通过本地状态和可靠消息进行衔接。
还要特别检查异常处理。有些代码捕获异常后返回了失败响应,但事务没有回滚;有些连接复用框架在异常路径下没有及时清理;有些重试机制在原事务尚未结束时再次发起扣减。它们都可能造成锁长期不释放或重复扣减。

库存扣减的基本目标不是“先知道库存,再决定是否更新”,而是让数据库在一次原子操作中同时完成条件判断和数量变更。应用只需要根据受影响行数判断成功或失败。
对于单件扣减,可以采用条件更新;对于一次购买多件,则应把扣减数量作为参数,并验证剩余库存不低于零:
UPDATE inventory SET available_quantity = available_quantity - ?, reserved_quantity = reserved_quantity + ?, updated_at = CURRENT_TIMESTAMP WHERE sku_id = ? AND warehouse_id = ? AND available_quantity >= ?;
这里有三个容易遗漏的细节。第一,扣减数量必须经过服务端校验,不能直接信任客户端参数。第二,受影响行数为 0 时,不能继续创建成功订单。第三,库存字段的更新方向要统一,避免某些流程扣减可售库存、另一些流程只修改锁定库存,最终造成口径混乱。
一个实用的事务边界通常只包含库存预占和订单意向记录,具体范围要看业务规则。不要为了追求“一次事务解决所有问题”,把仓库分配、通知、日志、外部接口全部塞进去。
可以把流程拆成以下阶段:
这种拆分的代价是出现短暂的中间状态,例如库存已预占但订单尚未完成。对此必须设计明确状态和补偿机制,而不是为了避免中间状态,把所有步骤都放进一个长事务。
这是我在性能排查中最常见、也最容易修复的一类问题。远程接口平均只需要 80 毫秒,看起来不长,但在高并发下,80 毫秒乘以大量事务,会把热点库存行变成排队资源。
如果远程服务偶发超时,事务持锁时间可能从 80 毫秒变成数秒。后续请求会继续堆积,最终表现为数据库连接池耗尽、接口超时和线程池阻塞。
如果业务必须确认某个外部结果,应先在事务内记录待处理状态并提交,再由异步消费者调用外部服务。只有在业务确实要求强一致且无法拆分时,才考虑同步调用,并为超时、重试和回滚设置明确边界。
库存同步、盘点、对账和批量回补任务不应该在秒杀高峰期以无限并发运行。批量任务的单次事务越大,锁覆盖时间越长;并发线程越多,和前台请求争抢资源的概率越高。
改造时可以从低成本措施开始:拆小批次、限制并发、错峰执行、降低重试频率、将非核心写入延后。如果数据库支持资源隔离或读写分离,也可以进一步拆分,但必须确认库存写入仍然只有一个权威写入路径。

秒杀开始后,最先到达系统的请求中,可能有重复点击、重复提交、库存已售罄、用户不符合资格和活动尚未开始等大量无效请求。让这些请求全部进入数据库,是对热点库存行的主动放大。
前置拦截可以包括活动状态缓存、用户参与标记、商品售罄标记、接口限流和请求签名校验。前置层的目标不是承担最终库存账本,而是尽可能减少无效请求向后传递。
对于用户重复提交,必须使用业务幂等键,例如用户、活动、SKU 和购买批次的组合键。幂等判断不能只依赖前端按钮禁用,因为网络重试、代理重试和客户端重复请求仍然可能发生。
缓存预扣适合处理极端突发流量,但它会引入缓存和数据库之间的状态差异。团队在采用之前,必须先回答:缓存扣减成功后,数据库扣减失败怎么办;数据库扣减成功后,确认消息丢失怎么办;服务重启后,缓存中的预扣库存如何恢复。
一个较稳妥的思路是把缓存层定位为“快速资格筛选和流量闸门”,把数据库或库存服务定位为最终确认源。缓存成功不一定直接向用户承诺订单完成,可以返回排队中或预占成功状态,由后续可靠流程完成确认。
如果业务必须让用户立即看到成功结果,就要明确这个“成功”究竟是库存预占成功,还是订单已创建成功。两个状态不能使用同一个含义,否则下游失败时很难向用户解释。
队列的价值不是让消费者无限加速,而是把数据库能够承受的处理能力显式化。假设库存数据库每秒稳定处理 3000 次扣减,消费者就不应该为了清空队列而瞬间启动 100 个并发线程,把数据库重新打回锁等待状态。
消费端需要根据数据库锁等待、事务耗时、连接池使用率和消息积压动态调整并发。消息积压并不必然是故障,关键是用户端是否能够看到明确状态,运营端是否知道预计处理时间,系统是否有超时释放和失败补偿。
消费逻辑必须幂等。可以通过业务唯一键、消费记录表、订单状态条件更新或版本号控制重复执行。幂等不是简单地“重复请求不报错”,而是重复执行不会再次扣库存、重复创建订单或重复生成仓储任务。
当单个爆款商品的请求集中度极高,且 SQL、事务和前置拦截已经优化,单行锁仍然成为主要瓶颈时,才考虑库存分片。基本思路是把一个商品的可售量拆成多个逻辑桶,请求先被分配到某个桶,再对桶记录进行扣减。
但库存桶不是简单地把一行改成十行。系统还要处理桶选择、桶耗尽、库存回补、最终汇总、订单取消和故障恢复。如果消费者按桶分配不均,可能出现部分桶已经售罄、其他桶仍有库存,但用户无法获得库存的情况。
因此,库存分片更适合具备较强压测、监控和数据修复能力的团队。中小团队如果只是偶发大促,优先使用限流、前置拦截和短事务,通常比直接引入库存桶更稳妥。

很多线上问题不是因为工程师不知道锁,而是设计评审没有明确谁对库存最终结果负责。库存可能由订单服务、库存服务、仓储服务和缓存脚本共同修改,出了差异后每个团队都认为自己只是中间环节。
在需求或技术评审中,至少要明确以下内容:
这些内容如果不在设计阶段写清楚,往往会在大促故障时临时争论。临时争论本身会延长恢复时间,也可能导致多人同时修改库存数据,进一步扩大数据差异。
平均分布压测很容易掩盖热点问题。假设压测脚本把请求均匀分到 10000 个 SKU,每个 SKU 的并发都不高,数据库表现可能非常好;但真实秒杀往往是少数商品贡献绝大多数请求。
我建议至少设计五类压测场景:
压测结果不能只提交吞吐量,还应保留锁等待样本、阻塞链路、事务持锁时间和库存对账结果。没有这些证据,团队很难判断优化是减少了竞争,还是只是让失败请求更早返回。
“锁等待超过阈值”对业务人员的解释价值有限。更有用的告警应该包含库存表、SKU、仓库、阻塞事务、所属服务、发布时间和当前影响的订单数量。
数据库团队可以和应用团队共同建立以下关联:
这样做的价值在于,故障发生后可以直接回答“哪类商品、哪个服务、哪次发布、哪项后台任务造成了阻塞”,而不是让多个团队分别截图、复制日志,再人工拼接时间线。
重启服务可能释放连接和事务,但不一定修复库存与订单之间的差异。若缓存已经预扣、数据库扣减成功、消息却没有消费,单纯重启只能让问题继续积压。
完整的预案应包括:

下面的案例采用脱敏示例场景,数据为情景模拟,不对应某个公开项目的线上结果。系统是一套面向多仓库发货的订单与仓储系统,商品库存按 SKU 和仓库保存,秒杀活动集中在一个爆款 SKU。数据库库存表中,该 SKU 在主仓库只有一条可售库存记录。
活动开始后,入口请求峰值约为 18000 次/秒,其中绝大多数请求集中到同一个 SKU。原有流程包括用户限购查询、价格校验、库存查询、库存扣减、订单写入、仓库分配和消息发送。库存扣减事务从开始到提交的平均时间约为 180 毫秒,尾部请求因下游响应变慢,持锁时间达到 1 秒以上。
这里的数字是为了展示排查方法的情景模拟。实际项目中,必须以数据库锁监控、应用链路追踪和压测报告为准,不能直接套用这些数值作为容量承诺。
压测初期,数据库 CPU 约 62%,磁盘 IO 没有明显饱和,应用实例的连接池使用率却持续上升。库存扣减 SQL 在无竞争环境下平均执行时间只有 8 毫秒,一旦进入热点压测,P95 上升到 260 毫秒,P99 超过 1 秒。
这个对比很关键:SQL 本身并不慢,慢的是它等待其他事务释放库存记录。此时如果只看慢 SQL 排名,容易得出“库存更新语句执行效率差”的错误结论。
继续追踪事务边界后发现,库存行更新完成后,代码还会同步调用仓库分配服务,并将订单消息发送到消息系统,最后才提交事务。仓库分配服务的平均响应时间并不高,但高峰期偶发抖动,导致部分事务在等待外部响应时继续持有库存锁。
这解释了为什么数据库 CPU 没有打满:大量连接并没有持续执行计算,而是在等待锁或等待外部服务。系统的有效处理能力被最慢的同步环节限制,新增连接只会把更多请求放入等待队列。
改造没有一开始就引入库存分片,而是先完成四项低成本调整:
调整后,情景压测中的平均持锁时间从 180 毫秒降至 24 毫秒,库存扣减 P95 从 260 毫秒降至 48 毫秒,阻塞事务峰值从 1260 个降至 210 个。订单最终状态的处理时间并没有全部消失,而是从同步等待变成了异步推进。
这组数据最值得注意的地方不是“延迟下降了多少”,而是远程调用次数没有减少。系统只是把远程调用移出了库存锁的保护范围,使数据库不再为外部服务的抖动买单。
异步化之后,系统新增了“库存已预占、订单意向待确认”的中间状态。消费者可能失败,消息可能延迟,用户可能在状态未最终确认前关闭页面。因此,前端不能把预占成功直接展示成已完成订单,后台也不能把所有异常都交给人工。
团队随后补充了预占超时释放、订单意向查询、消息重试、死信处理和每日库存对账。这样做的结果是,系统不再依赖一个长事务维持所有一致性,而是通过状态机和补偿机制维持业务可恢复性。

这类系统通常不需要立即做库存分片。建议先完成数据库和事务层治理:使用原子扣减、建立正确索引、缩短事务、移除远程调用、增加用户幂等,并对后台批任务限速。
如果活动规模不大,但流量具有明显突发性,可以增加入口限流和活动状态缓存。缓存主要用于拦截活动未开始、商品售罄和重复请求,不必一开始就让缓存承担最终库存账本。
团队应优先投入可观测性。至少要能够看到库存扣减 P95、锁等待数量、长事务、连接池占用、订单创建失败和库存对账差异。没有监控证据时,复杂架构通常只会增加排查成本。
这类系统可以考虑消息削峰和库存预占,但要先统一状态定义。建议明确区分“请求已接收”“库存预占成功”“订单已创建”“仓储任务已生成”和“履约完成”。
消费者并发不能只按机器数量配置,还要以数据库事务耗时和锁等待为反馈。可以从较低并发开始压测,逐步增加消费者数量,观察吞吐是否继续增长;如果吞吐不变而锁等待上升,就说明数据库热点已经成为限制因素。
对于多个仓库,库存路由应尽量在库存扣减前确定。不要先扣一个总库存,再在下游随机分配仓库,否则可能出现总库存足够但实际仓库不可履约的状态。
如果单个商品的请求在短时间内达到数万甚至更高,并且绝大多数请求都争抢少量库存记录,单行更新即使已经足够短,也可能仍然成为瓶颈。这时可以评估库存桶、预分配库存、排队令牌或独立秒杀库存服务。
但架构升级前必须先完成基线压测。至少需要知道:单条库存记录每秒能承受多少次成功或失败判断;失败请求是否能够快速返回;消费者在不同并发下的稳定吞吐是多少;库存回补和对账每天需要处理多少差异。
极端热点商品还可以采用资格筛选或令牌机制,让只有获得令牌的请求进入库存确认。令牌发放必须具备有效期、用户幂等和失效处理,否则只是把数据库竞争转移到了另一个不透明的资源池。
对于医药、食品批次、贵重物品或强监管业务,性能优化不能牺牲库存可追溯性。建议保留库存流水、订单流水和状态变更记录,并让所有扣减、释放和回补都能够通过业务编号关联。
缓存预扣和异步队列可以使用,但最终一定要有权威账本和对账机制。对账不是活动结束后才做,而应在活动过程中持续检查预占超时、订单缺失、库存负数和重复流水。
如果库存数量有限,且用户更在意“马上知道是否失败”,可以采用较严格的限流和快速失败策略。相比让数万请求长时间等待,这种方案能减少连接占用、降低锁等待,并让用户体验更可预测。
快速失败需要配合清晰提示、重试限制和客户端幂等。否则用户看到失败后不断重试,系统又会重新形成流量回流。接口层可以返回明确的库存不足、活动繁忙、排队中和请求重复状态,避免所有情况都返回模糊的系统繁忙。

原子条件更新是最应该优先采用的基础方案。它的优点是实现简单、数据库语义清晰,能够避免明显的先查后改窗口。缺点是热点商品仍然会形成行竞争,库存越集中,单行吞吐上限越明显。
它适合库存记录粒度合理、并发规模中等、业务希望保持同步确认的系统。即使未来要使用缓存或队列,原子条件更新仍应作为最终落库层的安全底线。
乐观锁通常通过版本号或更新时间判断记录是否被其他请求修改。它可以让冲突请求快速失败,但如果失败后不断重试,数据库仍然会承受大量竞争。
乐观锁适合冲突可接受、失败可以快速返回的场景,不适合无限重试的秒杀扣库存。重试必须设置次数、退避时间和总时限,并且每次重试都要受到整体流量控制。
悲观锁可以在事务内明确锁住记录,适合必须串行化处理的复杂库存逻辑。但它对事务边界非常敏感,事务一旦包含远程调用或复杂计算,等待会迅速扩大。
如果使用悲观锁,必须保证锁定后尽快完成,避免在锁内执行不可控操作。还要设置锁等待超时和故障告警,防止一个异常事务长期阻塞整个热点。
缓存预扣的最大收益是把大量请求挡在数据库之前,并提供更高的突发处理能力。它的最大风险是状态一致性:缓存扣减成功不代表数据库一定成功,服务崩溃后还可能出现预扣库存无法恢复。
采用缓存预扣时,应提前设计库存流水、可靠消息、失败回补、超时释放和定期对账。若团队没有维护这些机制的能力,缓存方案的表面吞吐收益可能不值得其长期复杂度。
消息队列能够削峰、隔离下游服务和提高系统弹性,但它会引入延迟和中间状态。用户不能再简单地把 HTTP 请求返回视为订单最终完成,产品和客服流程也要理解排队中、确认失败和超时释放的含义。
队列方案适合业务允许异步确认、团队具备消息治理能力的系统。如果订单必须在请求内同步返回最终结果,队列仍可用于非核心履约流程,但不应强行把库存确认全部异步化。
库存分片可以缓解单行热点,但会增加分配、汇总、回补和对账成本。分片数量也不是越多越好,过多的桶会带来空桶、分配不均和管理复杂度。
我通常把库存分片视为极端热点的专项方案,而不是普通仓储系统的默认架构。只有当数据证明单行竞争确实是主要瓶颈,并且团队能够承受状态治理成本时,才值得采用。

“压 1 万并发”本身没有足够信息。团队还应说明请求在多少个商品之间分布、库存数量是多少、单用户是否重复、成功和失败比例如何、后台是否同时运行任务,以及下游服务是否存在延迟。
建议至少记录以下输入参数:
只有把这些输入固定下来,优化前后的结果才具有可比性。否则一次压测使用平均分布,另一次压测使用单热点分布,吞吐差异不能说明方案真的更好。
第一组使用现有方案,作为基线。第二组只修改 SQL 和事务边界,观察数据库内部优化带来的收益。第三组增加限流、缓存或消息削峰,观察流量层改造是否进一步减少数据库压力。
如果第三组吞吐提升明显,但库存对账差异也增加,就不能只报告吞吐结果。每组实验都应保留成功订单、失败订单、重复订单、库存流水和最终库存快照。
对于库存分片等复杂方案,还需要增加故障实验:某个库存桶不可用、消费者重复消费、消息延迟、数据库短暂不可写、回补任务失败。系统在正常情况下变快并不等于具备可恢复性。
技术指标可以由数据库团队提供,但业务阈值需要产品、仓储和运营共同确认。例如库存扣减 P99 可以接受多少毫秒,排队状态最多持续多久,预占超时后多久释放,订单和库存允许存在多长时间的暂时差异。
如果这些阈值没有事先约定,活动中出现异常时,团队很难判断应该继续放量、降低消费者并发还是立即停止活动。阈值本质上是故障决策规则,不能只由单个工程师临时判断。

当锁等待已经严重影响接口时,第一步通常不是立刻杀掉所有数据库连接,而是确认阻塞事务是否属于异常长事务。若持锁者是正在执行的关键库存操作,盲目终止可能造成事务回滚和更大范围的重试风暴。
更稳妥的处理顺序是:

如果系统已经出现锁等待,第一批动作应该尽量小而确定:找到持锁事务,移除事务内远程调用,改成原子条件扣减,检查受影响行数,修复索引和异常回滚,限制后台批任务,补齐锁等待监控。
这些措施通常不需要重做整个系统,却能直接减少持锁时间和错误请求。完成后再进行单热点压测,确认剩下的瓶颈是否仍然集中在同一库存行。
当数据库内部已经优化,但无效请求数量仍然远大于可售库存,入口层就需要承担筛选职责。缓存适合做活动状态、售罄标记、用户限购和快速拦截;队列适合把订单确认和仓储履约从突发流量中隔离出来。
引入这些组件的前提,是团队能够处理异步状态、重复消费、消息积压、库存回补和对账。若这些能力还不成熟,先做限流和快速失败,可能比直接上完整异步架构更符合风险收益比。
只有当压测和线上监控都证明:单个库存记录的锁竞争是主要瓶颈,且短事务、前置拦截和队列削峰已经无法满足目标,才考虑库存桶或库存分片。
库存分片应当配套桶路由、库存流水、回补、状态校验和对账工具。否则系统虽然能够承受更高的瞬时请求,却会把复杂度转移到库存准确性和故障恢复上。
高并发秒杀不是某条 SQL 的单点优化,而是一次对团队流程的压力测试。研发需要明确事务和幂等,测试需要模拟真实热点,数据库团队需要提供阻塞链路,运维需要准备限流与数据修复,业务团队需要定义成功、排队和失败的产品语义。
我最看重的判断标准不是系统能否在压测报告里跑出一个漂亮的峰值,而是它能否在库存扣减失败、消息重复、下游超时和服务重启之后,仍然解释每一件库存去了哪里。
下一步可以按“等待对象,持锁事务,事务边界,热点分布,一致性补偿”的顺序做一次专项检查。先拿到锁等待和库存对账基线,再分别验证 SQL 优化、事务缩短、流量拦截和异步削峰的收益。这样做,既能减少无效架构投入,也能避免为了追求吞吐量而牺牲仓储系统最重要的库存准确性。


读者评论
文章对锁等待的分析比较到位,尤其是指出连接池扩容不能解决热点行竞争,这一点在秒杀场景中很容易被忽略。原子扣减和受影响行数校验也具有较强的实践价值。
从仓储业务角度看,库存同步、盘点和退货等后台任务同样可能成为持锁方,文章没有只盯着前台接口,排查思路比较全面。不过库存分片后的回补和对账成本还需要结合实际系统评估。
文章提出先优化 SQL 和事务,再考虑缓存、队列及库存分片,顺序比较稳妥。性能、业务正确性和恢复能力同时验收的观点值得借鉴,但具体方案仍需通过压测验证隔离级别和幂等设计。