用户连点两次、渠道超时重推、前端自己重试,同一个订单会到票务系统三次。会不会多出第二张票,不在于有没有加锁,在于幂等键取哪一列、命中之后返回什么、扣库存和落库谁先执行。
一、幂等键取哪一列。有外部订单号就用它,渠道重推时这个号不变,最稳。拿不到外部号的场景,用渠道号加票种加下单账号后四位再加取整到分钟的时间戳,看着能用,实际会漂:两次点击跨了一分钟,键就成两个。键要建唯一索引,落在单独一张表里,只留三列:键值、首次请求时间、首次结果的票单号。保留期跟渠道的重推窗口对齐,一般留七天,不要三天就清,第七天重推的那一笔正好落在空档上。
二、命中之后返回什么。不要回"重复提交"。上游应该拿到首次那笔的结果,同一个票单号、同一个码,让它自己结束重试。回错误最常见的后果是渠道继续重推并报警,一夜几百条假故障。命中要分两种处理:同键同内容,直接给首次结果;同键不同内容——数量、票种任一项不一致——拒绝,写进冲突待处理队列,人工判。这个队列每天清一次,攒着下次会被当成系统问题查一遍,白花半天。

三、锁在哪一层。防重检查放在读库存之前,不然两条同键请求都看见有余量,一起往下走。库存扣减和订单落库要在同一个事务里结束,中间不能夹一次外部调用。锁的粒度按键,不按渠道,按渠道加锁会让一个单量大的渠道把整条链路堵住。两条同键撞上来:一条拿锁执行,另一条等结果,不能绕开检查自己往下走。等待给三百毫秒以内,超了就返回"处理中",让调用方按固定间隔来查状态,别逼它立刻重发一次。
四、核销和退票同样要幂等。只给出票做防重,链子还是断的。核销的键用票号加核销次数序号,退票用票号加退票请求号,退票记录里单开一列存这个请求号,重发先查这一列。退票重发退了两次,是最难看出的一种错账:报表上只多一行红字,到对账那天才炸。所有撤销类动作一律要求幂等,重复请求返回首次结果,不返回第二次受理。

五、验收要真跑的三种重复。第一种前端双击:同一份提交连发两次,看订单表多几条,正确答案是零条。第二种渠道重推:把同一笔外部单号隔两分钟再推一次,看返回的是不是首次那个票单号。第三种超时重试:故意让第一次响应在三百毫秒后才到,看第二条是被挡在锁外还是挤了进去。跑完把唯一索引的冲突计数写进测试结论,冲突数应当等于重复请求数,少一条就说明真有一笔建了新单。
顺序不能颠倒:先给现有出票接口的键建唯一索引并把键值落库,再把命中后的返回改成首次结果,最后才调锁的等待时长。跳过第一步直接改返回,只是把重复出票换成重复单号,问题还在,只是看不见。
