基于可靠消息方案的分布式事务:Lottor介绍

分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

首先,解释下事务的概念:一组操作要么都完成之后提交,要么全部回滚。分布式事务特指在分布式环境下,一次事务设计多个服务进程,说白了就是跨进程的事务,这样就不能控制事务组的一致性。

分布式系统区别于传统的单体应用,单体应用的服务模块和数据都在一个服务中,使用Spring框架的事务管理器即可满足事务的属性。而分布式系统中,来自客户端的一次请求往往涉及多个服务,事务的一致性问题由此产生。

CAP理论

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

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

具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。

上面这句话的表述,很多人都用过,是的,这是一种误解。注意CAP定律的完整表述:Any networked shared-data system can have at most two of the three desired properties.

CAP 定律的前提是 P,当 P 决定后才有 CA 的抉择。因此,简单粗暴地说「三选二」是有一定误导性的。

BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

需求分析

功能需求

功能需求最主要的是满足分布式事务的一致性,涉及的事务组中的操作为多个写操作,当产生一个或多个写操作失败时,回滚整个事务组中的操作。

非功能需求

  • 性能:分布式事务对系统的性能必然是有影响的,需要寻找平衡的点。
  • 高可用:引入中间件或者协调者时,避免单点故障。分布式系统的高可用必然会牺牲部分一致性。
  • 可扩展:降低引入的业务耦合。
  • 伸缩性:系统能够弹性伸缩。

解决方案

强一致方案

X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。

XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

2PC

二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。

2pc

  • 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  • 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
  • 二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

    3PC

    三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。

3pc

如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续commit。相对于两阶段提交虽然降低了同步阻塞,但仍然无法避免数据的不一致性。在分布式数据库中,如果期望达到数据的强一致性,那么服务基本没有可用性可言,这也是为什么许多分布式数据库提供了跨库事务,但也只是个摆设的原因,在实际应用中我们更多追求的是数据的弱一致性或最终一致性,为了强一致性而丢弃可用性是不可取的。

柔性事务

根据BASE理论,系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。

弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。 在工程实践上,为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。但在电商等场景中,对于数据一致性的解决方法和常见的互联网系统(如 MySQL 主从同步)又有一定区别。

补偿机制:TCC

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认 Confirm阶段是不会出错的。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

TCC

TCC与2PC协议比较:

  • 位于业务服务层而非资源层
  • 没有单独的准备(Prepare)阶段,Try操作兼备资源操作与准备能力
  • Try操作可以灵活选择业务资源的锁定粒度(以业务定粒度)
  • 较高开发成本

本地消息表

类似于可靠消息方案。

本地消息表

消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

这种方案遵循BASE理论,采用的是最终一致性,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。

优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。

缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

事务消息

转账流程

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。

如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

如果endTransaction方法执行失败,数据没有发送到broker,导致事务消息的 状态更新失败,broker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法来决定是回滚事务还是继续执行,最后调用endTransactionOneway让broker来更新消息的最终状态。

  • 消费失败
    解决超时问题的思路就是一直重试,直到消费端消费消息成功

  • 消费超时
    消费失败怎么办?阿里提供给我们的解决方法是:人工解决。大家可以考虑一下,按照事务的流程,因为某种原因Smith加款失败,那么需要回滚整个流程。如果消息系统要实现这个回滚流程的话,系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比消费失败的概率大很多。这也是RocketMQ目前暂时没有解决这个问题的原因,在设计实现消息系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,这也是大家在解决疑难问题时需要多多思考的地方。

Lottor介绍

Lottor用于解决微服务架构下分布式事务的问题,基于可靠性消息事务模型实现。

Lottor的结构

Lottor由三部分组成:

  • Lottor Server
  • Lottor Client
  • Lottor UI

Lottor服务器与客户端之间的通信使用的高性能通信框架:Netty。所有的客户端(生产端和消费端)都会与服务器保持长连接。Lottor UI用于展示系统中的事务组详细信息,包括预提交的事务组、消费失败的事务消息,并支持页面操作失败的消息(如补偿或重试)。

Lottor的设计

功能介绍

生产方分为三步:

  • 预发送消息,首先会将消费方的事务组(一条或多条事务消息)组装好,并发送到Lottor Server,事务消息的状态为预发送
  • 执行本地事务:预发送之后,将会执行本地事务。
  • 发送确认消息:根据本地事务的执行结果,异步发送确认消息。如果本地事务出现异常,回滚本地事务,并将异常信息捕捉一起发送到Lottor Server。本地也会持久化该状态(定期删除)。

Lottor Server:

  • 接收预提交消息:收到预提交消息,将事务组中的事务消息分别保存,状态为pre-commit
  • 接收确认消息:状态为confirm,将更改相应的事务组状态,并将消息发送到对应的消费方(MQ异步实现),并标记事务消息的状态为unconsumed。否则,回滚状态只会修改事务组状态(定期删除)。
  • 回查预发送消息的状态:状态为pre-commit的事务组消息,Lottor Server将会定期回查生产方。
  • 回查事务消息的状态:状态为unconsumed(一般4h),Lottor Server将会定期回查消费方。

消费方:

  • 接收事务消息:订阅相关的主题,消费完成之后,将会异步发送ACK给Lottor Server,消费失败会将异常返回给Lottor Server。本地也会持久化消费的状态(定期删除)。

Lottor 客户端的持久化,提供了SPI接口,可通过配置动态指定。目前支持:JDBC、Redis、MongoDB和文件系统。

告警机制及消费补偿

这里所说的告警机制及消费补偿是针对消费端,可靠消息方案是保证了事务消息一定能够到达消费方,但是消费方可能因为某些原因而无法成功消费,有些消费异常是可以通过重试解决的,而有些异常是需要告警之后人工干预的。比如消费方暂时不可用,或者是多个消费方消费的顺序问题,可以通过定时的重试机制完成。而如果是由于生产方发送的事务消息出错(参数构造错误),此时消费方已经提交了本地事务组,所以是无法通过重试实现成功消费,导致需要告警,人为解决脏数据的问题。

适用场景

对于分布式系统的吞吐量有较高的要求,以及能够满足最终一致性的场景。如上面提到的告警机制及消费补偿,分布式事务是对微服务系统的完善,但是并不能完全保证一致性,可能需要通过告警等手段解决极端问题产生的不一致情况。

项目截图

项目结构

UI界面

首页

事务组信息

事务组状态

总结

本文主要介绍了分布式事务的相关概念以及业界一些常用的解决方案(参考了很多网上的博客),并提出了笔者基于可靠消息方案的实现:Lottor。后续文章将会详细介绍Lottor的实现,敬请期待。

参考

  1. 聊聊分布式事务,再说说解决方案
  2. 分布式事务 - 两阶段提交与三阶段提交
  3. 关于分布式事务、两阶段提交协议、三阶提交协议
  4. 分布式开放消息系统(RocketMQ)的原理与实践
  5. TCC型分布式事务原理和实现之:原理介绍
  6. 分布式事务之说说TCC事务
aoho wechat
欢迎您扫一扫上面的微信公众号,aoho求索,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!