本文主要介绍分布式系统中的一个大难点,分布式事务问题。
一般在单体系统中只有一个数据源的情况下我们通常是使用spring的事务管理器,来帮我打理事务相关问题,当时由于系统的慢慢扩展,单体系统以及无法满足我们的业务需求,我们加了多台服务器,但是随着业务量的增大,数据库成了我们扩展瓶颈,我们使用读写库,但是由于业务实在太大了,读写库也满足不了我们对性能的需求了,因为所有的请求都落入通一个库,写成了数据库的最大瓶颈,这时我们引入分库的概念,按照不同的业务模块来对数据库进行拆分。
对数据库进行拆分之后,系统中的数据源不在是单独的一个了,不同的业务使用不同的数据源,这个时候spring已经无法帮我们管理事务了,这个时候我们就引入了 分布式事务的概念。
这里提一下base理论有三大块
1.基本可用
在服务器宕机的情况下,允许返回慢,但是要有响应回复
2.软状态/柔性事务
允许系统存在中间状态,改状态对系统不产生影响,不同阶段之间的副本允许同步的存在少量延迟
3.最终一致性,
允许在某个时间点,不同库之间的数据存在不一致,但是在一段时间后要达到数据一致性。
分布式事务出现的场景
本地系统间分布式事务
分布式一直是分布式系统的一个难点问题,下面在介绍一下对数据一致性要求更高的支付项目场景(不同系统间的分布式事务问题)
这是一个支付接口的调用流程,
A服务调用支付宝或者微信的接口进行支付,支付成功后,支付宝进行接口回调,然后通知我们支付成功,这个时候我们可以修改对应订单的状态为已支付。
这个最大的问题在于回调问题,这是跨系统之间的分布式事务问题,因为它可以能存在a服务支付成功,但是b服务进行回调的时候由于网络延迟。接口错误等问题,回调失败,那么这个时候就会出现用户已经付了钱,但是订单还是显示未支付状态,这个可就是灾难性的bug了。
下面来介绍下这个回调问题。
同步回调:支付完成后,支付宝采用浏览器重定向方式进行会点
这种方式就是网站上常见的,我们支付之后,支付宝提示是否支付成功,然后在多少秒之后返回支付页面。然后本地修改订单状态
异步回调:支付完成后,采用后台通知方式进行httpclient进行回调通知。然后本地修改订单状态。
关于异步回调涉及到以下几个注意点
1.调用支付接口的时候我们要考虑url地址,给支付宝然他回调
2.当支付宝回调失败的时候怎么办?(支付宝内部实现的机制是重试,如果还是重试失败,他会触发定时任务重试,如果还是失败它就不会通知了。废弃通知)
3.当支付宝废弃通知的时候,由于用户已经支付完成,本地修改订单状态接口还没被触发,一直显示未支付,这个时候我们应该主动的去支付宝查询订单信息,调用支付宝查询接口,如果查询到对应的订单就就修改订单状态。
支付场景下的分布式事务是最难的。
关于事务的描述
事务分为刚性事务和柔性事务
刚性事务就是ACID,必须要满足,这个在单体系统中很常见
柔性事务:
1.两阶段型(JTA+AOTIMIC此机制是基于2pc来解决的)
2.补偿型 (一般我们的补偿机制都是通过job定时任务去查询事务是否成功,没成功就在job中做业务逻辑)
3.最大努力通知型,异步确保型。(使用MQ)
下面主要来介绍使用base理论中的最终一致性来解决分布式的问题(基于本地系统,不涉及第三方系统)
上面的图就是使用RabbitMQ解决分布式事务的思想
下单系统发送消息到交换机中,此交换机被两个队列进行绑定,绑定key 是同一个,不过上面的队列是延时消费。
正常的消费逻辑是消费者订阅交换机,然后下面的派单系统队列进行消费,消费完成后,订单落库,这个似乎数据是一致性的,
但是我们无法保证派单系统队列一定是消费成功的呀,这里我们有延时消费机制,当补单系统队列消费的时候会先到数据库中查询此条消息是否已经被插入了,如果如有插入就执行入库逻辑,进行数据入库(有幂等性机制的实现),如果补单队列也出现了,那么就要通知job进行补偿答复了,job也入库失败,那么就需要人工补偿了
上面的逻辑要注意三点
1.消费如何确保绝对能投递到MQ服务器中
可以采用生产者确认机制,如果发送失败可进行重试。
2.消费者如何确保数据不重复插入,也就是消息幂等机制
在入库前,我们可以下根据消息中的订单id来进行查询,如果库中已经有了这条数据,那么说明已经入库成功,我们就不需要重复消费。并且还要将消息的确认机制修改为手动确认。
3.如何保证分布式系统下的订单id唯一性
在单体系统中我们生成的uuid自然是唯一的(同一机器下的uuid不会重复),但是在分布式系统中由于机器不同,有可能会出现uuid相同的情况下,这个时候我们引用雪花算法,或者在以前的uuid中加上当前机器的标识符来达到此uuid的唯一性。