事务与柔性事务
作者:茅晨栋
上海华瑞银行数字银行开发中心资深架构师
ACID
在介绍事务与柔性事务之前,有必要介绍一下事务的核心属性,即ACID,原子性,一致性,隔离性以及持久性。网上相关的资料非常多,这里不错赘述。简单来说就是作为一个事务中包含的所有逻辑处理操作,作用在数据库时,左右事务中的所有操作都成功,对数据库的修改才会永久更新到数据库中,任何一个操作失败,对数据库之前的更新都会失效。
传统单体架构场景下,数据库事务会非常好的保证业务的一致性,但是在分布式场景下,会暴露出数据库性能和处理能力上的瓶颈,所以在分布式领域,基于CAP理论,在其基础上衍生出了BASE理论。
CAP
即一致性,可用性以及分区容错性
cap理论核心是一个分布式系统,不能同时满足三个特性,最多只能满足两个。
放弃分区容忍性。
一种做法是将所有事务相关,放到一台机器上,系统被迫从分布式退化成单机,从根本上失去可扩展性,这个选择一旦业务起量,会严重影响系统规模。
放弃可用性。
相对于放弃分区容忍性来说,其反面就是放弃可用性。当遇到分区事务,受影响的事务需要等待数据一致,在等待期间无法对外服务。简单来说就是加资源锁,传统的分布式事务解决方案,两阶段提交,就是这样的设计思路,后面会详细介绍一下啊。这种方案在多节点参与的事务场景下,会有很大的问题。(zookeeper,Hbase)
放弃一致性。
在分布式架构盛行之前,传统单体架构中,出现分区的情况很少,一般会满足一致性和可用性。但是在分布式架构的场景下,会有非常多的场景要放弃强一致性,放弃一致性并不代表不保证一致性了,而是保证最终一致性,所有就有人提出了BASE理论,柔性事务。
BASE
源自于eBay架构师在ACM上发表的文章,提出BASE理论。BASE理论是对CAP理论的延伸,核心思想就是分布式架构无法做到强一致性,但是可以采用适合的方式达到最终一致性。
“基本可用”
当分布式服务出现故障,保证核心功能可用,部分功能失去可用性,提供降级服务。
“柔性状态”
简单来说就是为交易设置中间状态,允许交易存在中间状态,不要求实时达到最终状态。
“最终一致性”
系统中所有的中间状态,数据副本,经过一段时间后,通过合适的手段,最终达到一致性。
传统分布式事务处理方式
两阶段提交
两阶段提交是国内一些技术供应商,早年提供的一种主流分布式事务处理方式,主要包括事务发起方,事务参与者,以及事务协调器。
设计思路是事务协调器在第一阶段要求所有的事务参与者提交“操作”,完成所有在预备检查,同时对请求资源加锁。第二阶段,事务协调器在所有的事务参与方间协调,提交或者回滚事务,同时释放资源锁。
这种方案可用达到强一致性,在等待锁的同时,失去了可用性。如果事务的参与者比较少的话,能有不错的性能表现,问题是在大型分布式场景下,事务的参与者非常多,事务协调器一旦需要在众多的参与者间协调资源和加锁,问题很明显。
事务参与者众多----->事务执行时间延长-----> 锁资源冲突概率提升
同时在并发场景下,失去了可用性,大量请求需等待资源的释放:
大量事务积压----->死锁------>性能,吞吐处理效率严重下滑
这就是为什么在今天的互联网场景下,几乎没有人用这种传统的分布式事务解决方案。
柔性事务解决方案
引入日志以及补偿机制
类似传统的数据库,柔性事务的原子性主要有日志保证。传统数据库中,对于事务的执行,会生成redo/undo日志。事务日志也一样,通过日志记录事务的开始,结束状态,参与者信息。根据具体的正向补偿以及反向补偿需求场景,生成redo/undo日志,可以通过日志解析来达到最终的一致性。
一般在实际应用中,redo/undo日志会记录在数据库节点中,从而保证日志与业务操作同时成功/失败。
这种做法在互联网企业也经常有人采用,但是一般实现比较粗糙,对于异常和补偿操作支持不够完善,而且维护的人力成本非常高。
可靠消息传递
第二种就是依赖中间件的消息投递机制,实现最终一致性。(rabbitmq/rocketmq/kafka)
其中,rocketmq是支持事务型消息的,中间件层面保证了该条消息保证会被消费,这种场景会在后面详细介绍。其他的两种中间件,就有可能出现消息仅投递了一次,但是没有被消费,也有可能出现消息已经被消费,但是重复投递的场景。
基于这样的异常场景,这就要求我们的应用系统在设计和开发时要达到两点要求。
第一,应用系统要根据具体的需求,对事务的要求,自己实现重试机制,在应用端保证消息一定会被事务参与者消费,达到最终一致性。
第二,因为消息可能会被重复投递,各个分布式节点对外暴露的服务接口,必须要实现幂等。各个业务场景不同,实现幂等的机制要求也会不一样。比如我行融资系统就是通过日志排重表来实现,通过业务元素识别接口请求的唯一性,在整体事务开始时记录在流水表中,并且设置成唯一性索引,在整体事务结束时,删除该笔流水。我行自研框架是另一种实现方式,也是通过业务元素识别接口请求的唯一性,同时通过该唯一性的key值在redis中建立排它锁,该种方式框架中已经做好了封装,使用者只需要将业务元素写Spring表达式就可以了,这种方案也是互联网公司用的非常多的方案,感兴趣的可以去看一下源码。
通过无锁实现
造成数据库性能瓶颈往往是强事务型带来的资源锁。所以放弃在数据库加锁也是柔性事务的主流解决方案,但是需要注意的是,放弃加锁,不等于放弃隔离性。所谓的无锁,其实只是一种事务绕行方案。
“避免事务进入回滚”
业务不管出现任何情况,继续朝事务处理流程的顺序继续处理,中间状态对外可见,由于事务不会回滚,不会导致脏读。即设置中间状态。改种方案多常见于状态机的使用。我行融资内部用的也很多,融资内部设有两套状态机,应对授信以及放款流程,上游系统来查询的时候,查到的可能是中间状态,这种场景下,就需要查证机制来保证最终一致性。
“辅助业务变化明细表”
比如对资金进行增减处理,可以记录增减变化的明细表方式。避免所有事务对同一数据库表进行更新操作,造成热点。同时使不同事务在处理中的数据互相不干扰。实现对资金的隔离处理。
例如对同一中间账户的转出与转入操作,必定会产生访问热点。而且容易出先锁抢占的问题。避免锁的方式就是创建明细表。
举一个我行融资系统该种方案的设计思路:
渠道 | 产品 | 可用余额 | 。。。 |
---|---|---|---|
租房 | 青客 | 1000w |
传统的余额校验逻辑,当每笔借据发生借款以及还款交易,都需要去更新整个产品的实时可用余额,势必造成热点数据读写,以及资源锁的争抢。
建立辅助业务明细表,建立1:n的数据对应关系:
借据 | 产品 | 额度 |
---|---|---|
123 | 青客 | 1w |
234 | 青客 | 10w |
这种模式下,不再需要对整个产品的实时可用额度做更新,只需要在实际发生借款时计算当前可用余额,公式如下:
当前可用余额=产品配置表中限额总数 - 借据明细表中相同产品额度汇总
“乐观锁”
数据库悲观锁对数据访问有极强的排他性,这也是操成数据库瓶颈的重要原因。使用乐观锁,一定程度上解决这个问题。乐观锁大多基于数据Version实现。例如刚才余额更新的业务场景,我们也可以使用乐观锁的思路实现,在产品配置表中增加Version字段,在事务开始前获取版本号字段,在最后执行前比对版本号字段,一致执行事务,不一致放弃或重试当前事务。
渠道 | 产品 | 可用余额 | version |
---|---|---|---|
租房 | 青客 | 1000w | 1 |
participant transcation1 as tran1
participant transcation2 as tran2
participant database as db
tran1->db:1.事务1开始,获取数据版本号为1
tran1->tran1:2.处理内部业务逻辑
tran2->db:3.事务2开始,获取数据版本号为1
tran2->tran2:4.处理内部业务逻辑
tran1->db:5.业务逻辑结束,比对版本号是否为1,更新版本号为2,更新限额字段,事务提交成功
tran2->db:6.业务逻辑结束,比对版本号是否为1,比对失败,当前事务失败
因此,乐观锁机制避免了长事务中的数据库加锁,对并发下的整体性能会有较大的提升,然而乐观锁方案也具备一定的局限性。乐观锁机制大多数需要在应用中实现,对应用的边界划分会有比较大的要求,也就是不同应用如果访问同一个数据源,开发者需要遵循乐观锁机制,不然会造成数据脏读脏写。这就是为什么在构建分布式体系时,所有的数据库操作都需要统一到该数据库对应的应用服务层。不允许其他应用单独的访问和操作。
以上三种方案多依赖于应用端的实现,对开发人员有一定的要求,下面介绍一下依赖于中间件,实现分布式事务的主流做法。
分布式事务中间件
消息服务中间件
主流的消息服务中间件:rocketmq/rabbitmq/kafka,这里我们主要讲一下rocketmq的事务型消息机制,使用场景,至于后两者前文已经提到,这里不做赘述。
这张图是阿里云官方产品文档提供的,官方文档也有详细的介绍,这里不再赘述。
需要提到的是,通过消息异步的方式,保证了事务的一致性,避免两阶段提交方式对多个事务参与者数据库的长时间锁定。另外要注意的:这种方案下,如果出现异常,一般采用正向补偿的机制,即不会像传统事务方式出现异常之后进行回滚,会通过消息的不断重试,或者人工干预让该事务链路继续朝前面执行。同时该种方案,对开发人员要求较高。本地事务执行,同步消息投递,以及本地事务的回查,异常的回滚,以及最终消息的投递,都需要应用端做支持。
下面介绍一下我在电商平台依靠mq实现的业务场景:
分布式事务中间件
主流的分布式事务中间件:DTX/GTS/ByteTCC/Himly/TCC-transaction
其中前两者分别是蚂蚁金融云和阿里云的分布式中间件产品,后面三个是国内已经开源的分布式事务中间件产品,感兴趣可以去单独了解一下。这里也主要讲一下这些分布式事务中间件类似的设计思路以及使用业务场景。
发起方:分布式事务的发起方负责启动分布式事务,通过调用参与者的服务,将参与者纳入到分布式事务当中,并决定整个分布式事务是提交还是回滚。一个分布式事务有且只能有一个发起方。
参与者:参与者提供分支事务服务。当一个参与者被发起方调用后,该参与者会被纳入到该发起方启动的分布式事务中,成为该分布式事务的一个分支事务。一个分布式事务可以有多个参与者。
事务管理器:事务管理器是一个独立的服务,用于协调分布式事务,包括创建主事务记录、分支事务记录,并根据分布式事务的状态,调用参与者提交或回滚方法。
TCC事务模型
这张图是从网上找的tcc事务模型,简单来说tcc的逻辑就是原先的一个事务接口需要改造为 3 个接口,Try-Confirm-Cancel:
服务调用链路依次执行 Try 逻辑。
如果都正常的话,分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
如果某个服务的 Try 逻辑有问题,框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。
同样,Confirm接口以及Cancel接口需要实现幂等机制。因为当Try成功/失败之后,应用一旦不可用,或者出现网络异常,中间件会一致重试Confirm/Cancel接口直至调用成功。
下面介绍一下我行贷款核算系统的交易以及核算如果用Tcc来实现事务一致性,主要的交易设计思路: