[再次强调法则长存]订单交易设计须知
昀哥 2020年10月23日
一,开展详细设计之前请先把大的设计原则写下来
每一位设计师都需要知道这个常识:
当你开始构建或重构一个复杂系统的时候,请先把大的设计原则写下来,然后在这些设计原则的框架内做推演。
而不是这种常见的工作方式:
根据需求分析,天马行空,想到哪儿是哪儿,给出一个设计方案,自己做一番推演之后,自我感觉良好。
阿里巴巴资深技术专家毕玄这样总结自己的系统设计方法:
回顾了下自己做过的几个系统的设计,发现在自己在做系统设计的时候确实是会按照一个套路去做,这个套路就是:
系统设计的目的->系统设计的目标->围绕目标的核心设计->围绕核心设计形成的设计原则->各子系统、模块的详细设计。
二,订单与交易的设计原则,我们可以写在设计文档的前面,作为总纲
设计原则一:历史可追溯、可还原、有快照!
进一步说明如下:
互联网核心服务很容易产生数据不一致,一旦出现数据不一致,一定要有旁证来修正,这个旁证尽量不要是文件日志,否则会累煞人。
数据库中关键记录的关键字段,原则上不允许直接修改历史数据。这里的“直接修改”指的是,没有把变更行为记录到操作日志表里,而是直接在原始记录上 update 甚至 delete,这种“毁尸灭迹”是明文禁止的,即使留下了文件日志也是不允许的。
对此注意两点:
第一,修改关键记录的关键字段时,必须在操作日志表里保留变更日志(old value和new value),并记录发起人、操作人和发起系统,一定要确保历史可回溯、可还原、有快照。
第二,严禁对关键记录做物理删除,只能是软删除。
商品中心、订单中心、库存中心等中心服务要记录清楚商品核心属性、订单核心属性、库存的每一次变更,强调记录“过程”,而不是仅仅在“结果”上Update(即覆盖)。
比如说每一次库存增减、盘点(盘盈盘亏)等变更,在哪一个系统发起的,哪一个操作员发起的,因为什么原因变更(有变更类型),从什么变成了什么。这样记录下来之后,如果从某一天上线之后引入了BUG,就能够追溯所有变更,并能够彻底地还原数据。
比如说对于记录了订单信息的 order_info 表,会员如果点击使用账户余额支付了订单的应付金额,那么该订单操作日志表就会做如下记录,原订单记录的重要字段(what)由谁(who)因为什么(why)在什么时候(when)从什么变为了什么(how),都会详细记录。
设计原则二:接口从一开始就要做幂等性和防并发(这俩本质上是一回事)!
特别说明:所有与支付交易相关的订单,都必须有一个全局唯一的clientOrderID(或者叫客户端流水号),作为客户端提交时的幂等性依据。
为什么特别写这个说明,因为我们有兄弟公司的重复扣款事故,短短几个月内已经连续三次了,就是因为他们没有设计客户端流水号,无法利用客户端流水号在服务端、或者在定时任务里阻止重复提交或重复扣款:
a) 某日,扫脸后,收银员“手抖”致“双击”确认按钮(在弹出对话框之前),导致重复扣款;
b) 离线支付又出现重复交易;
c) 某日系统定时自动补扣时,补扣慢,现场运营人员担心用户欠费影响就餐,所以在商家中心后台点击了手动补扣,导致重复扣费;
三次重复扣款分别在三个业务逻辑上,其实背后是一个毛病。
设计原则三:要求必须建立校验订单交易数据一致性的定时任务,做数据补偿!
进一步说明:以数据库操作日志为依据,校验单个订单、交易的各项数据是否一致,如果发现不一致,第一要告警,第二尽量自动修复,比如说原路退返。
设计原则四:凡是收银系统最终肯定是要接入异地双活体系的,这是躲不过去的,因为单机房是不可靠的!
进一步说明:由于我司有异地双活机制,所以与订单操作、资金账户划转、券操作等敏感操作的设计都需要提前考虑双活,免得回头还要推倒重做。
比如说订单号、券号、卡号等序列号的生成规则里,都应该有机房ID,能看出来这个订单是在哪一个机房生成的。
举例:我们某团队利用Twitter SnowFlake算法生成如下规则的订单号:
25位 = 时间戳(13位)+机房ID(2位)+容器ID/IP(5位)+线程号(2位)+序列号(3位)
某团队的云小盒订单号规则为:
起始位固定2(1位)+日期(6位)+根据商户ID获取hashRoute(2位)+步长(7位)+机房ID(1位)
比如说事先划定“多活表”(如订单表和退款表)、“非多活表”(如队列表)和全局表(如支付配置表)。
设计原则五:数据库设计和变更必须提前与DBA沟通!
进一步说明:
大表(如订单表)上预留足够多(如10个)扩展字段,后期复用时改字段名即可,不影响业务,无需停服。
大表真的被迫加字段的话,请提前一天加好,不要与上线动作绑在一起。
大表加字段,选择凌晨执行,因为以5G的数据量为例可能耗时10分钟左右,对业务有致命影响。
异地双活里,对于双活数据库而言,有主机房概念,如果先在主机房的双活数据库上做表结构变更,会导致otter同步中断,所以请先变更从机房的双活数据库的表结构,然后再变更主机房的。
三,回顾订单与交易上容易发生的错误
经典错误一:高并发高性能的竞争环境,设计方案完全不同,大家一定要画时序图,而不是流程图。
进一步说明:分布式系统最常见的就是未能做到防重复提交和防并发提交。这是传统软件开发者转入互联网开发时最容易犯的错误。
做系统设计时,最好画业务时序图。
通过时序图的辅助,以及同事的设计评审,能帮助你想清楚你的业务依托哪些点上的原子级操作来阻止重复提交和并发提交。
经典错误二:主从延迟问题,是传统软件开发者转入互联网开发时最容易犯的第二个错误。
进一步说明:当读写分离成为常态时,低估了数据库主从延迟对代码的影响。
开发者特别容易进入如下陷阱:
代码A向主库写入一条记录,立即通知代码B;
代码B去从库查询,由于主从延迟(比如大于半秒),没查到记录;
于是代码B报错。
经典错误三:不了解线程安全
进一步说明:
《RCA:转换日期格式线程不安全导致支付时间错误》:SimpleDateFormat 类的 java doc 中明确指明该类非线程安全,多线程情况下需要同步处理或者每个线程创建一个实例。
《RCA:TaskMall线程不安全致死循环》:HashMap 是线程不安全的,如果多个线程对 HashMap 做 put 和遍历操作,就有可能出现 HashMap 中的 Entry 实例的 next=它自身,导致死循环,cpu 空转。
经典错误四:先删除旧值,再插入新值
进一步说明:刚删了旧值,服务就宕机了,导致新值也没插入。
大家可能会觉得这是废话,但总是有先删后插的程序员。
举例:当年商品手动排序页面交互中,
——当点击“提交排序”时,先把旧排序序位删掉,再把提交的新排序序位存入;
——当程序有BUG时,旧排序序位删掉了,但接下来程序爆出异常而中断,导致新排序未存入。
……
之前我写过《郑昀:技术人间自有法则在》,不妨对照着看看,再想想你的设计有没有遵循什么原则、什么法则,有没有脚踩西瓜皮滑到哪里算哪里?
-EOF-