分布式事务的一致性

事务

是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。

分布式事务

在当今的架构由垂直架构发展到分布式架构,例如SpringCloud,dubbo等,已经由单一的单服务器,扩展到集群的模式,将一个完整的应用程序,构建成多个单独的单元,通过服务发现,来远程调用不同的单元。共同协作完成任务。 这时候单个的ACID已经不适应了。例如,用户登录的时候获取相应的积分,有两个操作,登录和增加积分,在单一的服务上,只需要将两个操作在同一个事务里就很好的做到,在分布式的服务中呢,如果也追求这种ACID,那么分布式就失去了意义。这时候引出了一个CAP理论。

CAP 理论

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

  • 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
  • 可用性(Availability) : 每个操作都必须以可预期的响应结束
  • 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成

事务的一致性

  • 强一致性:不管什么时候,都要求后续的是更新后数据
  • 弱一致性:后续拿到的数据可能是更新前的也可能是更新后的,经过一段不确定的时间,拿到的是更新后的数据
  • 最终一致性:最终所有的访问得到的都是更新后的数据

解决方案

通常采用两种方式:

1.采用时效性高的 MQ,由对方订阅消息并监听,有消息时自动触发事件
2.采用定时轮询扫描的方式,去检查消息表的数据。

提供回滚接口

在分布式架构中,如果一个模块需要同时调用后端的其他几个原子性服务,假如,有一两个失败了,如何解决。

这个时候,最先想到的就是同步调用,获取返回,假如我们有一个功能A需要调用后端的 D和C。如果调用D失败了,那么拿到返回之后,就不会调用C,如果D成功,C失败,会去回滚前面的D。

例如:
某个电商平台,在生成订单和减少库存的时候,是两个子系统服务,使用不同的DB。
1.将订单和较少库存翻到同一个本地方法中
2.先减少库存,后续生成订单
3.如果订单生成成功,就没什么问题,如果失败,调用较少库存的回滚方法

这种方式只有在依赖的服务比较少,或者非常简单的场景下使用,代码的耦合性比较高,代码量大,如果串行的太多,回滚太复杂

MQ(非事务)

用ActiveMQ来模拟,用户注册后发放红包的这一流程,首先用户注册成功后,向mq推送消息,红包应用充当消费者角色,来监听mq中的消息,来发放红包。

伪代码:

//向用户表插入一条数据

try{
int row = jdbc.insert("insert into user(phone, name) values('13800000000', 'lisi')");
//如果成功
if(row){
//如果成功。向mq推送一条消息
jms.send("message", message);
}
}catch(Exception e){
//失败就回滚
rollback();
}

上述代码,生产者没有什么太大的问题:

注册成功,MQ也成功。 OK!
注册失败,不会推送MQ。
注册成功,推送失败,代码回滚。

在消费者有一下问题:
1、如果mq推送成功,消费者出现异常,怎么办?消息已经取出来,消费失败,消息丢失。
2、如何避免重复的消费?

针对一,增加消息的持久化,这样即使宕机,也可以执行重试。(自身MQ的是持久化或者外部的持久化)
针对二,增加消费日志或者记录表,或者根据业务规则判断消息是否已经被消费。(尽量使用外部存储记录消费记录)

本地消息表结合MQ(非事务)

将本地消息表和MQ结合,即能保证消息的时效性,也能尽量避免消息的重复消费。

模拟流程

用上述的例子,增加关系数据库的记录表。

伪代码:

//向用户表插入一条数据

try{
int row = jdbc.insert("insert into user(phone, name) values('13800000000', 'lisi')");
//如果成功
if(row){
// 插入消费记录表
int r_row = jdbc.insert("insert into message(phone, status) values('13800000000', '0')");
//如果成功。向mq推送一条消息
if(r_row){
jms.send("message", message);
}
}
}catch(Exception e){
//失败就回滚
rollback();
}

为什么要用消息表

如果只是用mq的话,会出现通知失败,我们将丢失一条数据。如果只用消息表的话,数据库的瓶颈问题。
例如。消息到达MQ之后,消费方异常,如果没有记录表,这条数据就没了,如果有了记录表,我们可以采用轮询将为成功消费的数据重新进行消费。

生产者分析

注册成功,MQ也成功。 OK!
注册失败,不会推送MQ。
注册成功,推送失败,代码回滚。
注册成功,推送成功,消费异常,轮询取出消费。

以上来看,生产者方面,没有什么太大的问题。

消费方分析

取出消息后,更新记录表,操作对应的业务,业务失败,回滚记录表。
消费重复,操作前先查询是否消费成功,即使重复消费,也不能影响业务结果。

这种方式,基本可以满足一般的场景需求,实现了最终的一致性,消息表存在性能方面的瓶颈,一般场景可以满足,在大型项目中,瓶颈会凸显出来。

MQ(事务)

目前大部分的MQ都是不支持事务的,支持事务的貌似只有阿里的RocktMQ.

RocketMQ将消息分为两个阶段:Prepare阶段和确认阶段。

(1) 发送Prepared消息
(2) update DB
(3) 根据update DB结果成功或失败,Confirm或者取消Prepared消息。

当1和2成功,3失败的时候,RocketMQ会定期(默认是1分钟)扫描所有的Prepared消息,询问发送方,到底是要确认这条消息发出去?还是取消此条消息?

RocketMQ最大的改变,就是扫描消息表,这个事情,不在是业务方做,而是MQ在做。

如果消费失败,前面的模块又太多,此时,人工处理,比实现一个复杂的回滚要更加简单和可靠。

-------------本文结束 感谢您的阅读-------------