财务主管跨界写技术文先交代缘由公司商城搞限时促销活动结束对账账面库存和实物对不上多卖了四十七件。排查一周病根是库存扣减没做并发控制两个请求同时读到同样的库存数各自扣减各自写回库存被重复扣漏扣。修复方案用的就是乐观锁。这篇用财务人能懂的方式讲清楚这个案子。技术细节是技术同事实现的我负责提需求、验账、复盘点子所以行文是财务视角的技术理解专业人士看了别嫌外行外行看了能明白这篇的目的就达到了。一、事故的来龙去脉1.1 一笔对不上的账促销次日盘库仓管报实物比系统少四十七件。财务的职业敏感告诉我这不是丢货丢货是散的、随机的这四十七件恰好集中在促销爆款上像是系统性的多卖。调订单流水核对某几个时间点的订单同一件商品被同时下单两个订单都显示成功但库存只扣了一次。一分钟内几十单挤在一起问题全出在这些密集时刻。把订单按秒聚合画成折线促销开抢那一分钟陡起一个尖峰四十七件的差异全部落在尖峰里。图一出方向就定了不是管理问题是系统问题。1.2 并发问题的直观理解用财务的账本打比方会计A和会计B同时翻开同一本账都看到余额一百件。A记一笔支出二十件写回账本余额八十B几乎同时记一笔支出三十件写回账本余额七十。两笔支出都记了但B写回时用的是他开头看到的一百A的账被覆盖了。这就是并发问题读的时候是一个世界写的时候已经换了人间。库存扣减天生就是并发重灾区促销一开抢几十个请求同时抢同一件商品的库存读写撞车概率极高。1.3 为什么之前没出事平日的订单量下两个请求恰好同时读写同一商品的概率很低问题潜伏了两年。促销把并发密度放大百倍潜伏的病一次性爆发。这给财务人的启示账能平不代表账对问题没暴露不代表问题不存在。内部控制学的这句话在技术系统里同样成立。技术同事后来跟我说这个问题他们叫竞态条件潜伏两年没发作是因为平日流量不配揭露它。我说我们管这叫内控缺陷名字不同教训相通缺陷不怕暴露怕的是环境一直温和。二、乐观锁怎么解决2.0 方案选型的三角讨论定方案那天的讨论值得记录。技术同事列了三个选项数据库排他锁、分布式锁、乐观锁。排他锁最稳但促销高峰会把请求排成长队分布式锁要引入新组件我们没人维护乐观锁改造成本最小代价是冲突时重试。我用财务的逻辑拍板超卖的损失是确定的重试的成本是弹性的选弹性代价的那个。会后证明这个选择对了。2.1 乐观锁的思路乐观锁不是真的锁是一个约定读数据时记下版本号写回前检查版本号变了没有。变了说明有人先改过自己这份作废重来没变写回并把版本号加一。还是账本的比方会计A翻开账本时看了眼页码标记是五记账后写回前再核对页码——还是五放心写入页码变成六。会计B也要写核对页码发现已是六说明有人动过自己开头看的数字作废重新翻一遍再记。谁都不锁账本但谁的账都不会被悄悄覆盖。这个方案妙在符合人性的懒绝大多数时刻没人跟你抢一次就成偶尔撞车才重试。不像悲观锁那样永远按排队的最坏情形设计乐观锁按合作的最好情形设计只给意外备了预案。2.2 落到库存表库存表加一个版本号字段扣减的更新语句这样写UPDATEinventorySETstockstock-:qty,versionversion1,updated_atNOW()WHEREsku:skuANDstock:qtyANDversion:version;关键在条件里的版本号只有库存和版本都还是自己读到的样子更新才生效。生效的返回一行无效的返回零行应用层据此决定重试还是告罄。这句条件更新是整个方案的钥匙判断和写入合并成一步中间没有缝隙谁也插不进来。财务审核讲究支付和复核不能同一人这里同理核对版本和写回数据必须是原子的一个动作拆开就又有缝可钻。2.3 重试的节奏版本冲突后怎么办重试重新读库存和版本号再试扣减。但重试要有节制最多三次每次间隔几十毫秒随机退避。三次都抢不到说明这个商品的竞争太激烈直接返回库存不足——用户体验上抢购失败远好于页面转圈十秒。这个参数我们拿促销流量压测校过三次重试加随机退避冲突下的最终成功率九成五以上剩余半成是真实的售罄。2.4 兜底的最终防线乐观锁保住了单条扣减的正确性财务还要一道总账防线每小时跑一次对账任务订单明细汇总的理论扣减和库存表的实际余额核对不平就告警。这是财务的老本行——明细账和总账必须勾稽。技术手段再好账实的定期核对不能省。上线乐观锁后这套对账再没报过警但它每天都跑我知道它在那心里就踏实。三、上线后的验证3.0 上线前的演练上线前我坚持做了一次财务式的验收技术同事写了个模拟脚本一百个虚拟买家同时抢十件库存商品跑完库存恰好为零、订单恰好十笔。我看着控制台的数字从一减到零那感觉像看着试算平衡表两头轧平。技术的正确和账的正确那一刻是同一件事。3.1 用数据验证修复修复后搞了一次复现测试模拟促销并发下单同一商品一千次库存恰好扣减一千份无超卖无漏卖。之后每次大促前都跑一遍这个压测成了惯例。财务的严谨用在这里刚刚好修复要用复现来验证验证要可重复。复现脚本留在了发布清单里库存逻辑每次改动都要先过这一关。财务的信条是凭证要能追溯技术的信条是结论要能复现合在一起就是每次变更都要有可重演的证据链。3.2 对账连续平稳乐观锁上线后的两次促销账实差异为零。财务月结时库存科目一次过再也不用促销后做库存差异调整凭证。技术在前面修路财务在后面收果这个案子让公司头一次体会到技术问题修到位财务的账自然平。这个案子还改变了公司对财务的定位以前技术出问题财务是事后算账的现在关键系统改造财务提前介入定验收标准。位置往前挪一步工作量没多多少踏实感多了几倍。四、几点体会4.00 跨界协作的心得这案子还有一层收获关于协作财务和技术最初鸡同鸭讲我说借贷不平他说并发冲突各说各话。转折点是我把差异商品清单按分钟拉出来给他看他把数据库日志的时间线拉出来给我看两边的时间线一对上问题瞬间清楚。跨专业沟通的桥不是术语是共同的事实。4.1 并发控制是内控的技术翻译学内控时讲不相容岗位分离、讲复核本质都是防人在流程里犯错。并发控制防的是机器在流程里犯错思路一脉相承不信任任何单点的读写用机制保证每一步可验证。财务人理解技术从内控的视角切入最快。后来我给部门同事做分享就按这个路子讲并发问题就是职责分离问题乐观锁就是复核机制对账任务就是总账勾稽。一组财务概念映射过去技术顿时不再遥远。4.2 乐观和悲观是两种性格技术方案里有乐观锁也有悲观锁悲观锁是排队制一个一个来安全但慢乐观锁是先干后验冲突了重来快但要多写重试逻辑。促销场景抢得多退得快乐观合适如果是转账这种绝对不能重来的操作悲观更稳。方案性格要贴场景性格这笔账和选供应商一个道理。4.3 财务要懂一点系统 内部机制这案子之后我给自己定了功课每月花两小时了解公司系统的关键机制。不求会写代码只求出问题时能和技术对话能问到点子上。财务数字的源头在系统里不懂源头对账永远是对结果打补丁。多卖四十七件的教训值就值在把财务和技术这两本账真正对到了一起。