去年夏天我们在冷库加了一组货架,深处那两排完全没信号。手持终端扫完码点提交,转两秒,弹一句"网络异常",操作工就以为没提交成功,再点一次。等到月底对账,有十一天的单据在系统里是两条,实际只出了一次货。
另一种更麻烦:点了提交,界面卡住,操作工以为失败,货照发,单没落库。这种单在系统里根本不存在,只能靠盘亏发现。那半年我们盘出过三次这样的差异,每次都要花两天回溯。
后来的做法是把提交改成"先落本地,再排队上传",终端不再依赖当场有网。这篇写这套机制的几个关键决定,以及一份能直接用的补传配置。
那次盘亏的起点是一箱价值不高的密封件,账上少四箱。查监控、查拣货记录、查交接本,折腾两天,发现是操作工在冷库深处出了一次货,终端提交失败,他没当回事。
这类单的特点是没有任何痕迹:服务端没收到,终端也没留。要查只能靠人回忆。所以补传机制的价值不只是"重试",而是先在本地留一份,哪怕传不上去,单子还在。
现场遇到的失败有三种。一种是明确失败:终端弹"网络异常",两秒内返回,操作工能看见。一种是超时:请求发出去了,服务端可能处理了,也可能没处理,终端等八秒没回应就报错。还有一种是假成功:终端收到响应,但服务端事务回滚了,返回体里的业务码是失败。
三种形态要分开处理。明确失败直接进队列等下一次;超时要带幂等键重发,让服务端自己判断是不是重复;假成功要看业务码,业务码是校验错误就不要再重试,重试一万次也是错。
起初我们的做法是弹窗上加个"重试"按钮。结果是操作工连点五次,服务端收到五条一样的请求,四条成功入库,库存多扣四倍。
重试的前提是幂等。没有幂等键的重试,等于把网络问题放大成数据问题。这个道理说起来简单,现场加一个按钮的时候很少有人先想清楚。
后面复盘发现,盲区比想象的多:货梯里、月台外沿、钢结构货架后面、地下卸货通道。这些位置不是完全没信号,是信号在两格和零格之间跳。
信号跳的时候比完全没信号更难处理。请求发出去一半断了,终端不知道服务端收没收到。所以判断"有没有网"不能靠信号格数,要靠一次真实的小请求探活。
顺序必须反过来:点击提交的瞬间先写本地库,写成功才提示"已暂存";然后再尝试上传,上传成功把本地记录标记为已同步。
本地库用手持终端自带的轻量数据库就够,一张表存单号、报文、创建时间、重试次数、状态。状态只有四个:待传、传输中、已同步、待人工。四个状态够用,多了现场看不懂。
单号必须在终端生成,不能等服务端返回。服务端生成的单号意味着没联网就没有单号,没单号就没法幂等,整条链路立不住。
我们的单号规则是终端编号加日期加当天流水,长度固定十八位。流水号存在本地,每次加一,重装应用会重置,所以要在单号里带上终端编号,避免两台终端撞号。
补传要按生成顺序来,不能并发。原因很实际:同一个物料的出库和退库如果乱序上传,库存会短暂出现负数,虽然终归一致,但中间那一刻的报表是错的,值班的人会来问。
我们的做法是单线程顺序补传,一次取二十条,传完再取下一批。二十条这个数不是拍的,是实测出来的:再大,一次失败要重传的量大;再小,请求开销占比高。
下面这份配置是终端侧的实际参数,字段名按我们自己的叫法,逻辑通用。重点看三块:队列的退避策略、提交的重试白名单、冲突的处理方式。
{
"offline": {
"enabled": true,
"storage": "sqlite",
"maxRecords": 5000,
"maxDays": 7,
"warnDepth": 500
},
"queue": {
"writeAhead": true,
"order": "bySeqNo",
"batchSize": 20,
"flushIntervalMs": 30000,
"maxRetry": 8,
"backoff": {
"baseMs": 2000,
"factor": 2,
"capMs": 300000,
"jitter": 0.3
}
},
"submit": {
"idempotencyKey": "clientDocNo",
"timeoutMs": 8000,
"retryOn": ["NETWORK", "TIMEOUT", "SERVER_5XX"],
"noRetryOn": ["VALIDATION", "AUTH", "DUPLICATE"],
"probeBeforeFlush": true
},
"conflict": {
"strategy": "serverWins",
"quarantineTable": "sync_conflict",
"onQtyMismatch": "review"
},
"observability": {
"logQueueDepth": true,
"uploadNetworkOnly": false
}
}几个字段值得单独说。maxRetry 设八次,配合指数退避,从两秒涨到三百秒封顶,八次大概覆盖四十分钟,正好是一个班次内的合理窗口;超过八次就转人工,不再无限重试。jitter 加三成随机,是为了避免二十台终端在同一秒一起重连,把接入端冲垮。
retryOn 和 noRetryOn 分开列,是关键的一刀。校验错误、鉴权失效、服务端明确说重复,这三类重试没有意义,重试只会把队列堵死。probeBeforeFlush 是补传前先发一个轻量探活请求,探不通就不启动整批,省掉二十次超时等待。
onQtyMismatch 设成 review 而不是自动覆盖,是因为库存这类数不能盲信终端。补传的单据如果发现当前库存不足以扣减,直接进隔离表,让人看,不自动改账。
补传的报文里带客户端时间,但这个时间只能当参考。操作工改过系统时间、终端电池拔过、时区设错,都会让时间失真。
真正用于排序的是终端本地的自增序号,单调递增,不依赖时钟。服务端收到之后按序号重排,再落库。时间字段只用来给人看,不用来做业务判断。
一条出库单里有几类字段:操作工填的数量、终端采集的条码、系统算的单价、库存扣减结果。前面两类以终端为准,因为只有现场知道;后面两类以服务端为准,因为终端拿到的是旧值。
这条界线要写进接口约定,不能靠默契。我们起初没写清,终端把单价一起传上来,服务端照着存,结果调价之后补传的单据还是老价格,对账对不平。
补传发生在几分钟到几小时之后,这段时间库存可能已经被别人动过。硬扣会出现负数,不扣又和实物不符。
我们的处理是加一道条件扣减:库存够就扣,不够就整单进隔离表,同时给终端回一条"待人工确认"。隔离表里的单子由库管当天处理,能调的调,不能调的走差异流程。
隔离表怕的是没人看。上线初期我们建了表,没建待办,一个月积了七十多条,等发现的时候已经过了追溯期。
现在的做法是隔离件进班组长的待办列表,每天早会过一遍,超过二十四小时未处理的自动升级到主管。技术上不复杂,难的是把它变成日常动作。
终端界面上,操作工只需要看三个提示:已上传、已暂存、待确认。不要给他看队列深度、重试次数、错误码,那些是给运维看的。
"已暂存"这个提示要做得明确,带一个小图标和条数,比如"已暂存 3 单,回到有信号的地方会自动上传"。含糊的提示会让操作工反复点提交,反而制造重复。
我们要求交接班时看一眼终端上的待传条数,写进交接本。这条规则很土,但有效:待传条数不清零,说明有问题,接班的人会去问。
这一条也是我们补传调度的一个兜底,队列积压超过阈值会推一条提醒给班组长,具体的定时调度放在搭贝的定时任务里跑,和补传队列的探活错开时间。这个提醒不解决技术问题,但能让问题在当天被发现。
冷库作业一个班八小时,终端开着扫码和定位,电量掉得快。电量到两成还必须能完成本地落库,这是硬要求,不能因为省电把写本地关掉。
存储也一样。五千条的上限是按一兆左右的报文估的,实际跑下来一条出库单平均一点二K,五千条六兆,终端的剩余空间够。上限到了之后先传,传不出去才提示清理,不要主动删未同步的数据。
改成离线优先之后,头一个月还有三张假成功没被识别,都是业务码判断漏了。补上业务码白名单之后,连续四个月没有出现无痕迹的丢单。
重复单也降下来了。以前一个月平均七条重复,现在幂等键在服务端挡着,重复提交要么被识别为已处理,要么直接返回原单号,不再产生第二条库存流水。
补传失败的提示还是偏技术。终端上写的是"同步失败,错误码 409",操作工看不懂,只能找班长。这一块我们后来改成中文短句加一个"找谁"的说明,但推送更新的那两周,现场抱怨不少。
技术方案能解决数据一致性,解决不了理解成本。这一条我记着,下次做终端功能,提示语要在设计阶段就写出来,别留到上线前补。
问:幂等键放在终端生成,两台终端撞号怎么办?
答:单号里必须带终端唯一编号。只靠日期加流水,两台终端的流水都从一往上加,撞号是必然的。服务端收到之后先查这个键,存在就返回原单结果,不再落库。如果业务上确实可能出现同号不同内容,说明单号规则设计有问题,不要靠服务端补救。
问:本地队列积压到上限,新单还能不能落库?
答:能,但要按策略丢弃旧数据中的哪一类必须提前定。我们的做法是优先保留未同步的作业单,先清已同步的历史记录,再清重复提交产生的冗余记录。绝不能按时间先进先出直接删,那样删掉的可能是唯一一份实物凭证。
问:补传顺序被打乱,库存出现负数怎么防?
答:两道。终端侧单线程顺序补传,保证同一台终端不乱序;服务端侧所有扣减都带条件,库存不足就整单转隔离,不做部分扣减。跨终端之间仍然可能乱序,所以服务端的条件扣减不能省,它才是兜底的那一道。
问:终端时间被改过,补传的先后怎么判断?
答:用本地自增序号,不用时间。序号由终端维护,单调递增,写库时和报文一起持久化,不随系统时间变化。服务端只按序号排序,时间字段仅作展示。如果终端被重装导致序号重置,靠单号里的终端编号加日期做二次分段。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。