原则:
支付系统思考思路: 先考虑正常case, 先考虑并发[重复支付, 押金退款,提现], 再考虑事务中断.[重复支付]
能幂等先幂等(需要采用数据库事务), 不能幂等优先选择资金安全的方案. [退款和支付的不同处理]
避免事务中断,尽量采用面向对象嵌套法,每个内部子模块获取自己的数据,开头都需要对需要判断子模块实体的状态,外部系统写操作放在最后.
对账对出事务中断的部分: 修改状态后人工修复.
角色,用例,模块,依赖, 抽象,导致. [优惠模块为什么放置到订单之前,又双向依赖订单,而不是放置再支付中的原因]
从业务抽死剥茧出头疼地并发场景.
架构重构,千万不要盲目抽到新的模块. 没想明白之前,循环依赖会搞死你,虽然可以依赖倒置的概念,但maven不支持循环依赖. 测试驱动重构.
看得懂,讲明白下面的这些文章就出师了:
重构: 支付宝鲁肃:支付宝全局架构重构实践 http://www.infoq.com/cn/presentations/cl-refactor-system-arch/
CAP theorem 理论在 多副本存储 和 分布式存储中理解
微服务架构的分布式事务解决方案 龙果学院 含源代码
tcc github源代码 https://github.com/search?utf8=%E2%9C%93&q=TCC&type=
TCC是一个理念,其由Atomikos公司的创始人提出,如果想了解其具体内容直接到其官网下载个白皮书看下就好了,任何时候都是看官方文档才能更准确的获知答案。不过TCC只是分布式事务中的一个选项,且并非最优选项,这里有篇文章介绍https://github.com/QNJR-GROUP...
https://github.com/QNJR-GROUP/EasyTransaction 含多种模式 和 复合
https://github.com/prontera/spring-cloud-rest-tcc
try 就是冻结. 冻结资金,冻结券,或者直接修改订单状态. confirm时啥都不做. tcc三个步骤都需要保证幂等,否则问题很大. 如何做到自动幂等?
通过几天的资料查找,对解决分布式事务的方法有两阶段提交、支付宝分享的TCC(try-confirm-cancel)和基于消息的最终一致解决方案,其中第一条和第二条虽然也能解决问题,但普遍对第三种基于消息队列的最终一致解决方案推荐多比较高,所以第一条和第二条可以参考使用。 from 分布式事务方案整合
订单处理:本地事务
资金账户加款、积分账户增加积分:TCC型事务(或两阶段提交型事务),实时性要求比较高,数据必须可靠。
会计记账:异步确保型事务(基于可靠消息的最终一致性,可以异步,但数据绝对不能丢,而且一定要记账成功)
看看别人对资金安全的总结 http://www.infoq.com/cn/presentations/correctness-ensure-of-funds-in-internet-financialsystem
商户通知:最大努力通知型事务(按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对)
业务 | 方案1 | 方案2_黑ma |
支付 |
订单支付 押金支付 出行卡支付 月卡充值 充值支付 |
bill引擎,回调各业务实体. getFee 时新建实体,还是 success 时新建实体 |
支付和费用变更 |
费用不一致就退回. 问题,券不可退. |
多退,少补的原则. 记录费用详情. (拆分同一个费用项 id) |
退款和打款 |
退款记录再退款表 打款记录在提现表 |
退款和打款都记录在订单表. 打款依赖提现表. 提现表有两种类型: 司机提现, 退款打款. |
费用,优惠记录 |
静态字段标识. 券 id. 后来优惠越来越多,开始意识到需要抽象出 支付表: 含支付类型,支付 id (券 id)等 |
抽象出支付表 |
会计 |
无会计统计 |
基于帐户地变动去进行会计统计. [ ] 记住: 借代表增加,贷代表减少. 业务收入 1、原借款时:借:其他应收款 2200 |
结合会计
- 记账
- 复式记账.
- 会计凭证和账单.
- 司乘分离的差额. 需要记录. 不能放在 pay 表里.
- 多个收据对应一笔帐户变动
- 何时冻结费用变更
- 方案一 尝试支付后不可更改费用. 冻结费用最早进行.
- 缺点明显,一旦尝试支付过就不能变更费用.
- 方案二 冻结费用在网关层回调业务方. 先冻结,然后修改支付成功.
- 缺点:方案复杂,解决万分之一的问题
- 方案三 不冻结. 修改费用对主体状态的变更(还需支付,退款) 和 支付成功通知对主体状态的变更 需要并发加锁
- 如果支付通知的费用和 现有费用不一致, 状态仍然为待支付. 判断期间费用不可变更
- 变更费用后,仍需支付.那么就还需要支付. 如果需要退款.那么就退款.
- 方案一 尝试支付后不可更改费用. 冻结费用最早进行.
- 支付和退款退的那部分费用是什么?
- 需要和费用模块联动.费用历史.
- 方案一 费用模块用version, 每次变更保存新的费用项和增加 version. 费用模块保存 versionList
- 方案二 支付保存费用项 list, 每次自己做计算.
- 方案三 不用关心支付的费用是什么? 只关心总金额. 缺点是不满足有时候退款,需要根据费用来确定渠道退款顺序.(押金等)
- 需要和费用模块联动.费用历史.
- 如何重复支付判断?
- 方案1: 实体支付成功,我方未成功. 即重复支付.
- 考虑并发情况.
- 两笔支付都修改实体为支付成功,
- 如何支付溢出判断?
- 支付成功时,
- 锁定实体.
- 计算已支付金额..
- 修改自己状态. 此时其他流水无法修改自己状态. 已经分布式锁住了唯一健. 有并发竞争的复杂 case. 表面上只改一个整体的状态,但实际上是对计算逻辑有竞争.
- 是否支付溢出,那么就退款
- 释放实体
- 第二笔锁定实体. 计算已支付金额和已退款金额.
- 代码能处理正常业务逻辑, 但是能否处理事务中断后的重试? 如何面对 mq 或者 dubbo 的重试?
- 要做到幂等(见下面条目)
- 如果做不到幂等,那么就选一种没有资金损失的方案?
- 案例1 重复支付判断
- 原有逻辑
- 实体未支付, 属于正常支付成功case. 修改实体为已支付,修改账单未已支付
- 实体已支付, 属于重复支付case ,退款. 计算下多支付了多少钱, 多付了哪些费用项, 把多的进行退款.
- 问题: 假设后续流程出错,整个流程重试, 实体已支付 ,认为属于重复支付 case,贸然退款就有问题,出现资金损失.
- 解决方案1: 将自账单和实体统一起来判断.
-
- 先判断: 判断实体已支付,我方已支付. 略过. 实体已支付,我方未支付. 重复支付. 实体已支付,我方支付. 已经修改过状态
-
- 缺点: 假设正常case下修改实体为已支付后, 事务中断. 然后被 dubbo 或者 mq 重试.
- 问题: 实体已支付,账单未支付. 变成重复支付,退款.资金损失.
- 解决方案2: 重试最好导致的问题是本来要退款的变成了不退款. 案例思考: 先改账单支付,再改实体支付. 事务中断, 刚好此时实体被另外一笔支付支付. 然后被 dubbo 重试.
- 问题: 账单已支付,实体已支付. 略过进行下一步. 本来要被退款的被忽略了. 乘客自己会来打电话,或者对账系统能对出来. 支付的金额(减去退款) !=订单的总费用
- 原有逻辑
- 案例2 帐户加款
- 幂等. 只有做到了幂等,才能进行重试. 但是有些情况下,重试是mq,或者 dubbo 自动控制的, 如果没有肯定出现问题,选择一种不资损的方案.
- 方案一: 流水+事务 [能够抵抗微服务拆分,分库]
- 方案二: 状态前置判断法,
- 1.设置事务: 内部操作流水+状态修改处于一个事务中,写外部系统无法,可以放在最后?
- 缺点:不能够抵抗微服务拆分,分库. 例如:重复支付判断?
- 2.不设置事务: 相信内部操作流水+状态修改处于一个系统,会尽可能得处于一个事务中. (要使用面向对象编程法: 形参传入最上层的 beanWrapper,内部包含了所有对象,所有外部数据都先获取好,组装好. (更新除外) 注意: 不要按需获取,避免调用外部挂掉. ) 否则中间随意调用外部系统,写操作除外,可以放在最后?
- 缺点:肯定会出现事务中断,要选择一个没有资金损失的方案.
- 1.设置事务: 内部操作流水+状态修改处于一个事务中,写外部系统无法,可以放在最后?
- 双实体幂等控制(多对1操作) 都在同一个系统和数据库下,相信极大情况下都是一个事务内的. 可以通过对多和1的 双状态判断来进行幂等控制. [ 例如: 多笔账单对实体的支付成功逻辑, 基本上不会在实体侧保存一份支付成功流水,因为同一个数据库. 但如果微服务拆分后就不一样了,微服务拆分引发的幂等重试, 所以这种最好是幂等略过的方式 ,这样虽然数据会不一致,但是不会引发问题, 重复退款等.]
- 多方支付成功,1方支付成功. 略过 .
- 多方支付未成功,1方支付成功. 说明已经成功 (不考虑中断在中间的情况, 因为不采用分布式事务,也无从考证)
- 多方
- 双实体幂等控制(多对1操作变更)幂等判断, 两个实体在不同的系统中,必须要求1方记录多方的操作流水号. 例如 多笔支付重复支付和幂等判断 , 帐户的变更幂等判断.
- 回调时金额不一致的处理. (有了支付溢出专题讨论后,这里就简单很多了)
- 极端场景描述:
- 乘客需支付10元
- 唤起 app 尝试支付10元.未回调
- 改费用,改成了9元.
- 支付9元
- 10元回调回来.
- 9元回调回来.
- 两个方案
- 方案一,先设置为支付成功,再由业务方来确定是否全额退款,还是部分退款.
- 如果是全额退款的. 就会照成问题. 和正常支付成功且部分退款的如何区分? 本质上不用区分.就需要看下未退款金额即可.
业务上需要判断是否已支付. 不能单纯地看成功支付的笔数,而是要看总金额.
- 如果是采用多退少补的方式. 如何明确退款对应的费用? 费用 version_from version_to. 押金多少都不动.已账单为主.
-
- 乘客需支付10元
- 唤起 app 尝试支付10元.未回调
- 改费用,改成了9元.
- 支付9元
- 10元回调回来. ( 设置实体为支付成功,设置为支付成功,并且退1元. )
- 9元回调回来. ( 设置为成功,设置实体支付成功失败, 全额退款. 不退.)
-
- 如果是全额退款的. 就会照成问题. 和正常支付成功且部分退款的如何区分? 本质上不用区分.就需要看下未退款金额即可.
- 方案二 及早询问业务方是否费用变更. 同时冻结费用(越早冻结越好),如果变更如何应对.
- 这样就会出现
- 多退
- 如果是多退部分,支付成功并部分退款
- 如果是多退全部,支付关闭并全额退款. 这部分退款不需要对应着费用. 做一个标记,是全额原路退,还是业务退款.
- 少补
- 少的部分需要重新交. 交的那部分费用是什么,用 费用项表示. 负数
- 少退
- 如果是少退全部, 支付关闭并全额退款. 这部分退款不需要对应着费用. 做一个标记,是全额原路退,还是业务退款.
- 多退
- 这样就会出现
- 方案一,先设置为支付成功,再由业务方来确定是否全额退款,还是部分退款.
- 极端场景描述:
- 支付有哪些抽象概念?
- 费用:
- 优惠 (最开始单一的券,就简单的用字段描述,后面发现优惠多了,只能用关联表):
- 券
- 权益
- 包月
- 积分
- 哪些是应收款项
- 优惠的部分
- 哪些是显性成本:
- 优惠的金额. 首单减免,包月.
- 优惠券
- 权益折算成的减免时间,最终和原时间的差价
- 哪些是隐形成本:
- 哪些是隐形收入:
- 包月部分
- 余额部分
- 哪些是用户关心的支付.
- 余额+线上支付等
- 关闭全额退款还是二次支付?
- 费用是可变的,会导致实体状态是并发修改的? 回调流程,何时冻结? 是否需要提早冻结,层次很多的情况下.最底层网关层回调时是否就需要冻结?
- 像滴滴, 黑马一层(可变,有并发),收银台一层(可变,有并发,一旦并发,就看谁先锁住,回调先锁住,状态无法回溯的话,那么就生成新的实体.最好是如此.),phoenix 一层
1. 抽象不同的阶段. ( 引擎模式,更让人思考, 每个业务增加一个流程点, 越上游收集越好,例子是 回调回来费用变更.)
不同的阶段,分类主体不同. 比如计费阶段,根据业务分. 渠道阶段,根据渠道分.
2. pay 表 只保存乘客支付金额和优惠总额. 1 对多 优惠表
3. 优惠表里保存优惠类型,支付帐户,总金额.
4. 差额表里保存差额凭证
业务和统一收银台的交互 ,和 业务和 phoenix 帐户系统的交互没什么区别. 但是区别在数据上,都利用了签名,利用客户端 以及 签名的把两个服务端的交互解耦掉了.(微信就没解耦,微信和网络影响服务器资源)
区别: 1. 统一收银台不仅传递了支付金额,还传递了各项费用. 面向的 orderId. 一个 orderId 对应多笔支付. 内部封装掉.
这种做法比较巧妙. 比较适合用在通用组件上.(需要回调业务方. 客户端回调,解耦服务端)
表结构设计:
函数依赖要存储.因为可能会变. 老的订单会有问题. 卢peng 说不要存储.