目录
消费金融系统
业务网关 SS
综合查询平台 IQP
单证平台SSP
定价系统PMS
营销管理系统 MM
交易渠道网关 TC
财务系统 FA
大数据系统 ODX
风控系统 IRISK
风控系统 RMS
风控系统 RMFP
客户信息系统 CIS
进件系统 APS
账户管理系统 AMS
信贷放款系统 TDL
外部支付调用异步化
实时交易系统 TDC
技术点
核心交易逻辑
关于交易流程的最终一致性解决方案
核算系统 CORE
系统设计
错误码设计
概述
异常捕获机制代码示例
基于约定的异常处理机制
数据一致性治理
CAS校验
幂等&异常补偿
对账
对账系统
面试方向
一致性
工作中常见的生产问题
长事务问题
索引确实造成全表扫描
消金两大场景:
信贷
消费金融
消金系统分层架构:
展示: SS IQP
流程:APS TDL TDC
TC CORE FA MM RMS/IRISK/RMFP
核心:AMS CIS
基础:SSP OPR ODX PMS
统一外网的各种各样的参数风格,转换成消金内部统一的参数,比如消金内部统一的成功响应码是0000,失败是9999
主要是运营人员使用的
定价查询接口,返回的是一个很大的json,由于在AMS侧需要比较定价是否超话,而直接比较大json不太方便,所以每个大json都会带一个摘要比如md5值,每次只用比较md5值就知道该定价信息是否变化过
曾经做过一个宜家消费满800返80的活动,还有全家消费30返回10块,不过都有名额限制,需要去抢名额
后来,这个抢名额的逻辑,被我放在了异步流程中了,异步流程中有调MM拿中奖名额,有发MQ给Core通知放款
电商平台下的营销系统
主要是发优惠券,搞促销活动
千万级别的全量用户发券,如果想争取在一两个小时内发完,就需要用分布式任务调度平台xxl-job,配合rocketmq来实现分片执行,削峰填谷
定位:报盘,回盘,辅助对账
接一个渠道就是一个xxx-getway
渠道:建行,工行,平安普惠
和建行等直连成本低,其他的有手续费。就是从其他的快捷支付啥的有手续费成本高了,后来就直接和银行直连了,省钱
就相当于抢了支付宝/微信的一部分生意,可以将建行、工行的银行卡,绑定在平安消金的app上,然后通过平安消金的app来完成二维码支付
类似于微信支付的余额界面
可以理解成为数据中台,功能就是负责拉取TDC这边的支付数据,然后大屏幕准实时的展示每小时的交易笔数交易额度的变化情情况
二类户交易流程上每笔交易流程都会调用IRISK,来决定是否放行该笔交易
有客户的评级信息,一开始是ABCDE五个评级,每个评级的用户对应享受到的放款额度,放款利率都是不一样的。后来重构过一次,重构成L1到L10,10个等级了
感觉就是配置项,还有合规文件,对接第三方的一些合规规则啊,还有调用反欺诈反洗钱AI智能识别这些
会员账户:比如userId等
实名服务:比如身份证信息、手机号信息,家庭住址等
卡服务:一个人可能会在消金App上绑定多张银行卡
后面有绑建行的卡,直接使用消金app扫码支付,不用通过支付宝微信就可以完成扫码支付
存用户身份信息,比如身份证、地址之类的。存消金内部自然人唯一编号 userId,供消金内部所有系统共用,方便问题追溯
存手机号,比如说后面办什么促销活动,就可以通过手机号给用户发通知短信
负责贷前鉴权,鉴别是否是本人,流程如下:生成流水、客户信息,跳转到第三方页面,客户输入卡号、手机号等,鉴权完成返回信息给我们
负责授信、出额、开户流程,这整个流程,是以APS作为主导来串流程,二类户交易是AMS/TDC来串流程
授信业务分为:
授信几个阶段:
调查、审查、审批、放款、贷后工作
把客户的所有情况了解清楚后,了解了客户的征信情况,评估好所有的风险,出一份调查报告
放款,是资金出银行的最后一道关口
用户进来注册授信时,请求通过前端打到SS,再打到APS
账户系统的5大核心功能:额度,账户状态,授信开户,定价,发票
AMS三大业务闭环
域划分前,AMS很庞大,还负责交易、负责账单、负责还款,负责对账,整个公司内部各系统职责边界都划分的不清不楚,造成了很多因边界问题而起的矛盾
域划分后,AMS作为账户域只作为核心能力层,不负责流程的串联,不负责是否应该额度变更(包括额度扣减、额度恢复)的判断逻辑,所有的额度变更的发起都由外部系统发生。比如支付就由TDC向AMS发起扣减额度请求,还款就由CORE向AMS发生账户还款请求
消费定价更新
定时任务,定时从PMS刷新每个人的定价,因为如果PMS侧定价更新以后,AMS侧每个人的定价当月不能变化,需要等到下个月才能更新
重构交易的过程
重构前交易流程结构混乱,最关键的问题是,串交易流程的核心逻辑,不在一个类中,而是一个类串着下一个类,这样代码结构不直观
域划分
交易域,账户域,营销域,风控域等
最开始,AMS的业务是不清晰的,说白了就是不够“瘦”,
后来的AMS账户额度管控模块,只提供额度变更接口给别的系统调,至于是什么业务导致的额度变更,AMS就不去关心了。AMS账户状态管控模块也是一样,因为什么原因导致的账户冻结解冻,AMS都不关心,一切都由外部触发
账户表结构
一级账户表,主账户表
二级账户表,授信账户表:记录小橙花等不同的产品
三级账户表有两张:消费额度管理表,提现额度管理表(有一个核心字段,已用额度,消费时是增加已用额度,还款时是降低已用额度)
按照业务情况,需要有额度变动的明细表的。我举的例子搞分布式,也就明细表加个状态字段而已
当时做过对接度小满、平安普惠,也就是说平安消金作为借贷的资金方,度小满和平安普惠是作为客户触达方
对接度小满金融时,是度小满将客户在度小满界面发起的借贷请求,给到平安消金这端,平安消金这端经过一系列的检验,比如额度检验,风控检验等,检验通过后,带上该笔放款单号回调度小满的回调接口,通知度小满该笔通过,然后度小满自己调用真正的打款接口,给用户预留的银行卡打款。也就是这里,打款的控制者是度小满
对接平安普惠时,打款的控制者就是平安消金自身,而不是平安普惠。依然是客户的触达方,将客户发起的放款请求给到平安消金,比如额度检验,风控检验等,检验通过后,有两个重要的步骤:步骤一是带上该笔放款单号回平安普惠的回调接口,告知该笔放款成功;步骤二是调用TC的接口,TC再调用平安付的渠道,进行给客户真正的打款动作
注:
说到这里,就要提一个生产问题,当时一个外包同事负责步骤二,结果调用平安付超时了没有拿到响应(实际该笔平安付那边已经执行成功放款5w的动作了),然后外包同事就进行了重试,重试时,他竟然换了一个放款单号,没有用原来的单号,结果就给客户又打了5w的钱,造成了二次放款的严重生产问题
整个TDL就是一个大异步流程,异步里面又用MQ进行了二次异步,又因为一条MQ有消费超时时间,整个放款流程网络请求过多,可能会让单条MQ消费超时,所以采用的自己发送自己消费的模式,每发送接收一次MQ只处理一个放款流程七八个业务节点中的一个
放款主表trade_loan_mgr,有一个流程状态扭转与回退的状态字段,每执行成功一个节点,流程状态就更新一步
在外部支付中,经常需要服务方与第三方支付交互,获取预支付凭证,如上图所示。
这种同步调用的情况下,由于需要跨外部网络,响应的 RT 会非常长,可能会出现跨秒的情况。由于是同步调用,会阻塞整个支付链路。一旦 RT 很长且 QPS 比较大的情况下,服务会整体 hold 住,甚至会出现拒绝服务的情况
因此,可以拆分获取凭证的操作,通过独立网关渠道前置服务,将获取的方式异步化,从前置网关获取内部凭证,然后由前置网关去异步调用第三方
如上,
2:三方支付收单,可以采用异步回调的方式,让三方支付系统返回一个受理凭证,三方支付系统完成真正的支付动作后,异步回调渠道网关的回调接口,当前TDL就是采用的这种回调的方式
五大功能:交易、退款、冲正、撤销、对账
主要有
二类户交易内部又有两个分支
普通二类户交易,走普通的定价逻辑。商户分期,因为每家商户给定的利率都不一样,所以走商户分期时,需要每次交易都实时去查PMS定价系统,商户分期一般是3 6 12三期,三种选择。也还我还写了一个
支付路由
商户分期,是有一个预申请的概念,客户在勾选比如宜家的某个商品后,决定购买前如果决定使用商户分期,客户需要自己选择是3 6 12期,三种选择,选择好分多少期后,就可以生成对应的二维码,此时,就会在APS预先生成交易信息,商家扫描二维码后,开始真正的交易逻辑
交易流程,每次需要先去APS查询是否有预申请信息,如果有也走商户分期,否则走普通非分期交易
支付流程图
技术点
当时用过分布式锁,用过幂等,用过延时消息,用过最终一致性,用过乐观锁版本号
延时消息
最开始的版本二类户交易,上行的交易请求发来后,如果在600ms内没有收到消金的响应,就会立马发冲正过来
但是,AMS的交易流程走完至少要两三秒,所以就把很多步骤放在异步流程里了,这其中就包括AMS扣减额度成功后,通知core开始进行针对该笔交易的记息(还有后续账单的展示)
有一次上行的冲正请求过来后,core给返回了一个没有该笔交易记录,但是core团队又在他们的数据库中,发现了这笔交易
总结起来就是,上行的冲正请求达到core的时间,比交易异步流程中通知到core的时间要快
直观的解决方案就是,让冲正请求来得慢一点,然后我就在AMS里,把上行的冲正请求,一进来就丢进MQ了,延时1min,然后自己搞了个消费者消费它,才继续走后面的冲正流程
交易流程全部同步的跑完可能需要两三秒中,所以把一大堆操作,挪到异步流程中了,同步流程中就只有必要的数据获取、风控校验、每月/每日的限额校验,额度扣减等核心步骤了
分布式锁
每月/每日的消费额度限制
当时做过一个需求,限制客户每日在某个门店的消费额度,限制客户每个月的消费总额度
以主账户号 + 商户号为key,当日消费金额为value
因为没有使用lua脚本,通过账户号 + 商户号为key查询到当日消费金额value,判断额度还没有超过当日5000的限额,就以主账户号 + 商户号为key,当日消费金额+ 当前该笔交易金额amt为新的value写入redis,这两个步骤无法保证原子性,所以就给这两个步骤,一起加了个redis分布式锁
版本号乐观锁
账户表加了一个版本号字段,TDC先查出账户额度 和当前的版本号m,然后做后续的校验逻辑,这段逻辑可能耗费几百毫秒,然后真正调用AMS扣减额度时,把版本号m传递给AMS,让AMS自己去比对库中目前的版本号是否与版本号m是否一致
二类户交易的流程
当时的交易流程,是接口一进来,立马把所有的请求参数存一遍,交易主表trade_manager和交易事务表trade_transaction_detail
交易主表trade_manager,每比交易进来,都立马先记录一条记录,交易主表有个状态字段trade_status:初始化,支付成功,支付失败
最开始,搞了个大字段,把整个交易请求报文一股脑全丢进去,用来追溯问题的,后来被DBA劝退下架了,后来提出申请几台MongoDB专门存这种大json的非结构化数据,很合适,不过又被部门长拍死了
交易事务表trade_transaction_detail,交易/退款/撤销/冲正,等都要在这个表中添加一条记录,这个表有个关键字段 transaction_type,用来表示是TRADE REFUND REVERSED REVOKED
交易的错误码
错误码一定要明细,0000是成功,其余的都是失败,但是失败的原因各异,可能是额度不够失败,可能是风控校验不通过失败,可能是每日限额失败,等等,不同的失败都要有各自不同的码值,这样才能实现精细化管理,问题的定位也会更清晰
二类户交易流程,黑白名单检验,是不是可以使用余弦向量比较法?
好医生商城交易
背景:平安好医生有自己的商城,在平安好医生商城购物,可以使用平安消金的消费额度
实现:整个实现采用流行的异步化的实现
好医生商城下订单的信息,是给到了APS保存,所以APS维护了订单的状态信息。
下单成功后,好医生商城侧,发生支付请求,支付请求经TC转入AMS/TDC。AMS/TDC接收到支付交易请求时,立马先完整的保存一遍交易信息,并在交易主表中记录支付状态为初始化状态
然后AMS/TDC用异步线程开启内部的后续支付处理逻辑,同时同步流程直接给好医生返回了
3秒后,消金自己的前端会发生轮训,通过支付单号来AMS/TDC查询该笔交易的支付情况
支付回调的逻辑,除了等第三方回调,还会提供主动查询结果的功能,这种是经典的分布式最终一致性解决方案
整个支付操作流程是,客户在好医生商城侧成功下订单后,点击开始支付,会首先弹出内嵌于好医生商城app执行的消金支付流程界面,客户在消金前端页面上发起真正的支付请求,此时的支付请求,才会打到AMS/TDC来。也就是说,此时的消金前端是能拿到好医生商城传过来的外部交易单号的
22年3月,交易迁移到TDC
23年2月,账户再次重构,重构逻辑未知
蘑菇街交易创建过程中的分布式一致性方案
交易创建的一般性流程
我们把交易创建流程抽象出一系列可扩展的功能点,每个功能点都可以有多个实现(具体的实现之间有组合/互斥关系)。把各个功能点按照一定流程串起来,就完成了交易创建的过程。
面临的问题
每个功能点的实现都可能会依赖外部服务。那么如何保证各个服务之间的数据是一致的呢?比如锁定优惠券服务调用超时了,不能确定到底有没有锁券成功,该如何处理?再比如锁券成功了,但是扣减库存失败了,该如何处理?
方案选型
服务依赖过多,会带来管理复杂性增加和稳定性风险增大的问题。试想如果我们强依赖 10 个服务,9 个都执行成功了,最后一个执行失败了,那么是不是前面 9 个都要回滚掉?这个成本还是非常高的。
所以在拆分大的流程为多个小的本地事务的前提下,对于非实时、非强一致性的关联业务写入,在本地事务执行成功后,我们选择发消息通知、关联事务异步化执行的方案
消息通知往往不能保证 100% 成功;且消息通知后,接收方业务是否能执行成功还是未知数。前者问题可以通过重试解决;后者可以选用事务消息来保证。
但是事务消息框架本身会给业务代码带来侵入性和复杂性,所以我们选择基于 DB 事件变化通知到 MQ 的方式做系统间解耦,通过订阅方消费 MQ 消息时的 ACK 机制,保证消息一定消费成功,达到最终一致性。由于消息可能会被重发,消息订阅方业务逻辑处理要做好幂等保证。
所以目前只剩下需要实时同步做、有强一致性要求的业务场景了。在交易创建过程中,锁券和扣减库存是这样的两个典型场景。
要保证多个系统间数据一致,乍一看,必须要引入分布式事务框架才能解决。但引入非常重的类似二阶段提交分布式事务框架会带来复杂性的急剧上升;
解决方案为:
在电商领域,绝对的强一致是过于理想化的,我们可以选择准实时的最终一致性。
- 我们在交易创建流程中,首先创建一个不可见订单,然后在同步调用锁券和扣减库存时,针对调用异常(失败或者超时),发出废单消息到MQ
- 如果废单消息发送失败,本地会做时间阶梯式的异步重试;优惠券系统和库存系统收到消息后,会进行判断是否需要做业务回滚,这样就准实时地保证了多个本地事务的最终一致性
总结
- 这里就是MQ的一个应用场景:天然的重试机制,当然都需要配合幂等消费措施
- 蘑菇街的这个最终一致性解决方案,消金自己的交易流程,可以作为参考
- 总结起来,很多最终一致性解决方案的根本原理是类似的,也就是将分布式事务转换为多个本地事务,然后依靠重试等方式达到最终一致性
参考链接:https://www.cnblogs.com/Vincent-yuan/p/16074577.html
转账案例的最终一致性
拿a转账b举起例子。a操作生成一个账户金额变更记录,这个记录添加一个state,标记记录未完成,事务提交,然后发送消息到b那边系统
a这边系统支持幂等,等待下一步b回调通知转账结果完成。而a在操作其他业务,其能操作的金额是过滤掉这个未完成记录后的金额部分。这种就是我说的语义锁,好像有一部分金额被锁住了,也就是冻结一个中间状态
核算系统,负责贷后的相关逻辑,比如每日跑批算利息、比如还款、比如账单管理,包括账单查询,账单分期
有的公司支付系统每天千万级交易需要尽快的收集到大数据平台参与计算,就需要用到kafka,来把每笔交易数据给到风控系统
大数据风控系统,每天都会计算产生一些结果,比如白名单、黑名单等,如果计算得出张三进入了黑名单,那就要在支付系统APP内给张三发一条站内信,这种黑名单的通知的量是比较小的,但是对可靠性要求比较高,这时就不需要用kafka,就可以选择RabbitMQ或者RocketMQ
RocketMQ天生就客服了kafka、RabbitMQ的缺点,又综合了他俩的优点,RocketMQ的一个小的缺点是RocketMQ的客户端只支持Java
本人的异常处理原则是:强制固定 code、自定义 message。
异常信息的第一使用者是人,这里包括使用者(用户)和异常处理者(运营人员、程序员)。
细分一下,异常又分为业务异常和系统 bug。
业务异常是指业务流程中的异常场景,如支付时卡余额不足导致无法支付、用券时发现券不符合使用条件、用户执行了某个未授权的操作等。这类异常的触发者是用户自己(而不是系统),信息受众是用户。所以业务异常的信息提示必须注重用户体验,优秀的提示文字至少要做到以下几点:
尊重用户,不要让用户感觉受到冒犯或戏谑(请慎用自认为很“幽默”的话语);
清晰,应包含触发异常的关键信息(如当余额不足时应提示当前余额是多少);
具备指引性,用户看了之后清楚该怎么做;
第二类异常是系统 bug,如接口超时、非预期参数导致程序崩溃、代码逻辑 bug 等。该类异常的触发者是系统(或者说开发系统的程序员),信息受众是程序员。所以 bug 类型异常的信息提示必须对程序员友好,让程序员看到错误提示后能够快速定位到问题的原因、代码所在的位置。
我们说异常,一般就是指 bug 型异常,这类异常占程序员的精力也是最多的,也最值得优化处理机制。
bug 型异常具有如下特征:
不可控性。没有程序员会主动去写 bug,但没有哪个系统完全没有 bug。我们无法预知 bug 到底来自哪里、会有什么样的提示信息;
定位困难。当系统提示“余额不足”时,我们很快知道是用户卡没钱了,但当系统提示“参数类型错误”时,我们往往只能一脸懵逼;
可能涉及敏感信息。如 SQL 操作错误时可能会将整个 SQL 语句暴露给外界;
因而优秀的 bug 型异常处理机制应做到:
提示信息对程序员友好;
记录函数调用栈信息;
脱敏;
提示信息对程序员友好,可能意味着对用户并不友好,一些程序员正是据此以“用户体验”之名将 bug 提示信息转换成了“对用户友好”的提示文案,结果是所有人看了都云里雾里。
我的观点是:bug 型异常压根不用考虑用户体验。
为啥?
因为系统出 bug 本身已经是非常糟糕的用户体验了,用户不会因诸如“哎呀,系统开小差了”之类的废话就变得好受些,用户真正关心的是尽快能正常下单。
此时的当务之急是快速修复 bug,所以提示文案的定位功能就非常重要,一段纯技术性的文字,对于用户来说可能是天书,但对于程序员很实用。
然而,这不意味着给到用户端的错误提示就可以为所欲为。如果我们为了方便定位便将整个程序调用栈 alert 出来,虽然可能并不会进一步拉低用户体验,但至少给人的感觉是不专业,而且过多的信息也意味着很容易暴露敏感信息(如程序路径、软件版本、SQL 语句),如果对方是个黑客,你只能自祈多福了。
另外要注重脱敏。大部分框架在数据库操作失败时,其 message 信息中都会包含诸如 SQL 语句之类的敏感信息,这类信息不可暴露到外面。
综上,我们可以采取文案+日志的策略,文案也就是展示给用户端的信息,文案中包含关键信息,log日志中则包含详细信息(包括调用栈信息等)
大部分的 DB 库抛出的异常都有共同基类(如 DBException
),我们可以针对这类异常做脱敏处理。
这也告诉我们另一件事:当我们自己开发公共库时,最好为该库定义一个统一基类异常,这样当使用者想要特殊处理该库抛出的所有异常时不至于狗咬刺猬无处下牙了。
另外,有些团队并不想记录业务型异常的调用栈信息(“卡余额不足”时,调用栈信息并无多大意义)。我们可以在框架层面定义个业务异常基类:BusinessException
,异常处理时不记录该类型的调用栈信息。
异常信息的另一个使用者是系统。包括其他服务、前端 js 脚本等。
系统只会,也只应该关注错误码。所以和 message 的随意性不同,code 应具备相当的稳定性。
同一个系统,如果 406 表示“用户不存在”,就绝不应该再用其他值(如 604)表示相同的含义。
另外,“code 面向系统”这一特点也要求 code 定义的是某一类异常(而不是某一个异常)。例如“订单创建失败”是一类异常,在业务代码中针对不同的失败原因有不同的 message,但其 code 都是一样的。
然而人类对数字并不敏感,要不同的程序员都保证写 throw new Exception('用户不存在', 406)
(而不是写throw new Exception('用户不存在', 604)
)是不可能的
所以需要将数字文本化,也就是定义错误码常量:
const USER_NOT_EXISTS = 406
代码中只能使用错误码常量:
throw new Exception('用户不存在', USER_NOT_EXISTS)
禁止使用字面量。
不过上面这段 throw 并不理想,首先默认类型 Exception 并不具备业务语义,另外开发人员如果硬是用数字字面量谁也没办法。更可取的方式是针对每种类型异常定义单独的异常类,该异常类仅允许传入 message,类内部自行绑定 code:
先总结一下中庸主义的异常捕获机制特点:
强制开发人员自己编写异常描述文案;
整个项目强制使用统一的错误码定义;
为业务型异常定义单独的基类;
关键信息脱敏处理;
统一错误码定义:
const OK = 200
const SYS_ERR = 500
const NOT_FOUND = 404
const NOT_ENOUGH = 405
const USER_NOT_EXISTS = 406
...
业务异常基类:
class BussinessException extends Exception {
...
}
异常类定义:
class UserNotExistsException extends BussinessException {
constructor(message) {
super(message)
this.code = ErrCode.USER_NOT_EXISTS
}
}
...
业务层使用:
...
if (!User.find(uid)) {
throw new UserNotExistsException(`用户不存在(uid:${uid})`)
}
...
控制器基类捕获异常
class BaseController {
...
errorHandler(err) {
// 是否业务型异常
const isBussError = err instanceof BussinessException
// 是否数据库异常
const isDBError = err instanceof DBException
// 生成用于跟踪异常日志的随机串
const flag = isBussError ? '' : random()
let message = err.message
if (isDBError) {
// 数据库异常,脱敏
message = `数据异常(flag:${flag})`
} elseif (!isBussError) {
// 非业务型异常记录 flag 标识
message += `(flag:${flag})`
}
// 记录日志(日志要记录原始的 message)
log(err.message, isBussError ? '' : err.stackTrace(), flag)
// 返回给调用端
this.response.sendJSON({"code": err.code, "message": message})
}
function log(message, stackTrace, flag) {
...
}
...
}
即便框架层提供了完善的异常处理机制,你还是无法阻止开发人员写这样的代码:
if (!User.find(uid)) {
throw new Exception(’系统异常‘, 500)
}
一行代码就给你打回原形!
所以异常处理机制是基于约定的(团队公约)。
技术 Leader 必须对全员做系统的培训,并公开制定团队代码规范,对不符合规范的 pull request 坚决打回,对屡教不改的要进行“小黑屋谈话”!
参考链接:
关于“错误码”设计方面的思考|系统异常,我****
交易核心、支付核心等通过状态机不可逆的特性来进行CAS防并发
update pay_order set status = 'SUCCESS' where id = xxxx and status = ‘PROCESSING’
基于kv的分布式缓存锁,来解决重复支付的问题
支付全链路保持接口幂等
超时、网络异常等通过异常表进行补偿
对账是数据一致性的最后一道防护
T+1基于账单,内外对账
关于系统的数据库连接数设置:
数据库连接池到底应该设多大?
支付路由
支付路由系统演进史
现代生活已经离不开的银行卡支付,背后原理其实没你想象的那么难!|原创
轻轻一扫,立刻扣款,付款码背后的原理你不想知道吗?|原创
支付那些事儿
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIzMTgwODgyMw==&action=getalbum&album_id=1337216649245655040&scene=173&from_msgid=2247486352&from_itemidx=1&count=3&nolastread=1#wechat_redirect
订单-支付系统流程
为啥用户多次重试支付时不能重新生成订单:
1. 一方面生成订单的过程存在各种检测,是很重的操作;
2. 重新生成订单,那还要重新算优惠、选券、用积分等,由于存在时间差,这时候算出来的结果跟之前的可能不一样了,用户就会投诉。
所以,实际支付场景,到达支付阶段时,所有订单的东西都已经生成快照了
哎,编程为什么那么难?
加签验签的原理
程序员必备基础:加签验签_码农code之路-CSDN博客
手机没网了,却还能支付,这是什么原理?|原创
支付异常的常用解决方案
钱被扣走了,但是订单却未成功!支付掉单异常最全解决方案|原创
一笔订单,但是误付了两笔钱!这种重复付款异常到底该如何解决?|原创
抄答案就是了,两套详细的设计方案,解决头疼的掉单问题|文末又又送书
掉单的两种方案,异步补偿方案可以采用如下两种:
定时轮询补偿方案
延迟消息补偿方案
在线支付系统:1.支付宝对接-手机网支付接口-场景介绍_哔哩哔哩_bilibili
1.支付流水导入对账系统数据库(一般通过mq消息)
也就是支付系统将一笔笔支付数据通过MQ,下发到专门的对账系统。但是消金内部对账系统是内嵌在TDC中的,没有成一个独立系统。
2.为每个对账周期每个对账渠道生成一个对账批次数据,记录该周期内的对账结果。
消金是一天一次对账文件,也就是一天作为一个对账周期,也就是搞张渠道对账周期控制表,下载A渠道某一天的对账文件,就在该周期控制表中插入一天记录,状态为INIT,并增加一个对账文件地址字段
3.对账系统调用各个渠道进行账单下载,转换,得到统一格式的流水,保存到本地数据库的三方对账支付流水表中,将账单文件存储到文件服务器,渠道方的原始对账文件地址存到2中描述的对账批次数据中,以备后序财务查询使用,或者外部审计复核
将从渠道方下载回来的对账文件,存储到公司自己的文件服务器中,并把存储好的文件地址,存储到步骤2中的周期控制表中的对账文件地址字段中
4.支付核对三方渠道:以对账批次为纬度,以日期加状态成批取支付流水,通过支付流水中的请求三方订单号关联到3中录入的渠道侧流水
●如果能关联上,状态金额一致则对账无误,双方标记成功
●如果支付未关联到三方数据则对支付流水标记少账
●如果双方金额不一致或状态不一致则双方流水均标记差异
本地支付流水表,和三方对账支付流水表都应有一个“对账结果”字段,对账成功,则两个字段都标记为SUCCESS。支付未关联到三方数据,那么就说明本地的数据多了,三方的数据少了,对应到消金就是:消金有支付成功的记录,但是上海银行没有记录,其中一种场景就是,上海银行对该笔交易发送了冲正,但是消金侧未收到该冲正请求
5. 三方渠道核对支付: 与4中方向相反使用未核对的三方渠道流水来核对支付数据
●三方为未联支付,记多账
6. 对账差异的原因很多,有测试订单的原因,有系统漏洞导致识别渠道结果不准确的原因,有对账单解析逻辑错误的原因,也有可能是系统密钥泄露的原因,对账差异必须重视,所有渠道账单里面,出款账单核对最为重要,转账订单差异可能导致严重资损。
7.额外聊聊在开发维护对账系统时的一些小经验
●对账一定要实现自动化,能自动核对告警,而不是依赖人工到后台下账单
●对账能力优先支持用于出款的渠道,这部分渠道非常容易出大事故
●对账文件过大可以不用一次全部加载整个账单,一行一行地转换数据,分段录入就好了,通过数据库唯一键保证账单解析和保存即可。
Ps:
日切,所谓日切就是我方是今天的支付请求到渠道侧正好跨天了,这样渠道侧在次日生成的账单里就不会有这条数据,得等后天才会有,这种一般就是上述的少账问题,可以在通过T+2核对少账数据来确认这部分数据是否有问题。
结算
结算一般是每日结算一次,一般是商户有一个银行卡,支付系统也有自己在银行的公共账户。结算实际上就是一个定时任务,每天跑一次批,遍历出所有需要结算的商户,然后根据商户号,查出支付系统下今日有多少是该商户的订单,然后汇总这所有的订单金额,最后扣除手续费,把剩余的金额打到商户的银行卡上去
关于超时订单取消场景
最佳实践:基于分布式定时job
其他方案:比如rocketmq延迟消息
技术选型:对于超时时间在24h以内,对超时精度有要求的场景,推荐使用rocketmq的延迟消息的解决方案;在电商业务场景下,许多订单的超时时间都在24h以上,对于超时精度没有那么敏感,并且有海量订单需要批处理,推荐使用基于定时任务的跑批解决方案
从零开始设计对账系统 - 小黑十一点半
嗨,我想和你分享一下千万级支付对账系统是怎么设计的。
关于项目亮点可以从以下几个方面着手:一致性、稳定性、高并发、产品技术思维
一致性又分为:事务的一致性、分布式的一致性
分布式的一致性,就是指逻辑上应该保持相等的部分。主要是由于数据复制replicate存储在不同地方但是逻辑上应该一致。数据复制主要是从两方面进行考虑:
从性能考虑,比如内存拷贝到CPU内的L1 L2缓存的数据一致性,CDN、分布式缓存这都是通过复制副本来加速查询,CDN就是让数据离用户更紧
从安全考虑,数据库做主备,主挂了从立马就可以上,这就实现了高可用,这时主备之间就涉及到数据同步,这时只要是数据是有状态的,那么就可以出现数据丢失数据不一致
想实现分布式一致性,就会引出CAP理论
单机性能有限所以现在构建大型系统就有两个方向,一个是纵向增大单机性能,一个是处于弹性的考虑就是使用横向扩容。所以,我们强调尽量让应用机器做到无状态,无状态就意味着可以横向的扩容
有状态就要涉及到高可用,涉及到高可用就要去复制数据,只有多个数据副本才能保证高可用,涉及到数据复制那么就需要考虑这复制的多份数据的一致性的问题
比如,应用机器就是无状态的,而数据库中的数据,redis中的缓存数据等大多就是有状态的随时可能被改变状态的
有状态 ---> 高可用 ---> 多副本 ---> 一致性
注册中心更适合用AP
因为注册中心如果不保证C的话,会出现的后果,就是可能有微服务上线,有些微服务去读某个注册中心实例的时候可以读到新注册的服务,有些微服务去读还没更新好的注册中心可能读不到新注册的服务,这个不会有影响。如果是某个服务下线,同理,有些服务感知到了服务下线,有些感知不到,感知不到的话会导致请求失败。而请求失败我们可以用熔断+降级+重试来进行解决。同时注册中心需要能扛住高并发,也就是能服务于各个服务来抓注册表,心跳等等,也就是说微服务越多,要接受的qps越多,所以qps还是非常高的,而如果用了CP,必然无法扛住这么高的请求。综上,注册中心更适合用AP,也即是:服务发现这个场景下,可用性比一致性更加重要
有状态的服务如何保证一致性?
大多互联网系统还是使用的最终一致性,也就是保证了AP。金融系统可能会使用强一致性,也就是保证CP。所有互联网架构大多是在CP和AP之间进行取舍
如何有状态的数据之间的强一致?
可以先加一个分布式锁,让所有的读请求都阻塞住,等所有的节点全更新完后,再把读请求放进来,这是所有客户端读的数据都是一致的
如何实现有状态数据的最终一致性?
因为性能损耗,并且改造量太大中台后台系统都要联动改造所有一般不使用分布式事务,而是使用补偿机制来保证最终一致性。具体的补偿机制,定时任务兜底补偿、MQ延时消息重试补偿、本地消息表+定时任务扫描重试补偿、记录日志手动补偿、总之一句话适合自己业务的才是最好的
以订单支付场景为例,讲述如何实现最终一致性:
这时,如果支付系统到业务系统之间的MQ流程出现系统,那么还有定时任务来做兜底。这时如果把支付系统改成派单系统也能无缝衔接,订单支付成功后,保持订单系统和派单系统之间的一致性 。
支付系统集群全部崩了不能支付,因此订单也是待支付,一致性还是存在的。在支付后,订单系统崩了,但订单系统是集群,不可能全部崩溃,因此一致性还是存在
兜底的定时任务,也是需要失败重试的,当然重试次数要设置上限,超过上限以后就要告警通知开发人员人工处理,一直重试失败大多是系统有问题,需要开发人员查日志排除问题最后修复该笔数据
对于跨库的事务,比较常见的解决方案有:两阶段提交2PC、三阶段提交(ACID)但是这 2 种方式,在高可用的架构中一般都不可取,因为跨库锁表会消耗很大的性能
高可用的架构中一般不会要求强一致性,只要达到最终的一致性就可以了
最终的一致性的几种方案:
- 事务表:本地消息表(失败记录表 + 定时任务重试)
- 消息队列:事务消息
- 补偿机制:回滚、重试
- TCC 模式:占位 / 确认或取消
- Sagas模式(拆分事务 + 补偿机制)来实现最终的一致性
seata提供了tcc模式的支持,TCC是一种补偿型分布式事务
业务补偿的过程进行一个定义,即当某个操作发生了异常时,如何通过内部机制将这个异常产生的「不一致」状态消除掉
分布式系统中的业务补偿设计的实现方式主要可分为两种:
回滚(事务补偿) ,逆向操作,回滚业务流程,意味着放弃,当前操作必然会失败;
重试 ,正向操作,努力地把一个业务流程执行完成,代表着还有成功的机会。
Ps:因为「补偿」已经是一个额外流程了,既然能够走这个额外流程,说明时效性并不是第一考虑的因素。所以做补偿的核心要点是:宁可慢,不可错
比如,消金二类户正向交易流程对应的冲正流程,就是上海银行进行的一个业务回滚,从而保证数据一致性的典型
重试
“重试” 的语义是我们认为这个故障是暂时的,而不是永久的,所以,我们会去重试。这个操作最大的好处就是不需要提供额外的逆向接口。这对于代码的维护和长期开发的成本有优势,而且业务是变化的。逆向接口也需要变化。所以更多时候可以考虑重试。
重试的使用场景
相较于回滚,重试使用的场景要少一些:下游系统返回请求超时,被限流中等临时状态的时候,我们就可以考虑重试了。而如果是返回余额不足,无权限的明确业务错误,就不需要重试。一些中间件或者 RPC 框架,返回 503,404 这种没有预期恢复时间的错误,也不需要重试了。
重试策略
重试的时间和重试的次数。这种在不同的情况下要有不同的考量,主流的重试策略主要是以下几种:
策略 1 - 立即重试 :有时候故障是暂时性的,可能因为网络数据包冲突或者硬件组件高峰流量等事件造成的,在这种情况下,适合立即重试的操作。不过立即重试的操作不应该超过一次,如果立即重试失败,应该改用其他策略;
策略 2 - 固定间隔 :这个很好理解,比如每隔 5 分钟重试一次。PS:策略 1 和策略 2 多用于前端系统的交互操作中;
策略 3 - 增量间隔 :每一次的重试间隔时间增量递增。比如,第一次 0 秒、第二次 5 秒、第三次 10 秒这样,使得失败次数越多的重试请求优先级排到越后面,给新进入的重试请求让路;
return (retryCount - 1) * incrementInterval;
策略 4 - 指数间隔: 每一次的重试间隔呈指数级增加。和增量间隔一样,都是想让失败次数越多的重试请求优先级排到越后面,只不过这个方案的增长幅度更大一些;
return 2 ^ retryCount;
分布式系统业务补偿设计的注意事项:
因为要把一个业务流程执行完成,需要这个流程中所涉及的服务方支持幂等性。并且在上游有重试机制;
我们需要小心维护和监控整个过程的状态,所以,千万不要把这些状态放到不同的组件中,最好是一个业务流程的控制方来做这个事,也就是一个工作流引擎。所以,这个工作流引擎是需要高可用和稳定的;
补偿的业务逻辑和流程不一定非得是严格反向操作。有时候可以并行,有时候,可能会更简单。总之,设计业务正向流程的时候,也需要设计业务的反向补偿流程;
我们要清楚地知道,业务补偿的业务逻辑是强业务相关的,很难做成通用的;
下层的业务方最好提供短期的资源预留机制。就像电商中的把货品的库存预先占住等待用户在 15 分钟内支付。如果没有收到用户的支付,则释放库存。然后回滚到之前的下单操作,等待用户重新下单
TCC
TCC 也是一种两阶段提交协议,可以看作 2PC/XA 的一种变种,但是不会长时间持有资源锁
分布式事务模型--TCC_tcc模型_wh柒八九的博客-CSDN博客
这篇文章对TCC的概念定义等等做了介绍
分布式事务之TCC机制 - 简书
这篇文章对TCC所需要的表结构做了介绍,比如除了需要订单主表,还需要一张辅助的资源锁定的子表
最终一致性的详解部分见:RocketMQ应用篇_每天都在想你的博客-CSDN博客
大量请求访问失败,服务器资源正常,检查发现数据库连接被耗尽
事务操作期间,数据库连接会被一直占用、只有事务操作完成commit之后,连接才会被释放,所以说上线数据库连接被耗尽是数据库事务操作时间过长
比如事务操作中,有HttpClient访问第三方接口,由于第三方接口状况不可控的原因,导致以上问题。所以,对于网络请求,一定要设置合理的超时时间
Ps:
关于数据库连接池:
当前主流的数据库访问都是采用的“连接池 + BIO”的方式
访问MySQL需要通过JDBC,而当前市面上所有实现JDBC的驱动都是使用的BIO阻塞模型的,也就是一个连接对应MySQL实例中的一个线程,而MySQL的事务规定了同一个连接上的事务必须是依次执行的,也就是说在同一个连接上执行多个事务,并不能交替进行而是必须将事务的操作进行排队然后依次执行,如果像你说的只保持一个连接的话,假如有个非常耗时的事务,那不是把其他事务都给阻塞住了?所以只能使用多个连接对应多个线程,线程池也应运而生
未来的设想
未来实现一种新协议,以阻塞的方式访问数据库,Mysql一般来说瓶颈是磁盘呀,如果能搞成单连接非阻塞的,一个服务单线程就能有很高的吞吐QPS了(因为当前的网络带宽千M宽带,支持万级以上的传输是没有问题的,所以瓶颈不在传输,而在磁盘)
连接池优势
对连接的生命周期管理,程序员不需要关心连接的创建销毁等,简化了编程模型
性能优势,创建销毁连接都是耗费性能的
为什么很少见公网环境的httpl连接的连接池?
网络环境,连接是为了复用,如果公网网络不佳,连接经常断开难以保持,那建立连接池的意义就不大了,使用短连接的http请求,可能是更好的选择
成本问题,连接池的启动是需要耗费资源的,而客户端APP多了以后,对服务端连接的占用也会急剧增加,继而增加服务器成本,所以我们一般不在控制不了数量的客户端上建立连接池
而且,连接池连接数量可控这一特点,还可以形成对后端服务的一种保护,形成流量控制的效果,令牌桶的限流就是这种原理,一个个令牌就相当于一个个连接
架构技术:连接池的本质是什么_哔哩哔哩_bilibili
曾经userId在很早的版本就加了但是没有使用,后来过了半年多又用这个字段了,以为是老字段Y又是主要业务字段,所以没有检查这个字段是否存在索引,最后在流量冲击下,AMS停止服务了三小时,AMS账户系统停了三小时,那整个消金也就停止进件了3小时...