防重技术方案设计和幂等性设计是系统内部的设计,交易类跨系统接口设计是系统之间的状态设计。
防重技术方案设计
为解决容易导致数据一致性的多次重复提交数据情况的出现,分别针对系统建设过程的三层提出不同的解决方案:
客户端:基于token的表单防重
应用层:接口和功能操作必须幂等
数据库:不进行分库分表,基于关键业务字段使用唯一索引约束
客户端防重
客户端使用token作为防止表单重复提交的关键事项。包括某些需要防止重复提交的插入操作,都可以在客户端使用这种方式。
1、不管是SPA(单页面应用)还是普通的页面,在打开表单的时候首先需要从服务端获取表单token。
2、服务端在生成token时需要将token存储到redis,并设置ttl时间,避免redis存放过期的数据内存撑爆。
3、每次提交表单时需要将表单token和页面数据一起提交。
4、服务端在接受到表单提交请求时先检查有没有表单token,如果没有表单token则拒绝处理。
5、服务端在收到表单token时检查redis里面的值是否有效。
目前全球资金目前没有使用redis的原子性操作[脚本],这是个很大的问题,但即使使用了redis就不是没有问题了,redis主从同步。
应用层系统数据一致性设计
业务系统接口在实现时按照如下思路进行考虑,根据不同的业务逻辑采用不同的实现方式。
哪些地方需要考虑幂等
从功能上分
1.接口做幂等处理。如果没有幂等处理,采用接口表,通过业务ID来处理。建议对接口分等级,已经将分期的核心业务按照京东的接口分级方式进行分级,包括P0,P1,P2。
2.定时任务也要做幂等处理的要求。
从操作上分
1.select、delete(批量)id正常情况下是幂等操作,但是对于主从同步,出现从库同步延迟时,将会导致select从库时,出现select不幂等,最简单的处理方式是在第一次插入后,采用managerHint类进行强制读取主库返回参数。以后每次都将使用从库读取。但无论怎样,一定得通知接口掉用方,该查询接口是不幂等的。 在与某支付公司对接时,因在接口文档上并未写明该接口的不幂等,导致提交支付公司订单重复。当时开发人员是每次请求都会生成一个业务ID,导致支付公司认为是两次支付指令,出现了重复代扣的问题。当前另外一个问题是状态控制也不严谨。删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) 。
2.update、insert需幂等处理,如果不能进行幂等处理,采用异步的方式进行处理。数据先放接口表。异步处理日志记录详尽。为什么update要做幂等处理,这是因为
非幂等操作处理方式
1.对非幂等操作,尝试按照 《幂等设计》 的方案进行幂等转换。
2.【可选步骤】如果无法进行幂等转换的要尝试考虑能否将业务流程拆分出主业务操作,通过事务日志+事后补偿的机制解决之。
例如:从A账户扣款,B账户收款,可以将“从A账户扣款动作作为主业务操作”,这样即使“B账户收款出现异常”也可以通过事后补偿的机制促进事务向前推进。
该过程要求提供事务日志组件,详细跟踪各事务的推进情况,并在出现异常是及时由人工干预。
3. 如果不能拆分出主业务操作的,可以采用异步的方式进行。对于比较复杂的业务操作,可以先将业务请求记录下来,然后通过异步的方式,采用线程安全的方式解决。消费代扣流程就是采用这种方式。
a、接入层先将数据进行简单的校验后记录下来,然后返回调用方接受成功。
b、核心业务系统再对输入数据进行各种规则检查;根据业务规则进行相应的运算处理,详细记录各步骤处理过程【可参考步骤3】;将结果输出到输出层对应的存储表。
c、通过任务调度管理器将输出层数据输出到对应的业务系统中去。
DB层数据一致性设计
为了防止业务层出现问题和通过导入的方式造成重复数据记录。在数据存储层一定做防重处理,这是持久化数据的最后一道防线。
1.一般互联网产品不用数据库的函数、触发器、存储过程等。
2.核心交易业务建议不用分库分表(像上面说的某支付公司,就是因为采用了分库分表,导致了select的不幂等)。
3.关键业务建唯一索引,这也是保证数据不重复的最后一道防线,二级索引的叶子节点不保存记录中的所有列,其叶子节点保存的是<健值,(记录)地址>,如果执行计划认定该索引有效,则会通过该二级索引查询到主键,通过主键查询该表的聚簇索引。
5.隔离级别为Serializable(不建议,采用默认隔离级别RR,或者降低为RC,但是一定要评估清楚降低RC能带来的好处及带来的影响(不能限制重复读的问题))
6.date guard设置为最大保护模式。oracle->数据复制机制。同步复制/半同步复制/全异步。目前mariaDB失败没有告警。
7.建议加入疑似重复交易。【根据业务场景,比如发工资,一个月一次|比如选择不同的银行,可以不用考虑疑似判重】
8.时间差保证处理【比如消费日终,在跑日终3分钟内不能进行任何下一步操作】
幂等性设计
幂等本是个数学概念,但在编程中,一个幂等操作的特点是其任意多个词执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
锁与事务
- 自旋锁(Stamped|Atomic)、轻量级锁、重量级锁moniter enter、monitor exit 、CAS。
- ZK(分布式锁),redis setnx.—分布式协调。
- 不建议用MD5用唯一索引,与阿里规范保持一致。使用MD5做主键容易引起随机查找和页分裂。
- 做不到幂等的时候,放弃实时性要求,一步一步做处理。
- mysql
(1)悲观方式 select for update。 先查出数据到应用中,最后带状态更新,并判断更新的条数。
注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用。
(2)乐观方式:
乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。
乐观锁的实现方式多种多样可以通过version或者其他状态条件。
(3)通过唯一索引来实现幂等性
(4)
a. 通过版本号或状态实现。
update ... id = 0 and status = Y(有弊端)、update ... id = 0 and version='1' -- 更新时如果能带ID则尽量带ID更新。
b.通过条件限制
update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高。
注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,上面两个sql改成下面的两个更好。
update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#
update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0
- 分布式锁
还是拿插入数据的例子,如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper或数据库锁),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多个系统,也就是分布式系统中得解决思路。
要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)
redis分布式锁超过这个时间锁会自动释放。分布式锁过期了,解决方案当然就是续期。以下是续期方式:
思路一:任务执行的时候,开辟一个守护线程,在守护线程中每隔一段时间重新设置过期时间。
思路二:通过Redisson中的看门狗来实现(建议)。
-
select + insert
并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。
注意:核心高并发流程不要用这种方法,存在多线程安全的问题 -
状态机幂等
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助 -
对外提供接口的api如何保证幂等
如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号
source+seq在数据库里面做唯一索引,防止多次付款,(并发时,只能处理一个请求)
重点:
对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务, 注意,这里如果两个线程同时查询都没有被处理,都去发起第三方查询,如果前一个线程A返回失败,后一个线程B返程成功,且因网络原因B提前返回。更新时,一定要带状态或版本号更新保证幂等,否则刚才说的情况将会导致B线程成功的状态又被A线程置为失败。
总结:幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在金融相关业务系统建设过程中,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确。
关于定时任务的幂等
- 调度任务目前只能用一台job。全球资金考虑用用分片,不同分片(指定分片,防止IP漂移)读取不同的数据实现。
- 部分任务采用elasticJob。->一台挂了,另一台自动启动。在当前c采用xxl-job来实现任务调度。
- md付采用每天生成定时任务记录表,多个job竞争资源。
交易类跨系统接口设计
使用范围
交易类接口调用要详细记录好跨系统交互的原始数据记录,一是要打印日志,二是要记录到表,作为数据跟踪的基础。避免两边数据不一致等各种错误出现的情况下无法排查定位问题的情况出现。因此这类接口调用第一步要先将数据落地保存。
a、外围系统对本系统的调用数据(发送数据和返回数据)要写入接口表
其它系统调用本系统接口发送过来的数据需要进行处理的,如果业务逻辑比较复杂的,存在并发安全等问题的,先将数据保存入库后再异步进行处理。
注意:数据保存到接口表之后不得再对接口表的数据的业务数据进行修改。
b、本系统对外围系统的调用数据(发送数据和返回数据)要写入接口表
本系统需要发送给其它系统处理的数据,一律需要设计输出数据接口表,数据先写入到输出数据接口表,经过校验检查确认数据无误后再进行发送。
工作流程
接口表设计
跟外围系统交互过程中必须先理解对方系统接收的数据要求,并且同步分析本系统需要发送给对方系统的数据业务关系,据此进行数据库表结构设计。
数据库表结构设计只需要考虑交互数据结构,不需要考虑算法实现过程中的中间数据存储,需要将过程数据和结果数据分离开来。
接口表数据检查规则设计
数据在发送给第三方系统进行处理之前必须先考虑如何对数据进行检查,检查确保数据无误之后才可以发送出去进行处理。
接口表数据检查规则应该不同于算法业务逻辑,需要从另外的角度考虑校验。如代扣拆单合单算法需要从不同业务逻辑校验。
算法设计
算法应该尽量简单,需要重复考虑分布式锁、分布式事务(2PC等方案)、数据分布式一致性、数据重复提交等情况的应对措施。
业务逻辑流程
数据校验
对其它系统发送过来的数据处理之前必须先做相关检查校验,检查校验内容包括:
- 根据业务编码检查数据是否已经发送过来过
- 是否已经处理过
- 数据是否合法,是否有伪造,是否认证过的客户端
- 数据是否完整,格式是否匹配
算法设计
算法设计过程中必须做到如下几点:
- 多线程和单线程执行结果不受影响
- 多次调度和单次调度触发结果不受影响
- 分布式调度和本地调度结果不受影响
- 多次执行和单次执行结果不受影响
- 算法比较复杂的情况下必须多次数据入库存储中间结果,最好每一步中间结果都需要进行校验和检查
接口表设计
接口表需要考虑如下几点:
- 必须有发送状态字段标识数据是否发送
- 必须有发送时间字段
- 必须存储调用对方接口返回原始数据
- 发送给对方系统的数据格式必须原原本本的进行存储
- 可以设置些唯一索引,防止接口表里面的数据存在重复。如目前的风控系统调用第三方返回的数据。
- 每条数据必须有唯一的业务标识,通过业务标识识别每条数据,避免重复发送造成重复交易。
校验规则设计
考虑完接口表设计后要考虑清楚交易规则设计,校验规则可能需要检查项包括:
- 记录是否有重复,根据业务范围逐层检查,例如消费金融代扣接口表设计要考虑订单、商品、还款计划三层数据是否有重复
- 金额是否有误,可以对比上期金额,可以根据明细检查总金额 等,精度是否合适。
- 检查每一批数据是否有业务标识,是否可以根据业务标识进行防重处理
- 检查规则必须不同于算法部分的逻辑,必须从另外的角度考进行检查
- 同一条数据,对方返回错误。->检查校验后再发。唯一索引不能做数据去重,应该通过业务逻辑去重。
数据发送
数据发送过程中需要考虑如下问题:
- 对方接口是否支持幂等设计,如果否情况就比较复杂需要进行问题升级。像某支付公司就坑了我们一回。
- 每次发送数据必须包含业务标识字段,对方系统根据该业务标识字段进行防重处理。发送时间,存储对方接口返回的原始数据。对方的数据格式进行完整存储。每条数据必须有唯一的业务编号。
- 交易类数据做异步处理,比如在分期中用代扣和风控中用的是MQ做异步处理。
- 敏感信息数据需要加密
- 关键数据需要生成摘要信息做到防篡改,客户端需要做数据验签。比如T会员中采用数据验签来对数据防篡改进行校验。
- 多线程和单线程发送、分布式执行和集中式执行、单次发送和多次发送对业务都不造成影响。
禁止:
循环调用dubbo、http调用。在分期中,因某个领导固执己见,将在业务代码中循环调用dubbo服务,导致长事务锁表,引发生产事故。
参考:
https://blog.csdn.net/m0_37337849/article/details/97755987
https://www.xuxueli.com/xxl-job/#%E3%80%8A%E5%88%86%E5%B8%83%E5%BC%8F%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E5%B9%B3%E5%8F%B0XXL-JOB%E3%80%8B