☰
库存扣减的并发控制:乐观锁的应用
2026/10/14 19:10:15 网站建设 项目流程

财务主管跨界写技术文,先交代缘由:公司商城搞限时促销,活动结束对账,账面库存和实物对不上,多卖了四十七件。排查一周,病根是库存扣减没做并发控制,两个请求同时读到同样的库存数,各自扣减各自写回,库存被重复扣漏扣。修复方案用的就是乐观锁。这篇用财务人能懂的方式讲清楚这个案子。技术细节是技术同事实现的,我负责提需求、验账、复盘点子,所以行文是财务视角的技术理解,专业人士看了别嫌外行,外行看了能明白,这篇的目的就达到了。

一、事故的来龙去脉

1.1 一笔对不上的账

促销次日盘库,仓管报实物比系统少四十七件。财务的职业敏感告诉我这不是丢货:丢货是散的、随机的,这四十七件恰好集中在促销爆款上,像是系统性的多卖。

调订单流水核对:某几个时间点的订单,同一件商品被同时下单,两个订单都显示成功,但库存只扣了一次。一分钟内几十单挤在一起,问题全出在这些密集时刻。把订单按秒聚合画成折线,促销开抢那一分钟陡起一个尖峰,四十七件的差异全部落在尖峰里。图一出,方向就定了:不是管理问题,是系统问题。

1.2 并发问题的直观理解

用财务的账本打比方:会计A和会计B同时翻开同一本账,都看到余额一百件。A记一笔支出二十件,写回账本,余额八十;B几乎同时记一笔支出三十件,写回账本,余额七十。两笔支出都记了,但B写回时用的是他开头看到的一百,A的账被覆盖了。这就是并发问题:读的时候是一个世界,写的时候已经换了人间。

库存扣减天生就是并发重灾区:促销一开抢,几十个请求同时抢同一件商品的库存,读写撞车概率极高。

1.3 为什么之前没出事

平日的订单量下,两个请求恰好同时读写同一商品的概率很低,问题潜伏了两年。促销把并发密度放大百倍,潜伏的病一次性爆发。这给财务人的启示:账能平,不代表账对;问题没暴露,不代表问题不存在。内部控制学的这句话,在技术系统里同样成立。技术同事后来跟我说,这个问题他们叫竞态条件,潜伏两年没发作是因为平日流量不配揭露它。我说我们管这叫内控缺陷,名字不同,教训相通:缺陷不怕暴露,怕的是环境一直温和。

二、乐观锁怎么解决

2.0 方案选型的三角讨论

定方案那天的讨论值得记录。技术同事列了三个选项:数据库排他锁、分布式锁、乐观锁。排他锁最稳但促销高峰会把请求排成长队;分布式锁要引入新组件,我们没人维护;乐观锁改造成本最小,代价是冲突时重试。我用财务的逻辑拍板:超卖的损失是确定的,重试的成本是弹性的,选弹性代价的那个。会后证明这个选择对了。

2.1 乐观锁的思路

乐观锁不是真的锁,是一个约定:读数据时记下版本号,写回前检查版本号变了没有。变了,说明有人先改过,自己这份作废重来;没变,写回并把版本号加一。

还是账本的比方:会计A翻开账本时看了眼页码标记是五,记账后写回前再核对页码——还是五,放心写入,页码变成六。会计B也要写,核对页码发现已是六,说明有人动过,自己开头看的数字作废,重新翻一遍再记。谁都不锁账本,但谁的账都不会被悄悄覆盖。这个方案妙在符合人性的懒:绝大多数时刻没人跟你抢,一次就成;偶尔撞车才重试。不像悲观锁那样永远按排队的最坏情形设计,乐观锁按合作的最好情形设计,只给意外备了预案。

2.2 落到库存表

库存表加一个版本号字段,扣减的更新语句这样写:

UPDATEinventorySETstock=stock-:qty,version=version+1,updated_at=NOW()WHEREsku=:skuANDstock>=:qtyANDversion=:version;

关键在条件里的版本号:只有库存和版本都还是自己读到的样子,更新才生效。生效的返回一行,无效的返回零行,应用层据此决定重试还是告罄。这句条件更新是整个方案的钥匙:判断和写入合并成一步,中间没有缝隙,谁也插不进来。财务审核讲究支付和复核不能同一人,这里同理:核对版本和写回数据必须是原子的一个动作,拆开就又有缝可钻。

2.3 重试的节奏

版本冲突后怎么办?重试:重新读库存和版本号,再试扣减。但重试要有节制:最多三次,每次间隔几十毫秒随机退避。三次都抢不到,说明这个商品的竞争太激烈,直接返回库存不足——用户体验上,抢购失败远好于页面转圈十秒。

这个参数我们拿促销流量压测校过:三次重试加随机退避,冲突下的最终成功率九成五以上,剩余半成是真实的售罄。

2.4 兜底的最终防线

乐观锁保住了单条扣减的正确性,财务还要一道总账防线:每小时跑一次对账任务,订单明细汇总的理论扣减和库存表的实际余额核对,不平就告警。这是财务的老本行——明细账和总账必须勾稽。技术手段再好,账实的定期核对不能省。上线乐观锁后这套对账再没报过警,但它每天都跑,我知道它在那,心里就踏实。

三、上线后的验证

3.0 上线前的演练

上线前我坚持做了一次财务式的验收:技术同事写了个模拟脚本,一百个虚拟买家同时抢十件库存商品,跑完库存恰好为零、订单恰好十笔。我看着控制台的数字从一减到零,那感觉像看着试算平衡表两头轧平。技术的正确和账的正确,那一刻是同一件事。

3.1 用数据验证修复

修复后搞了一次复现测试:模拟促销并发下单同一商品一千次,库存恰好扣减一千份,无超卖无漏卖。之后每次大促前都跑一遍这个压测,成了惯例。财务的严谨用在这里刚刚好:修复要用复现来验证,验证要可重复。复现脚本留在了发布清单里,库存逻辑每次改动都要先过这一关。财务的信条是凭证要能追溯,技术的信条是结论要能复现,合在一起就是:每次变更都要有可重演的证据链。

3.2 对账连续平稳

乐观锁上线后的两次促销,账实差异为零。财务月结时库存科目一次过,再也不用促销后做库存差异调整凭证。技术在前面修路,财务在后面收果,这个案子让公司头一次体会到:技术问题修到位,财务的账自然平。这个案子还改变了公司对财务的定位:以前技术出问题,财务是事后算账的;现在关键系统改造,财务提前介入定验收标准。位置往前挪一步,工作量没多多少,踏实感多了几倍。

四、几点体会

4.00 跨界协作的心得

这案子还有一层收获关于协作:财务和技术最初鸡同鸭讲,我说借贷不平,他说并发冲突,各说各话。转折点是我把差异商品清单按分钟拉出来给他看,他把数据库日志的时间线拉出来给我看,两边的时间线一对上,问题瞬间清楚。跨专业沟通的桥不是术语,是共同的事实。

4.1 并发控制是内控的技术翻译

学内控时讲不相容岗位分离、讲复核,本质都是防人在流程里犯错。并发控制防的是机器在流程里犯错,思路一脉相承:不信任任何单点的读写,用机制保证每一步可验证。财务人理解技术,从内控的视角切入最快。后来我给部门同事做分享,就按这个路子讲:并发问题就是职责分离问题,乐观锁就是复核机制,对账任务就是总账勾稽。一组财务概念映射过去,技术顿时不再遥远。

4.2 乐观和悲观是两种性格

技术方案里有乐观锁也有悲观锁:悲观锁是排队制,一个一个来,安全但慢;乐观锁是先干后验,冲突了重来,快但要多写重试逻辑。促销场景抢得多退得快,乐观合适;如果是转账这种绝对不能重来的操作,悲观更稳。方案性格要贴场景性格,这笔账和选供应商一个道理。

4.3 财务要懂一点系统 内部机制

这案子之后我给自己定了功课:每月花两小时了解公司系统的关键机制。不求会写代码,只求出问题时能和技术对话,能问到点子上。财务数字的源头在系统里,不懂源头,对账永远是对结果打补丁。多卖四十七件的教训,值就值在把财务和技术这两本账,真正对到了一起。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询