机器已经加到三十二核,应用实例也扩了好几轮,库存还是会超卖,接口照样一片超时。开发的第一反应往往是并发代码写错了,数据库管理员的第一反应则是把锁加得更严。可把慢事务和锁等待展开看,现场经常不是“没人管住并发”,而是几千个请求都被管到了同一行库存上,排成一条很长的队。前一个事务不提交,后一个事务就不能改这行;应用机器再多,也只是让更多请求更快地赶到队尾。
热点库存是一处天然的串行点。只要所有请求都更新同一行,数据库最终就得一个一个处理,增加处理器、扩应用节点只能改善外围计算,不能消除这处串行化。 更麻烦的是,锁排队与超卖并不矛盾。超卖可能出在先查后改、条件更新遗漏、重试重复扣减、缓存与数据库错位,也可能发生在异常补偿阶段;超时则来自同一行上不断增长的等待。两种现象完全可能在同一场活动里一起出现。
本文的判断很明确:真正有效的办法不是盲目堆机器,而是缩短持锁时间,把热点拆成多个可并行的库存单元,在业务允许时用预扣或缓存承接前台请求,再把持久化改成可追踪的异步过程,并用幂等、流水和对账兜住异常。 这些做法不会凭空制造库存,也不会让一致性成本消失。它们做的是把原来集中在一行、一个事务、一个时间点上的压力拆开,让每一步都有明确状态、可观测指标和可执行的补偿动作。
典型现场不是数据库处理器跑满这么简单。入口请求量上来后,下单接口的响应时间开始拉长,应用连接池的活跃连接接近上限,等待获取连接的线程增加;数据库侧能看到更新同一库存记录的事务互相等待,最前面的事务迟迟没有提交。消息发送、订单流水、促销校验中的任意一个慢操作,只要还留在事务里,都会把后面整条等待队伍一起拖慢。
接口超时后,调用方通常会重试。用户连续点击、网关自动重试、任务补偿重放,都可能把同一笔业务再送一遍。如果扣减接口没有唯一业务单号,或者虽然带了单号却没有数据库唯一约束兜底,那么“第一次其实成功了,但响应没回来”的请求会在重试时再次扣减。现场看到的是库存少了两份,订单只有一份,于是被归因成并发写错;根因却是结果不确定时缺少幂等。
另一类超卖来自常见的“先查再改”。两个事务都读到可用库存,随后各自执行无条件更新。哪怕更新动作本身会按行排队,第二个事务拿到锁后仍可能照着旧判断继续扣,最终把库存扣成负数。正确做法不是依赖应用里的那次查询,而是让最终更新带上库存条件,例如仅在可用量足够时完成原子扣减,并以受影响行数判断成功与否。读取可以帮助展示,决定库存是否还能扣的那一刻必须落在受并发控制保护的原子动作上。
缓存参与后,症状会更乱。缓存里显示还有货,数据库里已经没有;或者缓存预减成功,落库消息却没有被消费;还有一种是缓存重启后从过期快照恢复,把已售库存重新放了出来。此时只盯数据库负数远远不够,要把业务单号、库存单元、占用状态、消息状态和库存流水串起来看,才能回答“哪一份库存被哪一单占住、是否已经确认、是否需要释放”。
排障时应先把时间线拼完整,而不是急着改参数。入口何时开始积压,哪类商品最先出现锁等待,事务在拿锁前还是拿锁后耗时,连接池是被执行中的请求占满还是被等待中的请求占满,消费端有没有堆积,对账差异从什么状态开始增加,这些信息能把问题迅速分成锁排队、重复扣减、状态竞态和异步丢失几类。处理器利用率低不代表系统有余量,热点行上的等待完全可以在低处理器占用下把链路拖垮。
现场止损也要分层。若仍在直接扣库,应限制热点商品进入扣减环节的并发,避免连接池被无限占用;若使用预扣,应暂停产生新占用并优先处理过期释放;若使用缓存预减,应确认消息是否持久化,不能在消息状态不明时直接把缓存补回。所谓止损,不是简单把开关关掉,而是先冻结会扩大差异的入口,再根据流水恢复真实口径。
关系型数据库对同一行执行更新时,持有该行写锁的事务没有结束,后来的更新只能等待。这是正常的一致性机制,不是数据库故障。假设所有订单都更新同一条商品库存记录,那么无论前端有多少应用节点,最终都会汇入这一行。数据库可以并行处理不同商品、不同库存桶,却不能让多个互相冲突的写操作同时改完同一行。
排队长度一旦增长,问题会从数据库向外扩散。等待锁的事务继续占着数据库连接,连接没有归还,应用线程开始等待连接;线程与请求堆积后,网关超时、上游重试、用户重复提交又制造更多请求。数据库本来只是一个热点行处理不过来,经过重试放大,逐渐演变成连接池耗尽、线程池积压、消息生产延迟和订单查询抖动的整条链路雪崩。
这里有个容易误判的现象:数据库处理器可能并不高。等待锁的事务大部分时间没有做计算,只是在等前一个事务释放资源。团队如果只看处理器、内存和磁盘,就会得出“数据库还很空,可以继续加流量”的错误结论。更有解释力的指标是行锁等待时间、等待事务数量、事务平均持锁时长、连接池占用时长,以及某个商品标识上的请求集中度。
把更新写成“可用库存大于等于本次扣减量才减”,可以让库存不足时安全失败,是直接扣库的基础动作。它解决的是检查与更新之间的竞态,却没有改变同一行必须串行执行的事实。活动规模较小时,这种方案简单、口径清楚,往往比引入缓存和复杂状态机更可靠;活动流量集中到单个热门商品时,它的上限则由这行记录的事务周转速度决定。
纯队列串行化也有类似特点。把同一商品的请求先写入队列,再由单个消费者依次处理,能够把数据库里的不可控排队搬到可观测的队列里,避免连接池直接承受峰值,还能明确拒绝和积压策略。但它仍然是串行,只是排队地点更合理。若业务要求即时返回,而且队列积压不能接受,最终还是要拆分库存单元,而不是把队列当成提高单行写入速度的魔法。
调小锁等待超时可以让请求更快失败,避免连接长期被占;调大则可能让更多请求等到成功。两者都不增加热点行的实际处理能力。把超时设得很长,应用表面失败率可能暂时下降,尾部响应却会明显恶化,连接池更容易被拖住;设得很短,如果上游不做退避与限次重试,又会形成高频重放。超时只能作为保护边界,不能替代入口限流、事务缩短和热点拆分。
重试同样要有节制。只有明确可重试的瞬态失败才应该重试,且必须建立在业务幂等之上。库存不足、活动已结束、订单状态已终结都不是重试理由。对锁冲突或暂时不可用的重试应当带退避,并限制次数;否则每个原始请求都会派生出更多竞争者,队尾越拖越长。
很多库存代码把“校验活动规则、读取用户资格、扣减库存、写订单流水、调用消息服务、更新营销统计”放在一个事务里,理由是希望所有动作一起成功。问题在于,事务一旦更新库存行并拿到写锁,后续任何一步变慢,锁都会继续被持有。远程调用的响应时间不受本地数据库控制,把它放在持锁区间内,相当于让热点行的通行速度取决于最慢的外部服务。
更合理的边界是:能在事务外完成且不依赖库存最终结果的校验提前做;事务内只保留必须原子完成的条件扣减、最小订单状态和库存流水写入;消息发送改为写本地待投递记录,由事务提交后的投递程序转发。这样既能保证“库存变更与待投递事件同时存在或同时不存在”,又不必在持锁期间等待网络消息服务。
这并不意味着所有查询都移出去。凡是决定本次扣减是否合法、且可能被并发修改的关键状态,需要通过条件更新、版本条件或受保护的状态转换来确认。活动文案、静态规则、商品展示信息可以提前读,真正的库存与订单终态不能只相信事务外的旧快照。缩短事务不是削弱正确性,而是把正确性集中到最少的共享状态上。
有些框架在方法入口就开启事务,随后做参数转换、权限查询和多次无关读取,直到方法结束才提交。即使库存锁是在后半段才拿到,数据库连接也可能从入口起就被占用;若中间还夹着较慢查询,整体并发承载会下降。事务应尽量靠近真正的数据变更开始,完成必要写入后立即提交,日志格式化、通知、统计汇总都应离开核心事务。
批量操作也要谨慎。把大量不同商品的扣减放入同一事务,看起来减少了提交次数,却会同时持有多行锁,增加交叉等待和回滚成本。库存场景更适合按明确业务边界提交,每笔请求可定位、可重放。需要批量落库时,可以在异步消费端按不会互相冲突的库存单元分组,并控制单批大小,避免一次事务包住过多热点记录。
事务提交前失败,数据库变更会回滚,调用方可以依据幂等规则重试;事务已提交但响应丢失,调用方重试时应返回原结果,不能再扣一次;事务提交成功但事件还没投递,应由待投递表继续发送;事件已经发送但消费者处理失败,则由消费重试与去重机制接管。把这几类失败混成一个“扣减异常”最危险,因为不同阶段需要的动作正好相反,有的该重试,有的只能查询确认,有的需要补发消息。
因此,每次库存操作都要能回答三个问题:业务单号是什么,数据库事务是否已经形成确定结果,对外事件推进到哪一步。只要这三个状态可查,故障恢复就不必靠猜。反过来,如果系统只有一条错误日志,没有持久化请求身份和库存流水,那么再完善的自动重试也可能把小故障放大成重复扣减。
库存分段会把一个商品总量拆到多个桶中,每个桶是一行独立记录。请求到来时按随机、轮询或稳定散列选择一个桶,扣减只锁定命中的那一行。原来所有请求争一把锁,现在分散到多把锁上,只要流量分布足够均匀,等待就能明显下降。这里真正提升并发的是冲突域被拆开,不是数据库突然获得了同时改一行的能力。
桶记录至少要能识别商品、活动、桶编号和可用量,并有防止重复创建的唯一约束。扣减仍应使用带余额条件的原子更新,不能因为分了桶就回到先查后改。库存流水还要记录命中的桶编号,这样发生差异时才能定位是哪一个桶、哪一笔业务造成,而不是只看到商品汇总不平。
桶数不能拍脑袋设一个很大的值。桶太少,热点仍集中;桶太多,初始化、聚合、回收、对账和索引维护都会变重,小库存商品还会出现大量空桶。稳妥做法是结合活动入口的并发集中度、单桶锁等待趋势、库存总量和数据库写入能力做压测,再决定初始分段数。分段数是容量设计参数,不是越多越安全。
当库存总量拆分后,随机或散列并不能保证每个桶以同样速度消耗。用户可能命中已经为空的桶,而其他桶仍有剩余。如果命中失败就直接返回售罄,会产生“局部售罄”;如果每次失败都扫描所有桶,又会把热点转成查询放大,甚至重新形成固定顺序抢同一个剩余桶的问题。
常见处理是给请求有限次数的换桶机会,每次按不同起点选择候选桶,仍失败再返回库存不足。候选信息可以在缓存中维护,但最终扣减结果以原子更新为准。不能为了找到最后一份库存无限遍历,活动尾段宁可把少量碎片转入专门的回收流程,也不要让每个请求都扫遍所有分段。
轮询能让命中更均匀,却需要维护轮询位置;稳定散列可以让同一用户较稳定地落到某个桶,有利于减少随机抖动,但某些用户群集中时也可能偏斜。实践中没有唯一算法,关键是观测每个桶的剩余量、锁等待和失败率,发现偏斜后能调整路由,而不是选定算法后长期不管。
再平衡的目标是把剩余量从冷桶调整到热桶,或更新路由权重,让新请求更多命中有余量的桶。直接在高峰期频繁搬库存会增加额外写入,还可能与扣减互相竞争。更稳妥的方式是只修改路由权重,减少真正的数据搬迁;确实需要转移时,以成对流水记录转出和转入,确保总量不变,并让对账能够识别这类内部调拨。
活动结束、订单取消或占用释放后,库存可能回到原桶,也可能进入统一回收桶。回原桶最容易对账,因为扣减与释放落在同一分段;统一回收便于把零散库存集中再售,但路由和汇总会更复杂。无论采用哪种方式,都要在占用记录里保存原桶信息,不能只保存商品总量,否则释放时无法证明库存回到了哪个口径。
展示库存不应在每次请求时实时汇总所有桶并锁住它们。页面上的“有货”可以来自异步聚合或缓存,最终成功与否由具体桶的条件扣减决定。财务和运营需要的准确汇总则从库存流水与桶余额生成,允许在展示链路与结算链路之间使用不同的新鲜度要求,但不能混用口径。
预扣也叫占用。下单时先把可用库存转为占用库存,支付或业务确认后再把占用转为正式出库;订单取消、支付失败或超过有效期,则把占用释放回可用库存。它的价值不只是削峰,还在于给每份暂时不可售的库存一个明确归属。只要占用记录能关联业务单号,运维就能查到“这份库存被哪一单占着”。
状态机至少要区分待占用、已占用、已确认、已释放和失败终态。状态转换必须带前置条件,例如只有已占用才能确认或释放,已经确认的订单不能再释放,已经释放的订单不能再次确认。用条件更新保证状态只从允许的前态转移,再配合业务单号唯一约束,可以处理支付回调、取消请求和超时任务并发到达的竞态。
预扣并不会自动提高数据库单行更新能力。如果所有占用仍然修改同一行库存,热点依旧存在。高流量活动通常把预扣与库存分段、缓存额度或队列结合:入口先在多个库存单元上取得占用,后续确认与释放按业务单号和原桶推进。预扣解决的是交易过程中的归属与可恢复性,分段解决的是同一时刻的锁冲突,两者关注点不同。
延迟消息适合在订单到期时触发释放,但消息可能延迟、重复或消费失败。若系统把它当成唯一机制,一次异常就可能让占用长期不归还。可靠做法是延迟触发加定时扫描双通道:消息负责及时处理,扫描负责发现超过有效期仍处于已占用状态的记录。两条通道都执行同一个幂等释放动作,重复到达只会看到状态已终结,不会重复加库存。
扫描也不能每次全表查过期订单。应围绕占用状态与到期时间建立可检索结构,按时间窗口分批领取任务,并避免多个扫描实例重复处理同一批记录。扫描程序只是发现候选,真正释放仍要校验订单状态、支付状态和占用前态。若支付系统的最终结果暂时不可确认,可以先把订单标成待核实,不能贸然释放后又确认出库。
在到期边界,支付回调和释放任务可能并发执行。两边都必须以状态条件争取唯一的合法转换:确认方尝试把已占用改为已确认,释放方尝试把已占用改为已释放。只有一个能成功。失败的一方读取最新状态并按业务规则处理,而不是无条件覆盖。这样不会出现库存已经释放给其他订单,原订单又被确认出库的双花。
业务上还要定义清楚“以支付平台时间为准,还是以本系统确认时间为准”。这是产品规则,不应藏在代码偶然的执行先后里。无论规则如何,决定结果的证据都要持久化,包括回调标识、订单状态版本、占用记录和操作流水,方便争议处理与对账。
预扣把库存保护得更严,也可能被大量未完成订单占住。需要观测占用未释放量、占用时长分布、确认率和释放率,并对同一账户、设备或业务主体的同时占用做合理限制。限制的目的不是替代风控,而是防止少量请求长期冻结可售库存。活动策略变化时,有效期也应经过容量验证,太长会压低实际售出速度,太短则会让正常支付在边界频繁竞争。
人工补单和客服改状态同样必须走状态机,不能直接改库存数字。任何绕过占用记录的手工操作,都会让“可用、占用、已出库”之间失去守恒关系。需要人工处理时,应生成带操作原因和审批信息的调整流水,再由统一库存服务执行,保持后续可对账。
缓存预减把热点库存放到更适合高频原子操作的缓存层。请求携带业务单号进入后,通过原子脚本完成去重、检查余额、扣减和生成待处理事件,前台很快得到“已受理”或“库存不足”。后台消费者读取持久化消息,再把结果批量或逐笔写入数据库。这样数据库不再同步承接每个入口请求,写入峰值被消息队列摊平。
最危险的缝隙是缓存扣了,但消息没有留下。如果扣减和消息写入是两个没有原子关系的动作,进程在中间崩溃,就会出现缓存少了一份,数据库永远不知道。设计时必须让预减结果与可恢复事件绑定:要么在同一缓存原子操作中写入可消费记录,要么使用具备可靠事务语义的消息路径,并保存可核查的发送状态。只承诺“代码下一行会发消息”不够,因为故障恰恰发生在两行之间。
前台返回语义也要说清。缓存预减成功通常只能表示“额度已受理”,并不等于数据库已经完成最终出库。若业务页面直接显示成交成功,后端却仍可能因订单非法、消息处理失败而回滚,用户体验和客服口径都会混乱。更稳妥的是把订单置为处理中或已占用,等持久化与关键校验完成后再进入确认状态。
消息系统普遍允许重复投递,消费者必须把重复当成正常情况。数据库侧以业务单号建立唯一索引,消费前后都能判断该库存事件是否已经处理。若消息重复,消费者返回已完成;若上次执行到一半失败,事务回滚后可以再次处理;若数据库已经提交但消费确认丢失,再次投递会被唯一约束挡住,不会重复扣减。
批量写入能降低提交开销,但要保留每条业务事件的结果。整批失败可以重试,部分失败则需要拆分并记录失败原因,不能只记一个“批量任务失败”后把成功项也重复执行。按库存分段或商品维度分组有助于减少互相冲突,但单批不能无限增大,避免长事务重新制造锁持有问题。
消费进度不能只存在进程内存里。系统重启后要知道哪些消息已经形成数据库结果,哪些仍需处理。消息位点、业务去重记录和库存流水共同提供恢复依据。队列积压时,入口是否继续接单也要有上限;如果积压已超过业务可接受窗口,继续在缓存里承诺更多订单,只会把实时故障变成更大的事后补偿。
缓存丢失后,数据库里的持久库存可能落后于尚未消费的预减事件。此时直接用数据库余额重建缓存,会把消息队列中还没落库的已售额度重新放出,造成二次销售。正确重建需要选择一致性基点:先冻结该商品的新请求,确认某个消息进度之前的事件都已落库,再以数据库余额减去基点之后仍在途的有效占用,生成缓存初值,随后恢复流量。
另一种思路是定期生成带消息位点的库存快照。恢复时加载快照,再重放快照之后的库存事件。无论采用哪种方式,库存值和事件进度必须成对出现,单独保存一个数字没有恢复意义。重建完成后还应对比数据库流水、缓存余额和消息未决量,差异没有归零前不要宣布恢复。
| 方案 | 能扛的并发量级(相对) | 主要代价 | 一致性风险 | 适合的活动形态 |
|---|---|---|---|---|
| 直接扣库(单事务) | 低,受热点行串行速度限制 | 实现简单,但锁等待和连接占用明显 | 条件更新可控,长事务会放大故障 | 流量平稳、单品不热、强同步返回 |
| 库存分段 | 中到高,取决于桶数与分布 | 桶不均、汇总、回收与再平衡复杂 | 局部售罄、调拨差异和口径错位 | 库存量较大、热点集中、允许聚合展示 |
| 预扣+超时释放 | 中到高,常与分段配合 | 订单状态机、到期任务和释放补偿 | 确认与释放竞态、占用长期不回收 | 下单后仍有支付或审核过程的活动 |
| 缓存预减+异步落库 | 高,取决于缓存、消息和消费能力 | 链路更长,恢复、补偿和运维要求高 | 缓存与库不一致、消息丢失或重复 | 瞬时流量强、可接受处理中状态 |
| 纯队列串行化 | 中,吞吐取决于消费者处理速度 | 排队可见但响应变慢,需要积压控制 | 重复消费、队列积压和结果回传延迟 | 宁可排队也不愿并发争锁的限量活动 |
这张表不是让团队直接挑“最高并发”的一项。方案越靠异步,一致性从同步事务转移到状态机、消息和对账,恢复成本也越高。若活动流量本就不集中,直接扣库配合短事务最省心;只有单行锁等待已经成为明确瓶颈,才值得引入分段。缓存预减适合能建设完整事件链路的团队,不适合只写一个扣缓存脚本就匆忙上线。
幂等不是在内存里记一个请求标识。库存扣减请求必须携带全局唯一且业务含义稳定的单号,同一业务重试始终使用同一个值,不能每重试一次就生成新号。入口去重可以减少重复工作,数据库唯一索引或去重表负责最终兜底,消息消费者、释放任务和人工补偿也都以同一单号识别操作。
幂等记录应保存处理结果,而不只是保存“见过这个号”。重复请求到达时,系统需要返回第一次的确定结果,例如已占用、库存不足、已确认或已释放。若记录只有一个布尔标记,进程在业务完成前后发生故障,调用方仍无法判断该继续重试还是查询状态。结果型幂等能把未知状态收敛为可查询状态。
唯一约束冲突也不能一律当成错误。它可能表示同一订单已经成功处理,此时应读取已有结果;也可能表示业务单号被错误复用,需要报警。区分依据是请求中的商品、数量、主体等关键字段是否与原记录一致。同号同参数可以返回原结果,同号不同参数属于严重的数据契约问题,不能悄悄覆盖。
一条可用的库存流水应能说明业务单号、商品、库存桶、动作类型、变化数量、动作前后状态、关联订单、来源事件和发生时间。预扣、确认、释放、入库、人工调整、分段调拨都应使用不同动作类型。流水不是为了堆日志,而是为了在余额异常时重新计算并追到具体业务。
余额表服务在线查询,流水表服务审计与恢复,两者不能只留一个。只存流水而每次实时汇总,热点查询和计算成本太高;只存余额而没有流水,出现差异后无法解释。写入时让余额变化与关键流水处于同一可靠边界,异步架构则让事件、落库结果与消费进度可相互核验。
库存守恒可以表达为:当前可用量与当前占用量、已确认出库量之间,应当和初始库存及后续入库、合法调整保持一致。分段场景要先按桶核对,再按商品聚合;缓存场景还要加入尚未落库的有效事件;预扣场景则必须把未释放占用单列。只拿缓存数字与数据库余额直接相减,很容易把在途状态误判为数据错误。
定时对账不应只输出一个差异总数。差异要分类为缓存已扣但数据库未落、数据库已有但缓存未反映、占用已过期未释放、重复流水、缺失流水、桶汇总不平、消息长期未决等,每类绑定明确处置。能自动证明安全的差异可以补偿,无法确认业务终态的差异先冻结相关库存并转人工核查,不能为了让数字相等而直接改余额。
发现不一致时,正确顺序是先止住新增差异,再恢复证据链,随后执行补偿。比如某个商品的缓存和数据库无法对齐,应暂停该商品新扣减,保留消息与流水,找出一致性基点,确认在途事件后再重建。边接单边改数看起来影响小,实际上会让差异不断移动,事后更难还原。
行锁等待时间能说明热点行是否正在排队,事务平均持锁时长能暴露事务边界是否过大,扣减失败率能显示库存不足与技术失败是否异常,占用未释放量能发现状态机卡住,对账差异量能评价异步链路是否可信。再配合连接池等待、消息积压、消费重试和各桶剩余分布,就能形成从入口到落库的完整视图。
这些指标要按商品、活动和库存桶拆分。全站平均值会把单个爆款的严重问题稀释掉。告警也不应只有固定阈值,可以关注等待趋势、占用增长后迟迟不回落、对账差异连续多个周期存在等变化。库存事故通常先出现结构性信号,等处理器跑满再报警已经太晚。
只压“库存充足时持续成功”无法验证真实活动。压测场景应覆盖大量请求争抢同一商品、多个桶分布不均、库存接近售罄、同一业务单号重复提交、支付确认与超时释放并发、消息重复投递、消费者暂停后恢复、缓存重建和数据库短暂不可用。每个场景都要检查最终守恒,而不只是看接口是否返回成功。
压测结果应回答容量边界在哪里:热点行等待从何时开始积累,连接池在何种积压下失去恢复能力,消息消费速度是否高于长期进入速度,分段后是否出现少数桶过热,停止流量后系统能否自行排空。不要给系统贴一个脱离商品集中度和事务模型的固定并发标签,同样的总请求量,平均分散到许多商品与集中到一个商品,结果完全不同。
故障演练还要验证“重启后不会多卖”。缓存、消息消费者、订单服务和数据库连接分别中断,恢复后核对业务单号数量、库存流水、可用量、占用量和确认量。任何需要临时登录数据库手工加减库存才能恢复的方案,都说明补偿路径没有真正建成。
热点库存治理归根结底是软件架构与数据正确性问题,基础设施能提供稳定承载,却不能替代幂等、状态机和对账。资源规划真正有价值的地方,是让应用入口、缓存与消息、数据库持久化互不争抢同一组处理器、内存和磁盘资源,并能分别观测容量。若所有组件挤在一台机器上,某一层突发占满资源,排查时很难分清究竟是锁等待、缓存抖动还是落库积压。
一万网络深耕 19 年(成立于 2007 年),做这类大促承载时通常会把应用、缓存和数据库拆成三档机器来配,而不是堆一台大的:热点问题的解法是拆分,配置上也要跟着拆,具体规格与价格以官网实时信息为准。
应用层更看重入口并发、连接管理与无状态扩展,缓存和消息层更看重内存稳定性、持久化路径与故障恢复,数据库层则要关注事务写入、日志和存储稳定性。三层分别部署后,容量调整才有针对性:应用入口拥塞就治理线程与连接,缓存命令变慢就检查热键和持久化,数据库锁等待上升就回到事务与分段设计,而不是把所有异常都归结成“机器不够”。
设备与网络方案只能为架构留出余量,不能承诺某个库存模型天然正确。采购或租用前应把组件角色、数据持久化要求、故障切换方式、备份恢复目标和压测计划写清,再根据实时业务需求询价。没有官网明示的规格与价格需实时询价,不能用单台报价推算整套架构成本,也不能把扩容当作掩盖单行串行化的办法。
比较基础设施方案时,真正该问的是数据库存储是否稳定、缓存持久化与重建需要什么条件、消息积压时磁盘和网络能否承受、故障切换后业务单号与库存流水是否完整。机器参数只决定承载边界,能否从异常中恢复,仍取决于应用有没有保存足够证据。典型部署思路,并非特指某一真实客户。
分段数应由热点集中度、库存总量、单桶锁等待、数据库写入能力和运维复杂度共同决定。先用接近真实的单品流量压测直接扣库,找到锁等待开始持续累积的位置,再逐步增加桶数,观察请求是否均匀、失败换桶次数是否上升、汇总与对账成本是否可接受。桶数过多会产生大量小余额和空桶,回收更麻烦,也会放大索引与初始化成本。比“一次定终身”更可靠的做法,是让桶路由支持增加新桶和调整权重,但避免在高峰期频繁搬动余额。
先判断失败发生在哪个阶段。原子扣减明确返回库存不足,不应重试,更不能补库存;事务提交前回滚,可以在幂等前提下有限重试;事务已经提交但响应丢失,应查询原业务单号并返回已有结果;缓存预减成功而消息未落库,要补发或恢复事件,不能直接把缓存加回;订单占用后业务取消,则执行一次带前态校验的释放。把所有异常都处理成“加回一份”会制造库存,把所有异常都处理成“再扣一次”则会重复扣减。
不能脱离架构阶段简单规定谁永远正确。若数据库是同步扣减的唯一真源,缓存只是展示副本,应以数据库流水重建缓存;若缓存承接预减且存在尚未消费的持久事件,数据库余额会暂时落后,必须把消息位点和在途事件一起纳入口径。判断依据应是可证明的一致性基点,而不是哪个数字看起来更大。无法确认时先暂停相关商品,保留事件和流水,完成对账后重建,切忌直接取两者中的较小值或较大值覆盖。
请求分布必须接近活动真实形态,刻意把大部分请求集中到同一热门商品,同时混入重复业务单号、库存不足、确认与释放竞态。持续观察行锁等待、事务持锁、连接池占用、各桶偏斜、消息积压和对账差异,并在压测过程中暂停消费者、重启缓存或制造短暂数据库不可用,验证恢复后总量仍守恒。只压均匀分散的商品、只看平均响应、压完不核对最终库存,几乎一定会高估系统能力。
没有脱离架构的统一比例。直接扣库要为锁等待抖动、连接池恢复和上游重试留余量;异步方案要同时核算入口接收能力、消息持久化能力、稳定消费速度和可接受积压窗口;分段方案还要考虑最热桶而不是只看平均桶。更实用的判断是让压测峰值高于业务计划峰值,并确认在故障注入后系统仍能排空且对账归零。若必须依赖长时间积压才能扛住,业务还要明确用户能否接受处理中状态。
不是。队列能把入口峰值变成可控积压,也能按商品键维持处理顺序,但消费者最终写数据库时仍要保证条件扣减和幂等。单消费者串行处理一个商品,确实可以降低锁冲突,却把吞吐上限转移到该消费者;需要更高处理能力时,仍可能按库存桶分区消费。队列解决排队地点和流量整形,分段解决冲突域,数据库条件更新负责最终防线,三者不能互相冒充。
只有差异原因明确、补偿方向唯一、重复执行安全时,才适合自动修复。例如已确认取消且占用仍未释放,可以走幂等释放;已落库事件缺少缓存反映,可以按确定流水重建。若差异涉及支付终态不明、消息是否提交不明、同号参数冲突或人工改数,脚本应先冻结相关库存并报警。对账的目标是恢复业务事实,不是把两列数字强行改成相等。任何自动修复都应产生新的补偿流水,保留原差异和处理依据。
热点库存治理最该拒绝的误区,是把所有问题都解释成机器不够。单行更新天然串行,处理器再多也不能让互相冲突的事务并行提交。小规模业务用短事务、条件更新和幂等直接扣库,简单可靠;锁等待已经持续累积时,把库存拆成多个桶;交易存在支付窗口时,引入可追踪的预扣与释放;瞬时流量远高于数据库落库能力,才考虑缓存预减和异步持久化。
方案越复杂,越不能省掉库存流水、唯一业务单号、状态条件、消息留痕和定时对账。所谓高并发正确性,不是高峰时接口返回得多快,而是重复请求、进程崩溃、消息重放、缓存重启和人工补偿都发生以后,系统仍能说明每一份库存去了哪里。把热点拆开,把落库异步化,把恢复证据留全,再用对账守住总量,这才是可控的库存扣减架构。
文中为通用工程实践分析,不涉及任何实测数据与客户案例;配置与价格一律需实时询价,具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品