交易一致的由来
交易一致的由来
警告
交易一致问题是目前分布式环境下的最主要的矛盾之一,也是最容易出问题的地方.这个问题横跨所有的框架,技术栈,编程语言.
引发这个问题的根源,是之前所有的计算机科学的理论和现在分布式的玩法之间的矛盾.我们不可能在不清楚原理的情况下,找到答案.
什么是交易一致
我们在平常的IT建设中,难免会开发到比较核心的业务场景,这种核心的业务场景要求一般都比较苛刻,提供的服务又比较关键.
我们以XX宝的出款到银行卡为例(这个例子大家都比较熟悉,而且比较关键),同时对场景做出如下假设

- 出款到卡排除各种内部原因,一定要成功,假设用户提交的一个出款到卡的申请,在没有意外的情况下,应该出款成功.
- 我们假设直接调用银行接口,而银行方面没有做任何限制,调一次就出款一次.
- 我们假设在系统的代码外没有任何技术工具,只有我们开发的系统.不存在分布式事务的平台,就算有,银行也不会帮你部署到他们的环境.
注意
交易一致,就是如果在我们的系统里面,出款的订单状态是成功的,那么一定是有一笔金额已经调用到银行接口,并成功的进行了转账,而且只有一笔,不管中间是什么原因,都要完成.
本地成功,远程成功,或者本地失败,远程失败,都是一致的.
本地成功,远程失败,或者本地失败,远程成功,都算不一致.
典型的出错案例
如果是一个没有写过在线交易类的程序员,设计出来的方案可能是这样的. 设计一张表来存储所有有的用户请求,假设表名为USER_APPLY(用户申请表),
| 订单号 | 用户id | 订单信息 | 状态 |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
没有接受过金融系统训练的程序员,代码的编写可能是这样的
controller{
插入订单数据
订单处理(订单信息)
}
@事务
订单处理(订单信息){
更新状态为出款中
出款结果 = 出款接口(订单信息)
保存状态
}上述的代码在测试环境完全不会有问题,但是为什么上了生产环境,某些时候就会出问题呢??
- 在正常情况下,"出款接口"的调用,一般会在100ms内返回,但是如果出现特殊情况,这个接口会不会需要100s才返回??
- 因为"出款接口"是一个网络请求,这个请求返回了timeOut,请问保存的是什么状态???
警告
上述2个问题引发的后果相当严重
- 如果"出款接口"在100s内返回,因为"@事务"的存在,当前的应用会长时间占据数据源中的连接,导致当前JVM的其他服务,会因为无法获取数据库连接而直接挂起,直接造成当前应用的假死状态
- 如果"出款接口"返回了一个timeOut,这个timeOut有2种可能,一种是请求在发起的时候,就没有发送到服务端,产生的超时异常,另外一种是请求在已经发送给服务端,服务端在返回的时候出现了各种问题,导致客户端无法接收到response产生的timeOut,所以,这边无论timeOut之后保存什么值,都是错的
在上述的意外以外,我们的程序会不定时的更新上线,如果在上线过程中,因为没有"优雅停机"的保护,可能会存在一笔订单在执行过程中被kill掉,这个kill的过程可能是在"出款接口"返回后,没有保存状态或者事务提交前.这就造成当前这笔订单在本地库中看来,是没有任何处理的,而在远程看来,当前订单已经提交服务器处理过一次.如果再匹配订单补偿,就会出现一笔订单出款多次的情况.很多公司就是死在这种事情上.
代码修正1
上面的解决方案在实际运行过程中会出现很严重的问题,所以我们会对上述的方案进行修正,修改后的代码往往是这样的
订单处理(订单信息){
@事务{
出款交易处理中
}
出款结果=银行出款();
@事务{
保存出款结果
}
}注意
我们通过对对本地事务的调整,来弥补上述问题.上面的问题会被解决. 唯一要注意的是,当出款交易的状态是处理中的时候,我们需要先检查查询银行当前交易是否已经被调用. 但是对于出款业务来说,银行调用()是有可能在银行端处理失败的,而交易号可能不能被重复使用.所以为了弥补莫名其妙的出款问题,并且可以重试,我们需要再次修正.
提示
如果业务中没有"静默成功"的选项,该方案是没有漏洞的.
代码修正2
原本我们的交易模型是一笔交易对应一次银行出款(),如果银行调用返回失败,但是我们的出款业务还要继续下去,下一次的银行出款()可能就会成功,所以之前定的交易模型应该增加另外的模型,出款申请.
| 申请号 | 订单id | 状态 |
|---|---|---|
| 0 | 0 | 0 |
同时我们的出款的代码可能会实现成这个样子
订单处理(订单信息){
@事务{
出款交易处理中
插入出款申请
}
出款结果=银行出款();
@事务{
保存出款结果
}
}警告
分析上述代码也会存在下面的问题
- 一笔出款申请如何能保证只调用一次银行出款()?在上述的代码里面并没有体现.
- 银行出款()的调用完全取决于出款申请的记录数.而在出款申请中,我们不能通过简单的唯一索引让一笔订单只生成一个申请,一旦出现多个申请记录,有可能都是成功的.
上述的代码在多线程的环境下,会存在重复出款的隐患.
代码修正3
为了满足业务的需求,我们引入了新的模型,导致之前修正2的代码直接移植过来的时候又会出现重复出款的问题.修复的代码可能是这样的.
当然,补单部分的伪代码就没有贴出来.理论上是,在哪边停留,就需要在哪边再次执行.
订单处理(订单信息){
if(锁(出款订单)){
@事务{
出款订单出款中
插入出款申请
}
}
if(锁(出款申请)){
@事务{
出款申请出款中
}
出款结果=银行出款()
@事务{
保存出款结果到出款申请
保存出款结果到出款订单
}
}
}直到目前为止,我们才算完美的解决了出款这样一个问题.
问题抽象
上述问题是金融行业内比较常见的出款示例,现在是整个IT都经常出现的问题,不分框架,不分技术栈,而且普遍存在.在目前微服务的架构体系下,尤为突出.而且为了解决这个问题,每个公司都有自己不同的方案.从事务平台到分布式数据库,都是现在的技术热点.
我们可以对上述业务代码,进行简单抽象,得到如下的编码示例

所以我们最终获得了一个原子性的代码模板,上述的业务实际上是由2-3个代码模板一起组合实现的.
if(锁){
结果=do(订单)
@事务{
save(结果)
}
}最终示例中的提现是一个由上述原子操作组成的流程操作.无非就是在不同的步骤里面,可能锁=null,也有可能是do=null,还有的可能是save=null
上述思路就是qilu-state的抽象的由来.
和分布式事务的关系
我的框架和分布式事务平台,最终都是希望,在各种可能出现不一致的情况下,通过补偿修复的方式,保证数据一致.
粒度差异
无论是TX-LCN还是seata(GTX),都是分布式事务的平台,一个平台要运行起来的复杂性远远大于框架对于代码的要求.
这种复杂性会体现在应用的逻辑部署结构上,也会体现到每次线上问题排查上.
理念差异
分布式事务平台,都具备事务协调器,事务管理器和资源执行器的概念.这些概念的前提是通过RPC的调用,想借助RPC的调用过程,协调RPC执行过程中的事务,进而达到一起进退的目的.
我的框架是直接剖析远程调用和本地事务,最终的解决方案是通过代码分离的方式,让事务内部不能包含远程调用.
双方理念不合,孰优孰劣,不做评价
适用性差异
分布式事务平台,一定要求越来越多的系统接入,最好是所有系统都接入.但是,就算是所有系统都接入,也存在问题,比如上述出款的示例,2家公司之间的通讯,是无法使用中间平台来协调的,这个就是分布式事务平台的局限性.
但是作为框架来说,以极小的侵入性,通过规范编码方式,带来的可靠性,我个人觉得目前是最完美的方案.
提示
如果这篇文章看不明白,请多看几遍,否则后续的交易一致的文章,你无法看懂.
