分布式事务一致性

来源:互联网 发布:mac照片怎么复制到u盘 编辑:程序博客网 时间:2024/05/16 14:56

1.分布式事务产生的原因

1.1数据库分库分表

当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。


1.2应用SOA化

所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。


2.几个概念

2.1事务的ACID特性

2.1.1原子性(A)

所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。

2.1.2一致性(C)

事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。

2.1.3隔离性(I)

所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。

2.1.4持久性(D)

所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。

2.2CAP特性

2.2.1 C(一致性)一致性是指数据的原子性,在经典的数据库中通过事务来保障,事务完成时,无论成功或回滚,数据都会处于一致的状态,在分布式环境下,一致性是指多个节点数据是否一致;

2.2.2 A(可用性)服务一直保持可用的状态,当用户发出一个请求,服务能在一定的时间内返回结果;

2.2.3 P(分区容忍性)在分布式应用中,可能因为一些分布式的原因导致系统无法运转,好的分区容忍性,使应用虽然是一个分布式系统,但是好像一个可以正常运转的整体

2.3BASE特性

2.3.1 BA: Basic Availability 基本业务可用性;

2.3.2 S: Soft state 柔性状态;

2.3.3 E: Eventual consistency 最终一致性;

3.分布式事务应用场景举例

3.1支付

最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。

3.2在线下单

买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。

4.分布式事务解决方案

4.1 基于XA的分布式解决方案

XA协议指的是TM(事务管理器)和RM(资源管理器)之间的接口。目前主流的关系型数据库产品都是实现了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的,事务管理器和资源管理器之间也使用了XA协议。 本质上也是借助两阶段提交协议来实现分布式事务的,下面分别来看看XA事务成功和失败的模型图:

成功模型图;


失败模型图;


总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。

4.2 本地消息表

其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。如果不考虑性能及设计优雅,借助关系型数据库中的表即可实现。

下面是一个跨行转账的栗子;

STEP1:第一步伪代码如下,扣款1W,通过本地事务保证了凭证消息插入到消息表中。

STEP2:通知对方银行账户上加1W了。那问题来了,如何通知到对方呢?

通常采用两种方式:

采用时效性高的MQ,由对方订阅消息并监听,有消息时自动触发事件
采用定时轮询扫描的方式,去检查消息表的数据。
两种方式其实各有利弊,仅仅依靠MQ,可能会出现通知失败的问题。而过于频繁的定时轮询,效率也不是最佳的(90%是无用功)。所以,我们一般会把两种方式结合起来使用。

解决了通知的问题,又有新的问题了。万一这消息有重复被消费,往用户帐号上多加了钱,那岂不是后果很严重?

仔细思考,其实我们可以消息消费方,也通过一个“消费状态表”来记录消费状态。在执行“加款”操作之前,检测下该消息(提供标识)是否已经消费过,消费完成后,通过本地事务控制来更新这个“消费状态表”。这样子就避免重复消费的问题。

总结:上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。

4.3 MQ(非事务消息)

通常情况下,在使用非事务消息支持的MQ产品时,我们很难将业务操作与对MQ的操作放在一个本地事务域中管理。通俗点描述,还是以上述提到的“跨行转账”为例,我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功。这样一致性似乎很难保证。
先从消息生产者这端来分析,请看伪代码:


根据上述代码及注释,我们来分析下可能的情况:
⑴操作数据库成功,向MQ中投递消息也成功,皆大欢喜
⑵操作数据库失败,不会向MQ中投递消息了
⑶操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚

从上面分析的几种情况来看,貌似问题都不大的。那么我们来分析下消费者端面临的问题:
⑴消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致
⑵尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果

如何保证消息与业务操作一致,不丢失?
主流的MQ产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的(有些MQ可以自定义重试次数)。

如何避免消息被重复消费造成的问题?
⑴保证消费者调用业务的服务接口的幂等性
⑵通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)

总结:这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。如果MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。不过在没有充分测试的情况下,不建议在交易业务中直接使用。

4.4MQ(事务消息)

举个例子,Bob向Smith转账,那我们到底是先发送消息,还是先执行扣款操作?

好像都可能会出问题。如果先发消息,扣款操作失败,那么Smith的账户里面会多出一笔钱。反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith收不到钱。除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的思路呢?

下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:


总结:据笔者的了解,各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。比较遗憾的是,RocketMQ事务消息部分的代码也并未开源,需要自己去实现。

原创粉丝点击