大事务拆分项目应用及系统异步化处理-再看分布式事务

时间:2022-08-15 06:16:13

Case1

一、交易还款业务大事务

废话不多说,直接一个金融还款业务创建交易账单case,还款逻辑如下:
1、用户点击还款,发起还款请求
2、服务端接收还款请求,进行还款计划查询校验
3、进行其他还款验证
4、返回用户还款计划,用户接收后,执行还款(调用支付网关等)
5、支付网关通知交易还款结果成功与否
6、交易更新还款账单状态
整个交易还款操作毋庸置疑,必须保证事务一致性,故上述6步操作均包裹在一个大事务中进行。除了大事务包裹导致数据库资源占用之外,月末还款高峰,还款请求密集,db资源响应问题集中凸显。

二、解决方案

1、数据库事务异步化
简言之,将大事务拆分成小事务,小事务间通过MQ等异步机制进行通讯。这就涉及到如何进行事务拆分,事务1成功,肯定影响事务2的执行。例如如果支付网关执行还款成功为事务1,而交易更新状态拆分到事务2中,则就有可能出现,MQ经由丢消息,导致交易未接受支付成功请求从而执行换狂账单状态更新。这就是不合理的事务拆分。应当保证网关支付结果跟最终交易账单状态的原子性。也就是说这两部操作得在一个事务中。
则上面大事务可拆分为
1、用户点击还款,发起还款请求-事务外
2、服务端接收还款请求,进行还款计划查询校验-事务外
3、进行其他还款验证-事务外
4、返回用户还款计划
以下3步为1个事务中操作逻辑:
5、用户接收还款计划,执行还款(调用支付网关等)
5、支付网关通知交易还款结果成功与否
6、交易更新还款账单状态
这样拆分,即将前4步查询操作,拆出大事务中。 拆分原则:对必须保持顺序执行的服务,按照顺序执行。对能够同步执行(无太多强事务要求)的服务,均采用异步处理。以达到增强服务并行性,减少响应时间。
如果考虑到出现查询后其他终端进行更新操作,导致脏数据,可在查询处进行乐观锁等判断处理。乐观锁处理就不会采用数据加锁机制,由数据使用方判断数据过期与否,是否进行后续更新等操作。步骤之间,可通过mq进行通知,同时利用重试机制、mq消息的必达性等进行数据最终一致性保证。
三、再看一致性原理
ACID 强一致性与CAP、BASE的最终一致性,(柔性事务), 同样结合case对分布式事务一致性进行理解。
用户下单,针对仅有1个库存的商品,AB用户都相中,A先浏览加购物车继续浏览其他商品,而B用户立刻对该商品进行下单。此时,多个数据库实例中,B下单操作后,实例1进行减库存,而其他实例同步需要时间,如果A在同步尚未完成时进行下单,查询数据库实例2尚有1个库存,此时就出现超卖现象。
分布式系统如何解决这种问题?
在B下单后,只有当DB1\2\3所有数据均同步完成后,才返回B用户下单成功,期间不接受其他用户对该商品的下单请求(C数据一致性得以保证。且分区容错性P由于db的多实例集群,1个down掉其他db补上,分区容错也得以满足)。而此时就涉及服务可用性,在数据同步期间,服务对其他用户是不可用的(A未得以满足)。要解决可用性A的问题,可将数据仅保留一份,无集群复制同步操作,不存在服务不可用时间,则下单服务随时可用(A得以满足)。但数据库单例节点,挂了服务全完,此时就放弃分区容错性P,保证服务随时可用A。
这就是CAP中三者只能满足其二的实例分析。根据业务需求进行取舍。
而BASE则提出BA基本可用、S柔性状态、E最终一致性。基本可用是指服务例如有4个节点,down调一部分,保证还有活的提供服务就行。柔性状态即允许中间状态,例如上述3个数据库实例,可存在1、2已完成同步,3数据未同步状态。最终一致性即经过一段时间后,数据最终保证全同步即可。

Case2

总结自http://blog.jobbole.com/89140/从支付宝转账1万块钱到余额宝,如何保证支付宝账户跟余额宝账户的收支平衡。本质上问题可以抽象为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。

一、本地事务,支付宝扣1w与余额宝入1w,两个update写在一个本地事务中。

实现1:sql事务
Begin
transaction
update A set amount=amount-10000 where userId=1;
update B set amount=amount+10000 where userId=1;
End
transaction
commit;
实现2:spring加个注解就搞定
@Transactional(rollbackFor=Exception.class)
public void update() {
updateATable();//更新A表
updateBTable();//更新B表
}
但实际情况是,支付宝账户表和余额宝账户表显然属于两个独立系统的数据库服务,不会在同一个数据库实例上,分布在不同的物理节点上,这时本地事务就无法控制。

二、分布式事务

1、事务协调器发送prepare告知支付宝-1k的请求,同样发送prepare告知余额宝+1k的请求。(prepare消息写到本地日志中,保证如果出现故障,在故障后恢复用,本地日志起到现实生活中凭证的效果)
2、支付宝、余额宝返回事务协调器yes,我可以执行这条sql,并执行具体本机事务,但不commit(同样,yes or no 都写到本地日志中)
3、事务协调器接收到均yes,发送支付宝、余额宝commit请求,将执行结果成功与否返回事务协调器。(日志)
4、协调器均接受成功,则完事儿,有任一失败,执行事务abort回滚。
问题:
两阶段提交涉及多次节点间的网络通信,通信时间太长。事务时间相对于变长了,资源锁定时间增长,造成请求等待。不适用于高并发场景。

三、使用MQ解决避免分布式事务

形同吃饭付了钱后,饭店并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。
只要这张小票在,你最终是能拿到炒肝的。同理,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加 1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。

如何保证消息可靠保存?
1、业务与消息耦合,支付宝扣款,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(本地事务作保证)。执行成功后,通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。
2、业务与消息不耦合,抽象出消息模块。
1)支付宝执行扣-1k,先不commit,同时发送消息给消息模块。
2)消息模块记录,记录成功后,支付宝commit本地请求
3)commit结果发给消息模块,commit成功,消息模块发送消息,让余额宝+1k,失败则不发送
4)余额宝+1k后,发送消息模块,增加成功。如果+1k失败,消息模块未收到success请求,则重试,再次发送

如何保证消息不重发?
支付宝或消息模块记录消息发送流水,发送成功否,余额宝返回结果等。发之前先查一遍,如果状态是已执行,且接收到余额宝成功结果,则丢弃此条消息发送。