比如这篇《平安付资损防控设计白皮书》就很专业而全面地介绍了资损防控的方方面面,作者:平安付架构委员会,版本:1.0。虽然发布已经有一段时间,但里面很多思路现在仍然是实用的,值得收藏,在需要的时候随手翻出来参考。
下面是原文,未做删减,只优化了排版以适配在线阅读,版权归原作者所有。有些内容已经过时,请自行甄别。
1. 什么是资损
广义的资损定义:任何由于系统故障、缺陷、人为操作、安全漏洞导致支付公司或支付公司的客户蒙受直接或间接损失的事件,都属于资损事件。
狭义的资损定义:由于支付公司的原因,导致支付公司产生直接资金损失,或产生赔付。
2. 资损产生的原因
由于支付行业的特殊性与复杂性(主要处理资金相关业务) ,支付公司处于资损的风口浪尖,最容易发生资损,可以说资损风险无处不在。常规来说,资损原因主要可以分为以下三类:
系统方面的原因。主要由系统分析、设计与实现处理错误等原因导致的资损。
产品设计运营以及使用方面的原因导致的资损。
信息安全方面的原因导致的资损。
本文档主要从系统分析、设计、开发人员角度分析第一方面的资损产生原因。
3. 资损防控定义
公司业务一旦发生资损,既涉及相关的经济损失,又会涉及大量的社会影响;因此资损是公司的高压线。资损防控与资金安全是产品与系统设计、实现、运营的第一原则。简单来说,资损防控就是从资损发生的前、中、后三个阶段做好相关的防、控、管等各项工作。
防:主要指的是防范资损发生。在资损发生之前,从产品与系统设计、实现、运营等来源,对资损进行分类预防进而避免资损的发生。
从流程上讲,对于涉及到资金相关的项目需求:
启动阶段要求需求方提供资金清结算与核对方案;
需求阶段要求明确资金正常流程与异常流程;
系分阶段要产出资损点分析、制订完善的防控措施;
测试阶段除进行信息流的分析验证,还要进行资金流的分析与验证;
发布评审时要再次进行资损风险评估,各防控措施是否落实到位;
验证时,产品与业务方需要对资金流进行完整的验证才算验证结束。
控:主要指的是控制资损。一旦资损,我们需要知道哪里资损了?资损了多少?及时进行止损,将资损规模降到最低。
监控与核对是“控”常用的两大技术手段:
系统与产品需要制定相关的系统、业务监控指标,及时进行资损风险的提前预警;资金上通过证证、证账、账账、账实等核对手段进行资金流操作结果的核对。
资损发生后,我们需要及时资损。这一阶段时间与速度最为重要,根据经验,在资损发生的几个小时内,止损的成功率较高,损失几乎可以全部追回。如果是已经识别的资损故障,可以根据已经制定的应急预案进行按部就班执行即可;未识别的资损故障,需要及时汇报给应急小组,以最高效的沟通手段制定应急方案,快速处理。
管:指的是资损的管控。在资损处理结束之后,对资损故障进行详细的复盘与分析。排除类似或相关联的缺陷与隐患,制定并实施改进计划;对相关经验进行分析与沉淀,避免重复犯错。
4. 系统设计资损防控规范
从系统架构层面整体来看, 支付公司的系统可以抽象为如下结构:
对外部商户提供收单服务类的系统
连通支付公司与各金融渠道的网关类系统
支付公司的内部业务处理系统
消息、调度等中间件系统
数据库、缓存等存储
从系统架构与业务架构上来讲,各个结构连接的地方最容易出现资损。因此我们将从接口服务层面与系统设计层面对资损进行分析并总结相关规范。
5. 服务接口类设计规范
按照上述系统抽象架构所示,支付公司接口服务可以分为三类服务:
渠道网关类服务
收单类服务
内部系统接口服务
下面按照各类服务风险又高到底进行分类介绍。
5.1. 渠道网关类服务
5.1.1. 常见资损风险
渠道网关是连通支付公司与各金融渠道的网关类应用,处于资金处理的最后一个环节。渠道网关类服务是最容易发生资损的服务。
(一) 返回码的处理。
返回码的处理是渠道端最容易出现资损的部分。
我方系统会对渠道的返回码做映射,当映射错误,或渠道返回系统异常等未知错误我方处理不合适,或我方系统流水勾兑异常等情况发生时,容易造成资损。如我们在和渠道方无特殊约定的情况下,将无此交易处理成失败就会导致资损。
(二) 调用渠道使用的流水号产生重复。
如果流水号重复时候,渠道报一个流水重复,我们置为处理中;当我方调单查询的时候,查到了被重复的上一笔交易,状态勾兑为成功,就会发生资损。
常见的重复有如下场景:
1、流水号设计不合理发生重复(如商户号+8位sequence,一年后开始重复;日期+6 位流水号,交易量超百万,一天开始重复);
2、流水号使用不合理发生重复(我方不同业务,渠道同一接口,使用不同 sequence 出现重复; 同一业务的不同交易之间使用不同 sequence,导致重复)
3、升级迁移导致重复(生成规则升级,结果和历史数据重复;业务迁移拆分时,新建了 sequence,和原有的重复)
(三) 调用渠道方流水号的产生与使用机制不合理。
如我方在报文发送时生成流水号或对流水号做二次处理均可能导致渠道方自身的幂等失效。
(四) 渠道服务接口中金额单位的处理。
内部金额单位和外部系统金额单位不一致,转换出问题。内部单位为忽(*1000000),渠道接口单位为分、为份额,漏转换或转换错误;币种参数传递错误,导致对方按错误币种处理;交互接口金额格式特殊,转换出错(如银行部分接口前补 0)。
(五) 渠道日切的处理。
由于银行有日切问题,金融渠道也有交易日日切等问题。当涉及跨日问题时,处理不当(如查询用自然日期而非约定的渠道交易日期),容易导致资损。
此外,日切处理不当还会导致清结算差错处理的问题。
(六) 不同金融机构对接细节尤其关键的幂等控制细节不清楚。
不同的渠道存在着不同的幂等性控制,想当然的去进行渠道对接容易出现幂等性问题进而资损。
(七) 和渠道对接,未严格遵从渠道端的信息安全规范。
如果报文未签名、加密或安全级别不够,或使用了不合适的第三方jar 包,可能导致信息被篡改,或导致其他争议。
(八) 渠道网关接口连错环境
由于渠道端测试联通测试不方便,经常需要修改代码进行测试、或者我方测试环境连对方生产环境;如果代码提交生产或者测试队生产进行了错误的交易,极易导致资损。
(九) 调用渠道接口先发后至、后发先至和响应乱序。
由于我方和渠道费连接的网络环境或网络连通方式比较复杂,极易导致先发后至、后发先至和响应乱序等情况。
比如渠道方是反交易比正交易先到、查询请求比交易先到,处理不到就会造成资损;极端情况下,还可能出现响应乱序情况,即我方发出的A请求,收到的 B 请求的结果。
5.1.2. 渠道网关类服务使用规范
(一) 返回码的处理是渠道处理的重中之重,必须慎重。
需要遵从如下规范:
1、返回码必须要按接口维度进行成功、失败、未知分类。返回码变更必须慎重,原则上必须禁止通过线上SQL 变更。
2、返回码在业务上线时候或变更上线的时候一定要验证支付交易、查询、交易、冲正类交易的成功、失败、处理中情况。
3、明确返回结果“成功”字段背后的含义,是操作成功,还是业务成功,还是两者双成功。返回结果真正的“成功”只有一个,可以对“成功”做硬编码处理。
4、渠道返回系统内部异常、超时、无此交易、流水重复、我方系统流水勾兑异常等不代表交易失败,不能明确确认成功失败,需设置结果为未知。
5、流出类交易慎设置为失败,流入类交易慎设置为成功。
(二) 规范渠道流水号的生成和使用。
1、各渠道网关系统需要采用规范的流水号生成规则,避免灵活的生成方案。
2、新渠道、新业务或有迁移发生的时候,需要对sequence使用方式专项评估,设置合理的初始值。
3、流水号的产生必须在受理或预处理阶段提前生成,之后不允许再重新生成或更新任何流水号(包括对格式进行转换);
绝对禁止在报文发送环节进行渠道流水号的生成。
4、在发送报文给渠道的时候时,需要考虑第三方 jar 包会有重发的默认设置(如 httpclinet 默认是重发 3 次),我方需要慎重选择重发策略;对方无明确的幂等说明的时候,严格禁止设置重发。
(三) 时间规范。
统一使用数据库时间,禁止自定义日期处理函数。
业务处理发起尽可能避开对方可能的日切,跨日,跨月,跨年时间段;如果渠道方跨日业务需要特殊处理,涉及时需要进行特殊的分析设计。
部分金融交易类渠道在接入的时候,尽可能使对方接受以我方时间为准。
(四) 金额单位规范。
目前出口网关金额以分为单位为主,分、元、份额都有,要将金额转换放在出口系统,内部需要保持一致的金额单位;金额的计算过程,对原始金额数据不能更新,计算得出的结果作为新的金额字段存储。新业务新渠道上线一定要验证支付交易内部记账结果和实际变动相符(可以去渠道查询):如银行渠道入金,要保证内部记账金额和银行卡扣款金额一致。
(五) 设计上要考虑因先发后至或后发先至等情况。
1、查询交易和反向交易处理的时候,要控制和原交易间隔一定的安全时间;要先关联原交易,关联不到原交易需要保证返回处理结果后,原交易不会被执行(可以插入幂等表以避免原交易被执行)。
2、要控制渠道等外部调用及数据库访问等的超时时间,避免资源的长时间堵塞,造成后到的交易先处理。
3、对于消息、调度等,要从意识上认为其没有任何次序,从设计上考虑保证防重以及和先后次序无关。
(六) 必须严格遵守渠道方的加密签名等安全规范。
(七) 测试和生产环境必须严格隔离。
源头上必须禁止为了测试需要修改代码(必须修改时候,要将测试相关的信息外化配置,和产线隔离),war 包中不能有临时性的测试解决方案。
测试环境如果要连接到渠道方的生产环境的话,需要特殊申请并且一定要指明防火墙的失效日期。
5.2. 收单类服务
5.2.1. 常见资损风险
(一) 服务描述不清楚、不规范
对于商户接口类服务,服务接口、数据交互描述不清楚或不规范,导致商户错用接口,进而导致商户资损。
(二) 金额参数单位乱用
在收单服务接口的所有与出入参数里,金额单位的约定是极易出错的地方,分、元、忽的使用一定要明确,否则会导致金额扩大或缩小100、1000000倍。
(三) 幂等参数不明确、不合理
收单服务的幂等性控制参数一定要明确指出,否则容易造成商户:接口调用时,对同一笔业务进行多次服务调用,导致业务重复处理,造成资损;支付公司结果通知(同步或异步)时,商户没有幂等处理,对同一笔也处理多次导致资损。
(四) 接口参数长度或类型不明确
收单服务对外出入参数中,各参数的类型长度约定不明确,将引发数据库超长、数据转换与截断等问题,严重时将导致资损。
(五) 请求参数合法性校验不严密或未校验
对外提供的接口,没有对参数进行合法性校验或缺乏明确的校验规则。如收到外部请求,传入未签约的支付方式,没有签订手续费费率,可能导致漏收手续费。
(六) 接口的错误码或返回码定义不明确
错误码、返回码对于异常、超时、幂等等结果不明确,会导致商户重复针对同一笔业务进行多次调用或将可以判定为成功的业务而判断为失败。
另外,外部响应结果如果包含多层状态(通讯状态、业务状态等),需要明确业务状态是由哪个状态或者哪几个状态组合决定,判断错误会导致严重资损。
5.2.2. 收单服务接口设计规范
(一) 接口描述规范。
接口服务能力描述简洁明了,对于服务能力、功能、数据交互有一个清晰的描述。
(二) 金额单位规范。
金额参数必须以“元”为单位,同时精度要求小数点后两位。禁止以分或忽为单位(线下收单行业规范可以以分为单位)。
(三) 幂等控制。
需要约定好幂等控制的参数是哪个参数(如外部交易单号)或哪些参数(如交易日期+交易流水号)组成,对于幂等控制的方式双方需要达成一致。
(四) 收单服务的参数类型与长度必须明确且设计合理。
任何一个参数都要明确其类型与数据长度范围,该范围需要确保在整个体系内有效。
另外,对于关键的控制类参数(如幂等控制的参数)需要设计为字符串,减少不必要的转换。
(五) 对于商户的接口权限与接口参数做好权限控制与合法性校验。
(六) 对于错误码或返回码需要清晰定义。
返回的结果中需要明确成功失败状态的含义:业务成功,操作成功,双状态。明确错误码的含义,准确识别出成功、失败、处理中、超时处理、重复处理、不做任何等重要处理结果;对于各种异常处理(如超时重发等),不能缺少。
(七) 对外服务接口规范需要遵从最新的“文档规范说明.docx”。
5.3. 内部系统接口服务
平安付内部系统间默认使用 DUBBO 作为分布式服务框架,进行系统间的RPC 调用。
5.3.1. 常见资损风险
(一) 服务接口中,各参数类型、长度等相关约定不清楚,会导致数据库超长、被截断等隐藏问题,严重时候会导致资损。
(二) 服务接口提供方在应用层面,对服务的被使用情况不清楚,导致服务调用方使用错误的接口或组装错误的逻辑。
(三) 服务接口中,幂等性参数未说明或未正确设置,导致同一笔业务重复调用被服务方重复执行成功,可能导致资损。
(四) 服务接口中,金额参数单位未按照规范进行使用,造成金额被扩大或缩小100 倍或1000000 倍,造成资损。
(五) 返回码对于重复、超时、异常等结果说明不清晰,请求方错误判定业务状态导致错误的业务逻辑处理,或者更换幂等参数后错误的对同一笔业务进行重复调用。
(六) 关键性且有时效性的业务处理,只依赖于关联系统的消息通知且无其他的补救措施。由于消息的异步性,到达时间是不可靠的,会影响业务的处理。
(七) 服务接口设计的过程中,没有考虑向前、向后的兼容,以及版本发布过程中的兼容性处理问题。如服务使用方使用老服务落单,发布后使用了新服务支付,新老服务不兼容,有可能使应有的校验失效,导致资损。
(八) 服务接口需要序列化的对象子类和父类具有共同属性的时候(Dubbo使用Hessian 做序列化与反序列化),会导致属性值丢失。
(九) 对于涉及文件处理与批量处理的服务接口,缺乏相关明细数据的说明及相关的校验机制。由于内部系统均使用 NAS 等进行文件共享,接口中传递的只是文件路径;没有进行过相关的加密,没有文件正确性校验功能,可能会导致数据丢失。
5.3.2. 内部服务接口设计规范
(一) 接口定义需要完整、清晰、规范。
1、接口里面需要详细的定义好出入参的类型、长度、选必填以及相关取值等约定;服务使用方在使用前需要对输入做好相关校验。
2、服务提供方接口里需要详细描述接口的功能以及对业务和其他接口的影响,涉及资金流向的关键的接口还需要提供资金流向说明。
3、服务使用方使用接口前需要判断接口是否符合需求(功能、信息流、资金流等)、以及自身如何利用服务方接口完成自身的业务处理。
4、由于序列化问题,façade 接口中的对象规避子类和父类具有共同属性问题。
(二) 接口中的金额参数使用忽(*1000000)为单位。
内部系统的金额参数不要被更新,如果有更新等需求,使用新的字段。计算的时候使用 Money 类。
对于金融产品,有时候有收益的计算(对收益的计算处理方式不尽相同,如“四舍五入”):那么该产品收益计算的需求首先要明确,并根据需求进行代码开发,并且重点关注临界值的计算。
(三) 幂等参数。
接口描述必须对幂等参数做着重说明, 服务提供方需要使用服务使用方的幂等参数来做幂等控制。
(四) 对关键性且有时效性的业务来做处理时候,不要只使用消息来进行通讯。
原消息的生产者可以使用 RPC 对消费者进行调用以获取明确的响应,消费者异步进行任务处理;必须用消息解耦的时候,需要增加监控以及手工触发相关运维功能以保证业务的处理。
(五) 内部接口涉及交易处理的,服务端不管有无幂等控制,不设置默认重发。
(六) 接口的兼容性处理。
1、接口在设计过程中,要避免无兼容性设计。
2、在非破坏性变更的时候,既需要保证向前向后的兼容性,也需要保证发布过程中的兼容性(包括接口字段和各种业务处理) 。
(七) 返回码(错误码)的处理。
1、要明确每种返回的结果码的含义和场景,包括成功、失败、系统超时、系统异常、重复请求等重要场景;
2、需要明确返回结果成功的的含义,是操作成功,还是业21
务成功,还是双成功;
3、返回的错误编码要包含三位的内部系统编码,且要约定好每种错误场景的业务处理。
(八) 接口中涉及文件及批量场景的处理。
1、内部系统间接口中如果有文件内容的传递,接口说明中明确相关文件内容,并需要对关键信息进行校验,防止文件内容不完整或者错误(可以在接口中约定传递文件的MD5值)。
2、文件和批量的明细内容需要对没条数据做清楚的说明:包括是否每笔有自己的唯一流水、是否用其做幂等控制;对文件和批量的整体需要说明是否存在接受部分成功、部分失败的情况。
6. 系统服务功能设计规范
在分布式的环境中,一个系统的设计可以抽象为下图:
除掉可用性问题外,导致资损的问题可以归结为如下三类:
幂等控制问题
兼容性问题
并发互斥问题
6.1. 幂等性控制设计
幂等性问题是分布式系统设计中最常见的问题,是分布式系统服务的一种承诺, 承诺只要调用接口成功, 外部无论何种原因的多次调用对系统的影响是一致的。
在支付公司,我们常说的幂等性控制往往不是重复访问的只读结果一样,更多的是指业务活动、资源变更等重复请求,同一个服务操作实例最多只允许执行一次。幂等性控制包括两个层级:
1、最基础的幂等性控制需要完成唯一性控制。主要根据幂等性(唯一性)控制字段即可完成。
即:根据约定的幂等控制字段(唯一性字段),对业务重复提交、请求方重复请求、定时任务、网络原因等导致的重发的数据来判断是否重复提交。如收单系统根据商户号+日期+外部订单号,拒绝同一个商户在同一天内重复发送同一个外部订单号的交易请求。
2、在唯一性控制的基础上,如果要做更复杂的幂等性控制;这时候,需要和其他业务要素一起来进行判断。
如果全部关键业务数据请求和现有的数据一致,可以使得请求一次和重复请求多次都不会对业务处理产生影响。如在收单系统中,如果外部商户请求的“商户号+日期+外部订单号”与现有交易中的数据一致,并且对其他关键字段金额、收付款方等做比对;如果比对一致可以返回前面处理的结果,不一致可以提示商户“请求参数与原先参数不一致,请检查”。
幂等性未做控制或控制不合理是系统设计中最容易产生资损风险的环节。在系统设计中,各资金处理业务至少需要保证幂等性的第一层级“唯一性控制” 。
6.1.1. 常见幂等控制问题资损风险
(一) 上下游调用中, 幂等字段使用自身产生的流水号做幂等控制容易出现幂等被击穿。
(二) 幂等字段的选用不合理。
选错幂等字段,在业务上导致应该做幂等控制的未做或不该控制的被控制。比如本该选用交易流水号做幂等控制的选用了业务流水号。
(三) 幂等字段设计的全局性考虑不全。
1、空间全局性考虑不全。
如外部商户请求的幂等控制,是使用“商户号+请求流水”还是使用“商户号+日期+请求流水”还是使用“商户号+日期+交易类型+请求流水”来设计幂等字段做幂等约束。
2、时间全局性考虑不全。
幂等字段的设计是幂等一天,还是三个月,还是永远。在我们使用数据库来做幂等控制,由于分区、归档等原因,会导致幂等失效。比如分区后建立的本地索引(非全局)的唯一索引,只会在当前区有效;数据归档之后,幂等控制无法判断归档走后的数据。
(四) 幂等底层控制设计不合理导致幂等失效。
如使用 select 而非 insert 进行唯一性控制,高并发的时候就会使幂等失效。
(五) 消息、定时任务等非同步场景的幂等性未考虑。
(六) 上下游系统交互的幂等约定不清楚。
如很多银行机构接口无幂等性保证,如果我方不清楚将框架设置为异常重发就会造成资损。如我们只约定了唯一控制的幂等约定,对哪些字段进行幂等第二层级的判断没有约定。
6.1.2. 幂等性控制设计规范
(一) 幂等性字段规范。
1、使用上游流水做幂等控制:上下游系统调用中,下游系统在可以使用上游系统的请求流水进行幂等控制时,不要自己生成流水进行幂等控制。
2、关键的资金业务处理,需要选用全局的唯一处理流水号做幂等控制。如账务系统、各渠道网关系统均需要选用 UTPP 的支付流水号来贯穿始终进行幂等控制,以保证幂等在整个支付过程中、在整个资金体系中全局有效一致。
3、选用幂等字段时候,除了理解其字段控制含义,还需要理解其业务含义;以达到选用合理幂等字段的目的。如上游有业务流水号、交易流水号,下游系统到底选用哪一个做控制,需要考虑其在特定场景下的业务含义。
4、幂等字段的选用要考虑其空间和时间的全局性、扩展性。要考虑选用哪些字段进行幂等控制、幂等字段流水号的长度够不够用。幂等控制时间是多少,由于数据库的归档、分区、分库等操作,会不会导致幂等失效;部分的数据库结构操作、数据操作甚至需要我们变更幂等的控制机制。
5、除了约定哪些字段进行幂等唯一性控制;对于实现了幂等控制第二层级的服务,还需要约定哪些字段用于幂等控制。如收单系统和商户约定了“商户号+请求流水”做幂等唯一性控制;还约定了“收付款方账号+金额”做第二层级的幂等控制字段,重复请求下“收付款方账号+金额”一致会返回交易状态,不一致会返回“请求参数与原先参数不一致”。
(二) 消息处理、定时任务等非同步场景需要和同步场景一样考虑幂等性。
1、对于消息的处理,不能依赖消息次序来进行业务处理;要考虑乱序、消息同步处理等问题。可以通过消息创建时间、现在的业务状态顺序判断消息是否过期;消费掉消息后是否需要再次进行业务处理。
2、定时任务执行的时间、次数都是不可控的。更需要通过业务状态、业务规则对定时任务进行幂等控制。
3、需要明确:消息处理、定时任务只是业务逻辑处理的一个触发,并且其触发是不保证次序和次数的,实际生产中消息中心消息重发、调度中心多次调度都是存在的;一定要保证在系统的业务逻辑处理中考虑业务的幂等控制。
(三) 幂等服务的约定。
1、幂等性的处理机制约定:仅幂等唯一性控制,还是需要进一步根据哪些字段做幂等第二层处理。
2、幂等性交互的约定:上下游交互,需要明确幂等性处理的相关规约(如幂等异常处理机制、返回码、错误码等)。
3、服务重发的约定:
a) 交易类的服务,禁止网络自动重发。无论内部的dubbo、外部的httpclient、 xfire 等禁止使用框架组件自动重发。
b) 上游系统调用下游系统,请求未发送前,上游异常,可以置失败、进行重发。
c) 上游系统调用下游系统,请求已发送再出现系统异常,必须向服务方确认状态后,明确可以重发才能进行重发。
d) 不能明确可以重发的,就不要重发;遵循重复就严原则。
(四) 服务的幂等实现。
常见的幂等实现方法,有如下几种:
1、幂等控制表实现。
一般情况下,我们会在数据库上设计一张幂等控制表,并根据幂等控制字段建立唯一索引。
在服务使用方请求过来的时候,先往幂等控制表 insert一条数据,通过数据库本身唯一约束进行控制;当有重复请求,将不能 insert 重复的数据,有相关异常抛出,提示幂等控制表已经有同样数据存在。达到幂等控制目的。
由于是基于数据库的幂等控制表的方案,当有数据库变更,尤其是幂等控制表有归档等数据迁移、分区、分表、分库等情况时候,将会导致数据库唯一约束失效的场景,需要着重考虑。
2、其他实现。
也可以借助分布式缓存等进行幂等控制实现。
此种方式不是建议使用控制方式,如果需要使用,请和各种域架构师进行评审。
6.2. 兼容性设计规范
由于业务在发展,系统一定处在变化之中,变化就一定会有兼容性问题的存在。大到每次业务上线、系统发布,小到一次业务迁移、配置项变更;每一次变化都会带来一定的系统风险。
兼容性考虑不充分除了影响可用率外,还会导致资损的发生。因此兼容性设计是系统设计的重中之重,产线上已知的很多资损问题都和兼容性考虑不周有关。
常见的兼容性设计需要考虑如下的兼容性场景:
6.2.1. 常见兼容性问题资损风险
(一) 老系统和新数据结构等不兼容。
由于我们现在生产发布是 DB 脚本发布现行,每次发布过程中均存在着老war 包新db(表结构、索引、数据等)的情况。考虑不周会导致服务与数据出现兼容性问题。
(二) 新系统不能处理老系统产生的数据。
如老系统落单了一笔支付交易,新系统上线后不能对其进行支付。
(三) 老系统不能处理新系统产生的数据。
如系统上线后发生了回退, 老系统不能处理新系统产生的数据将会导致问题。
(四) 收单服务接口、渠道网关接口、内部服务接口兼容性考虑不全。
详见接口规范章节。
(五) 发布过程中,由于存在一定时间的新旧 war 包并存,消息和定时任务的兼容性考虑不全。
1、虽然我们在产线进行了消息分组,一定程度上规避了老系统生成的消息,新系统消费的消息兼容性问题;但当消息的源头生产者没有进行上下线的时候,会出现新系统消费老格式消息的问题。
2、定时任务(尤其是非调度中心触发的调度任务)的调度频率、执行系统、内部执行逻辑不兼容,容易出现资损。如某个系统自身的定时任务选择系统执行者的逻辑发生变化,本来该只有一个执行者的任务,导致两个执行者执行。
(六) 发布过程中,新老系统的幂等控制、并发控制不当,导致资损。
有可能单独来看,新老系统都能控制好幂等和并发,但是在发布过程中,有可能双方的控制机制不一致等情况,使得业务重复处理,引起资损。如网关的幂等在发布过程中曾经被击穿导致的资损。也可能老系统的业务没有并发场景,新系统上线后需要考虑并发,新老系统处理同一笔请求的时候(尤其是任务调度处理的时候),将做不了并发控制。
(七) 新老业务流程、业务处理规则不兼容。
比如一笔理财的抢购支付过程中,分为落单和支付两个环节:库存控制的逻辑老业务流程是支付成功后再扣库存,新业务流程是落单扣库存再支付;如果落单使用老系统、支付使用新系统;在兼容性考虑不周全的情况下会导致不扣减库存,出现超卖。
再比如,某个业务系统业务发生变化,状态机状态迁移发生了变化,原来的一个业务状态再支付成功的时候迁移为处理中,新状态机设计为迁移为成功;那么对于这笔中间态的业务数据的处理就可能导致资损。
(八) 业务迁移对新旧业务的连续性等兼容性考虑不全导致资损。
主要包括参数字段(包括备注)、业务规则处理、反交易处理、开关处理等方面。
业务迁移不仅仅是新的业务规则替换老的业务规则,在整个替换过程中,更需要保证业务的连续性;系统如果不能兼容新旧业务的处理会有极大的资损风险。
如UTPP 迁移过程中,新增或变更了很多业务参数,业务平台切换新接口的时候,如果对新旧参数做兼容处理,会导致资损;新账务迁移过程中,商户出金中的基金划拨场景的商户出金交易新旧业务规则不同导致实际划拨了资金;新账务迁移过程中,如果一笔交易正交易走老体系,冲销走新体系将会出问题(反之亦然);如迁移的开关复用了以前的开关,开关混用,导致问题。
(九) 数据或数据存储迁移导致的兼容性问题。
我们后续会碰到很对的数据迁移:如缓存从redis迁移到hippo、数据库从 oracle、mysql 迁移到 pg 等等。如果迁移过程中数据格式不同、甚至丢失精度会引发资损。
业务数据迁移,如会员、ccdc 等数据迁移,如果新老系统对业务数据的处理不兼容会导致资损。
6.2.2. 系统兼容性设计
随着业务的不断变更和发展、系统和架构也在不断的调整和优化;
这其中既有新业务、新系统的上线,也有老业务、老系统、老数据的迁移。
在这些新老的变化过程中,我们既不能导致业务的间断影响可用率,更不能产生资损造成公司损失。为了防范这些新旧间处理的风险,我们需要从数据、系统、发布、业务等各个环节做好兼容性设计。
(一) 数据库、接口服务新增的向前兼容性原则。
向前兼容设计更多的体现在数据库、接口的初始化设计阶段(新增数据库结构、新增接口)。
数据库结构与服务接口在设计过程中,在数据库、接口复杂度与字段(个数、长度等)允许的范围内,尽可能的考虑到未来可以预见的情况,支持未来的业务拓展。
新增设计时,需要考虑到:
1、 数据库字段、接口字段的合理冗余。比如在设计交易接口的时候,除了考虑交易金额字段,考虑未来有收费的场景,可以增加手续费金额字段。此时,也要注意避免过度设计。
2、 数据库字段、接口字段长度的适度放大与类型的泛化。比如在允许的范围内,可以将交易金额字段设计的范围长度大一些;某些类型字段定义为字符串类型而非整数类型;时间参数精度设计到毫
秒等。
3、 数据库字段、接口字段或接口处理逻辑的约束。考虑到未来有可能扩展或放松现有约束(如非空判断等),在接口中考虑默认、异常等多种处理情况。
(二) 数据库、接口服务变更的向后兼容性原则。
向后兼容设计是最常见的应用场景,更多的提现在数据库、接口的变更阶段(变更数据库结构、接口)。
服务提供方在做接口非破坏性的变更时,需要确保服务以向后兼容的模式演变;在无需变更现有消费者的实现或配置的条件下被更多的消费者重用。
由于我们发布过程是 DB先发布,所以新老系统是会同时使用一个数据库结构的。因此必须保证数据库结构发生变化的时候,新旧系统可以同时使用变更后的数据库结构。
一般数据库、接口变更,分为如下场景:
1、删除字段。
a) 禁止直接删除正在使用的字段。
b) 对于接口字段,如果确认该字段所有的请求方和服务方都不再使用。分多个版本,按照请求方先去掉该字段,确认所有请求方完成后,服务方方可以进行字段的删除。
c) 对于数据库字段,老系统要先解除和数据库结构间的字段依赖(包括语法依赖和业务依赖)再进行删除。对于已经产生的数据,要考虑保存机制。
2、新增字段。
a) 服务提供方需要考虑各服务请求方的兼容性。服务提供方
对该字段的约束允许为空,接口处理可以以提供默认值等方法保证逻辑处理兼容性。
b) 数据库结构新增字段的时候,需要保证系统与数据库结构的语法与业务兼容。数据库字段可空。
c) 服务请求方在升级新增字段的时候,根据服务提供方该新增字段是否已经上线,制定兼容性方案。
3、变更字段约束。
a) 禁止直接进行字段约束从严的变更。接口进行从严变更(如长度收缩)的时候,需要各服务请求方先检视请求方约束是否符合条件;不符合条件的,先完成请求方的变更,后续进行服务方的升级。数据库进行从严变更的时候,要保证老数据符合规范、系统先完成约束控制后进行变更。
b) 字段约束放松的变更。接口按照服务提供方先进行约束放松,后续版本各请求方进行变更;请求方不能早于服务方进行变更。
4.变更字段名称。
a) 禁止直接变更正在使用的字段名称。尤其是对于为多个服务请求方提供服务的接口的字段。
b) 当需要变更接口字段名称的时候,按照新增字段、删除字段的流程来处理变更。
5.变更字段类型。
a) 禁止直接变更正在使用的字段类型。
b) 如果需要变更类型的时候,建议新增字段来解决。
6.数据库索引变更的时候要考虑对产线执行计划的影响。
(三) 数据库、接口服务删除原则。
1、禁止直接删除正在被使用的数据库结构和接口服务。
2、如果需要进行删除。接口按照服务请求方先变更,在确认已无服务请求方请求接口的前提下,服务提供方才可以进行接口删除。数据库结构需要保证系统不会再放我的情况下,并做好数据的保存。
(四) 对于文件字段、缓存对象、消息对象的使用和设计,也需要遵从数据库与接口服务相关的兼容性设计原则,保证向前向后的兼容设计。
(五) 生产系统发布过程中兼容性考量。
发布过程中,要从系统、数据、业务组合考虑兼容性。
1、需要考虑系统和数据之间的兼容性。
设计过程中要保证新老系统产生的过程数据可以相互处理,即老系统产生的中间数据,系统发布后,新系统可以出来;新系统产生的数据,系统一旦出问题回滚后,老系统也可以处理。
如果不能处理的时候,不能选用直接的 war 包回滚方案。可以通过中间兼容版本保证回滚的兼容性、也可以通过数据订正,保证新系统产生的数据部会被老系统处理(数据订正方案是紧急情况下的处理方案,不建议采用);另外,也可以通过引流等方式解决这部分数据问题,以保证其兼容性。
2、需要考虑系统和业务处理之间的兼容性
生产系统发布过程中,在老系统受理的业务,新系统可以继续处理(如老系统落单、新系统继续支付,老系统正交易逻辑、新系统反交易逻辑),并且要保证业务规则、业务流程的兼容处理。
3、新老系统间的兼容性,避免幂等击穿、并发失效等问题
对于新系统的功能变更,幂等机制、并发控制机制等只可以增加,不可以减少,避免老系统未能控制的幂等、并发,新系统也可以控制住;如果是相关机制变更的话要慎重,控制只可以从严,不可以从松,控制字段、控制场景、控制业务要考虑新旧系统兼容的情况。
4、兼容性分析过程中,不可以孤立看新旧业务、新旧系统、新旧数据的处理,需要组合考量。
a) 组合考量业务的处理。
业务处理的各个场景和过程中都会有被新 war 包、老 war包处理的两种情况。要通过分析业务各个场景和过程中在新旧 war 包中的处理步骤和流程,判断他们之间在业务上是否有依赖关系,并分析在组合的业务场景下是否可以被正确处理。
比如,订单交易平台修改了支付的冲销逻辑,业务兼容性需要做如下分析:对于一笔支付业务,有支付和冲销两种场景,支付场景、冲销场景被老系统如何处理、被新系统如何处理?支付和冲销有没有依存关系?老系统的支付是不是必须被老系统冲销?老系统支付新系统冲销可以不可以被支持?新系统支付老系统冲销可不可以被支持?
如果冲销服务有查询状态、冲销发起两个过程,还需要考虑着两个过程和各个业务场景组合的情况。
b) 组合考量新旧系统兼容性的处理。
系统的多种处理和控制机制是分多个阶段的,每个阶段也都会有被新 war 包、老 war 包处理的两种情况。通过分析者每个阶段在新 war 包和老 war 包中不同处理以及控制的目的、逻辑、方式直至字段代码,判断其相互之间有无依赖,每个阶段在什么场景下可以成为下个阶段的前置;并分析其在组合场景下可否互相处理完成功能。
比如,某个系统对幂等控制进行优化。需要分析该系统处理的任务有任务抓取、处理等阶段,老 war 包怎么进行任务抓取?怎么进行任务处理,幂等控制的?新 war 包是怎么处理的?抓取的任务是做完幂等控制才能被处理?还是抓取后流入到处理进行幂等控制?新 war 包抓取的任务老war 包能不能处理?
如果除了处理阶段流程变化,还有处理逻辑变化,还需要进一步考虑并发、幂等控制的字段及实现代码在各个组合场景下的兼容性
c) 组合考量新旧系统与新旧数据的兼容性处理。
发布过程中存在新系统写数据、老系统写数据、新系统读老系统写的数据、新系统读新系统写的数据、老系统读老系统写的数据、老系统读新系统写的数据供六种场景。要分析以上各个场景的读写是否正确,读写之后的业务处理方式是否正确,是否会对下一步业务处理产生不兼容的影响?
比如一个业务改造走挂内部帐逻辑的改造。要考虑新系统的时候写数据会记录挂账单编号 ,老系统写数据没有挂账单编号,新系统读老系统没有挂账单编号的业务数据怎么处理?
(六) 生产系统发布过程中,要考虑消息、调度的兼容性。
按照前述的处理原则,发布的时候还需要避免消息处理、调度的不兼容处理。
其他处理方案(不建议):当有消息不兼容的时候,需要告知部署同学按分组下线消息源头生产者;当有调度不兼容的时候,部署的时候可以暂停调度。
(七) 业务迁移过程中,涉及到新旧业务流程、新旧业务规则、新旧业务数据等的兼容处理,需要从分析、设计、上线、验证等各个环节把控。
1、对于业务迁移,必须详细分析所迁移业务的新旧规则、流程、参数,并和上下游系统确认。
2、新老业务与新老系统之间必须能够业务处理流程和规则上保证平滑处理、无缝切换。
3、尽可能保证新老业务、系统、数据直接的交叉处理。不能保证的时候,要通过相关表示避免交叉。
4、业务迁移的风险较大,需要通过开关、引流等进行产线验证。
5、迁移过程中,要有完整的报警、核对等跟踪机制,保证及时发现问题;有完整的开关等流控机制,保证有问题的时候可以及时断流。
(八) 数据和数据存储的迁移。
涉及到系统的底层环节(数据),要分析不同数据结构、不同数据存储之间的差异,保证这些差异可以被系统正常接受处理。
1、数据迁移(如CCDC数据迁移、会员数据迁移)前后,要保证业务系统能正常识别和处理。不能保证的话,要考虑停机迁移。
2、数据存储迁移,尤其是不同存储之间的迁移,要考虑其之间的差异(如oracle 和pg 的差异、redis 和hippo的差异等)。尤其是精度、sequence 等相关的差异更应该注意,这些都和资损密切相关。
3、迁移过程中,要有完整的报警、核对等跟踪机制,保证及时发现问题。
4、数据是系统的底层环节,迁移的时候一定要有完备的回退机制。
(九) 风险较大的系统发布与业务迁移,采用开关的方式规避风险,保证业务的兼容运行。
6.3. 并发互斥控制设计
在整个分布式系统中,大到业务的并发处理、服务的并发请求,小到数据库表的并发读写、 java 对象的多线程访问,并发无处不在。因为并发,所以互斥。
我们所说的系统层面的并发互斥控制,主要包括:
1、服务的并发请求控制(进程级) 。
服务的并发请求,既包括dubbo、http等同步调用的场景也包括消息、调单等异步调用场景,需要一起考虑。
此外,在业务处理中,还存在各种互斥的资金处理的场景与流程,也是我们需要在设计的时候考虑的。
2、服务内处理的并发请求控制(线程级)。
a) 内存中的并发控制。主要是内存中的对象、线程变量等资源共享资源进行并发控制。
b) 内存外的并发控制。主要是指内存外的存储资源,如数据库表的访问等的并发控制。
6.3.1. 常见并发互斥类资损风险
(一) 在互斥的业务流程与场景中,业务处理过程中,无状态迁移变化的控制。
复杂的场景中未使用状态机。或者状态机状态迁移合法性验证不全面。或者状态机设计不合理,使得状态迁移变化不可靠。
常见的状态机设计问题有:
1、状态被执行有多次。如支付成功多次可能导致业务被推进多次。
2、终态还可以继续变化。如代发成功又变更为待支付。
3、互斥状态没有做并发控制。如发货和退款同时发生,同时成功。
4、状态机没有统一控制。
5、多个状态机耦合在一起,状态控制混乱。如交易核心支付状态、交易状态耦合在一起,由于不同的状态控制,导致支付状态成功、交易状态失败的问题。
6、状态机的状态迁移被漏处理。
(二) 并发请求处理中,没有考虑到消息与调度场景。
比如收单服务请求支付核心进行支付处理,同步响应结果和消息通知结果同时到达,未做并发处理,业务被推进了两次(可能申购了两次基金、也可能通知商户发货了两次) 。
(三) 多线程环境下,对内存中资源未做并发控制或并发控制不严格,尤其是对线程变量的使用。
如对共享数据访问不加锁、单例对象进行写操作、线程变量进出不清空等均会导致并发控制问题。
(四) 数据脏读,并根据脏数据做判断。
需要注意的是,不仅仅是对数据库中的数据访问存在脏读,分布式缓存中的数据访问也存在脏读。
(五) 常见的数据库锁操作问题。
为了防范脏读脏写、不可重复读等常见的数据库并发问题,我们常用锁机制进行处理;不合理的锁使用会导致风险。 常见的问题有:
1、先读后锁,而非先锁后读。 读的数据不能保证是最新数据,会导致仍然基于脏数据做判断的风险。
2、批量 update 流水的时候, where 条件中没有原有流水的状态,且没有判断更新条数。容易出现不可重复读的问题。
3、SQL 语句中使用 update set select 完成数据的更新,有可能导致update 锁机制不生效。
4、锁没有顺序且未设置超时等退出机制(如 SQL 中未加 nowait) ,导致死锁。
6.3.2. 系统并发互斥设计
(一) 业务状态比较复杂的场景,使用状态机进行状态控制。
状态机是针对业务状态比较复杂的场景下,并发互斥控制比较常见的设计模式。如续期宝业务、统一订单交易平台均使用状态机来进行业务状态的管理。
状态机设计过程中需遵循如下规范:
1、由于业务状态的迁移,可能由同步服务请求、异步消息、后台调度、用户操作、运营操作等多种情形触发。在此情况下,需要保证状态机统一进行控制,不能设计多个状态迁移变更模块。更不能设计多个状态机进行对不同情形做状态迁移。
2、要设计终态,且保证终态不能被继续迁移。如一笔交易订单的超时关闭就是终态,在设计上就要避免其不能再次迁移为待支付。
3、要保证状态迁移和业务处理的一致性。
4、设计中,针对状态迁移的处理,需要对所有可能的状态做判断。极端情况下,可以判断所有的状态。
5、由于业务状态比较复杂,为防止状态机状态设计腐化,与状态机实现代码表现不一致。状态机不能只根据文字描述进行开发,要先设计或更新状态迁移表或状态迁移图;可以明确表达出入口状态(初始状态)、终态、状态跃迁路径再。
6、不能用同一个状态字段表达多种状态迁移。简单的业务处理,可能不使用状态机进行状态迁移的并发控制。但仍然需要使用统一模块进行状态控制、并考虑多种情况下的并发请求,做好业务的并发与互斥处理。
(二) 并发请求的处理。
针对分布式环境下的并发请求的处理,多是基于数据库或分布式缓存等资源等进行并发访问控制。常见的并发访问控制模型主要有如下几种:
1、基于资源的并发控制 (悲观) 。
这是比较经典的并发控制模型,使用较多,如账务系统中的记账操作就使用该模型控制用户账户余额的并发更改。
2、基于资源的并发控制 (乐观) 。
由于是在第4 点更新领域对象的时候检查并发;其实整个过程中并没有锁定任何对象和记录。所以采用该机制的时候,系统应该要容许不可重复读问题的出现。
3、基于分布式锁服务的并发控制。
分布式锁可以借助分布式缓存实现。
(三) 数据操作的规范。一锁二判三改。
1、一定要保证先锁后读,避免脏读。数据库可以使用 selectfor update 这种模型进行加锁。
2、提交的时候要判断更新的数据的状态是否符合要求,避免不可重复读。要在 SQL 的 where 条件中增加原有流水的状态,且检查了 update 方法返回的记录条数是否满足预期。
如果是状态机的状态进行 update 的时候,要基于状态迁移来写 where 条件中的前置状态值,where status=xx 或者where status in (xx,yy) 。update 的结果,影响行数,0、1、N的时候处理逻辑均需要实现。
3、不要使用 update set select 完成数据的更新,有可能导致update 锁机制不生效。
(四) 多线程操作规范。
多线程操作规范需要遵循常规的 java 并发操作规范,保证操作的原子性。
代码开发的指导原则应该是:尽可能的避免在多线程间共享数据、共享操作;如果要共享数据,需要对共享数据加锁;尽可能的使用 spring 提供的单例框架配置单例的 bean;不要对单例的bean进行写操作;业务代码开发过程中,尽可能的避免使用线程变量;使用线程变量的时候,一定要在出入口进行清空。
(五) 避免死锁。
无论是数据库还是分布式锁服务或者jvm 内部的锁, 在可能发生死锁的场景, 加锁的时候需要通过加锁排序和设置超时时间等方式避免死锁。
7. 业务产品分析资损防控规范
7.1. 业务产品分析
支付公司的大部分业务都涉及到资金的流转,对于每一笔的资金业务,往往既涉及信息流的变化,也会涉及资金流的变化。
在我们做业务的需求分析设计的时候,如果和业务方未能理解一致或者分析不全,将会从业务源头上产生资损风险。
7.2. 业务产品功能
7.2.1. 常见业务产品分析资损风险
(一) 资金流、信息流、业务流等和理解和业务产品方不一致。
(二) 只分析了信息流的处理,遗漏了资金的处理,或遗漏了某些阶段的资金处理。
比如基金类商户的出款类交易迁移的时候,没有考虑商户不需要真正的出款。
(三) 资金需求中,对资金结算模式等理解不一致。
比如退款成功的理解不一致;是退到商户户还是退到个人户的理解不一致。
(四) 业务产品设计的时候,部分支付产品充退接口设计不完整,尤其资金流设计不完整。
执行充退逻辑时异常考虑不完备会有资损。
(五) 系统业务设计未能遵守先借记后贷记的原则。
(六) 业务缺少资金平衡检查。
(七) 正反交易资金流不一致容易资损。
(八) 内部户的使用不当。
比如挂销机制不合理。
(九) 内部作业人员操作界面未设置合理的限制条件。
如营销平台的卡券发放等。
7.2.2. 常见业务产品分析资损防控点
(一) 需求分析中对信息流、资金流的分析要完整全面。
业务需求中要包括完整的信息流和资金流(业务系统还需要包括相关的物流等业务流),不但要包括正向的完整描述,也要包括反向的描述;不但包括正常情况的描述,也要包括异常情况下的处理描
述。
(二) 业务迁移过程中,要梳理出新旧业务全面的信息、资金流流向。
验证过程中,要核对业务信息、支付信息、渠道信息、银行账户流水信息等。
(三) 新支付产品需求分析的时候,要对支付产品的信息流和资金流做完整分析。
尤其是反向资金流。
尤其是内部业务系统包装的渠道,尤其注意冲退逻辑的实现是否完备。
(四) 资金结算模式的理解。
需求分析过程中,要与业务方梳理出清晰完整的资金结算模式。形成双方认可的资金结算的流向图,流转到具体的资金账户。
(五) 业务设计的时候遵从先借记后贷记原则。
(六) 要保证正反交易的资金流向一致。
对于正方交易资金流向不一致的场景(如冲退转代发、部分支付产品冲退退余额),设计与系分的时候要着重评审,并重点测试验证。
(七) 收单、渠道、支付等系统都需要设计相关的资金平衡检查。
如渠道冲退的金额不能大于原交易流水的金额、收单商户退款的金额不能大于可退金额等。
(八) 内部账户的使用。
必须由业务方和运营财务人员沟通后申请,技术人员不可自行配制;要有挂销机制;要有相关的业务和技术核对机制。
(九) 内部作业系统设置合理的业务条件限制。不允许有无限制条件的输入输出。
如 OMS 查询可以限制为一个月;营销平台的卡券发放一个人可以设置上限 1000个等。
7.3. 业务参数调整
7.3.1. 常见业务参数调整资损风险
(一) 商户参数调整。
业务自行调整合同业务参数影响其他收单商户。
(二) 渠道参数调整。
(三) 结算规则调整。
(四) 通过数据修订等方式进行业务参数调整。
7.3.2. 常见业务参数调整资损防控
(一) 可以验证的业务参数,需要在测试环境等验证过进行调整。
(二) 风险较大的业务参数调整的时候,可以通过限流等措施在产线上逐步开放。
(三) 系统在设计的时候,要避免可能引起业务作业人员误解的配置,避免商户间的串用、滥用、误用。
(四) 业务类参数的调整要平台化,不要通过数据修改等方式进行参数调整。
以上正文结束。本白皮书是我目前为止见过的最专业且全面的资损防控设计白皮书,从对商户提供的服务到内部应用,再到渠道对接,从产品设计到运营,各种场景都有涉及。值得收藏,随手翻阅。