分布式事务解析

发布于: 2020 年 06 月 17 日

事务的特性跟隔离级别

首先,我们先来看看事务的四大特性:

  • 原子性(Atomicity):事务的所有操作要么全部成功,要么全部失败回滚

  • 一致性(Consistency):事务必须使数据库从一个一致性状态转换到另一个一致性状态

  • 隔离性(Isolation):事务之间必须要相互隔离,不能被其它事务干扰

  • 持久性(Durability):事务一旦提交,那么对数据库数据的更改就是永久性的,不能丢失

事务并发会带来一些问题,主要有:

  • 第一类更新丢失:指事务A完成,事务B异常终止,事务B回滚造成事务A的更新也同时丢失

  • 脏读:指事务A读取了事务B未提交的修改,然后事务B回滚了,此时事务A读取的数据就是不正确的

  • 不可重复读:指事务A读取了两次数据,在这两次的读取过程中事务B修改了数据,导致事务A两次读取出来的数据不一样

  • 第二类更新丢失:指两个事务同时更新同一条数据,后完成的事务就会造成先完成的事务更新丢失

  • 幻读:事务A读取了两次数据,在这两次数据的读取过程中事务B添加了数据,导致事务A两次读取出来的集合不一样

为了解决事务的并发问题,一般数据库都会提供四种事务隔离级别:

  • 读未提交(Read Uncommitted):所有事务都可以看到其它未提交事务的执行结果

  • 读已提交(Read Committed):保证一个事务只能看到已经提交事务所做的修改。会存在同一个事务多次读取数据的结果不一样的问题

  • 可重复读(Repeatable Read):保证一个事务多次读取数据的结果是一样的

  • 可串行化(Serializable):强制事务串行执行

四种隔离级别会产生的问题如下表格所示:

由于分布式事务也是由多个单机事务组成的,因此,为了更好地理解分布式事务,让我们先看看MySQL InnoDB是怎么实现单机事务的。

MySQL InnoDB事务实现原理

MVCC

MySQL事务主要通过MVCC(多版本并发控制,Multiversion Concurrency Control)来实现。MVCC是数据库管理系统常用的一种并发控制,可以无锁实现,解决了读写锁造成的多个长时间的读操作饿死写操作的问题,大大提高了性能。

那么InnoDB是怎么实现MVCC的呢?首先让我们来看看InnoDB的几个重要的机制。

InnoDB实现事务的关键机制

Redo Log

重做日志,采用的是WAL(Write-ahead Logging),因此属于顺序IO,大大提高了修改数据的性能。InnoDB所有的修改都先写入Redo Log(会保证幂等性),再更新到Buffer Pool,保证了数据不会因为MySQL宕机而丢失。Redo Log采用了物理Page中的逻辑记法,记录了PageID跟Record Offset。

Undo Log

回滚日志,保存了事务发生之前的数据的一个版本,可以用于事务回滚,通过Undo Log可以实现非锁定锁。由于Undo Log是逻辑日志,写Undo Log并不是顺序写入的,因此Undo Log也需要缓存在内存中,并且对Undo Log的操作也需要记录到Redo Log里。

隐藏列

  • PK(Primary Key):在InnoDB中,每一行数据都有一个Primary Key;如果用户创建表时没有指定的话InnoDB会创建一个隐藏的Primary Key。因为InnoDB的行数据都是放在聚簇索引里的,因此必须要有一个Primary Key

  • ROW_TRX_ID:事务ID,单调递增的唯一ID,每个事务都会分配一个,用于可见性判断

  • ROW_ROLL_PTR:保存回滚段(Undo Log)地址信息(spaceid,pageno,offset),用于回溯上一个版本

Read View

Read View主要用来判断数据的可见性。在InnoDB中,Read View主要包含如下数据:

  • m_low_limit_id:大于这个ID的事务均不可见;记录创建Read View时当前最大的事务ID

  • m_up_limit_id:小于这个ID的事务均不可见;记录创建Read View时当前最小的事务ID

  • m_ids:记录创建Read View时当前正在执行的事务ID列表;之所以还需要记录当前正在执行的事务ID列表是因为可能有些事务执行比较慢,超成m_up_limit_id记录的是很早之前的事务ID,实际上已经有很多比m_up_limit_id这个事务结束得早

  • m_creator_trx_id:创建该Read View的事务ID

CheckPointing

InnoDB对DB所有的修改操作都会在Redo Log记录一条日志,因此通过Redo Log能完整地还原DB数据。但如果每次重启DB后都需要通过Redo Log重做才能恢复DB状态的话,那启动时间会变得相当长。因此,为了缩短启动跟异常恢复的时间,就引入了CheckPoint功能。InnoDB定期会往Redo Log里写入CheckPoint信息,CheckPoint主要记录一个全局唯一递增的标号LSN(Log Sequence Number)。而且Page在修改时,会将对应的Redo Log记录的LSN记录到Page上,这样恢复重放Redo Log时,就可以用来判断跳过已经应用的Redo Log,从而实现了重放的幂等。

锁机制

InnoDB的锁可以用来解决不同事务之间写数据的并发问题。InnoDB支持行级锁,大大提高了不同事务之间的读写并发。InnoDB的锁是基于索引实现的,因此,如果没有索引那会退化到表锁。InnoDB的锁主要有:

  • 记录锁:锁定一行数据

  • Gap锁:锁定一个区间

  • Next-key锁:记录锁 + 间隙锁,锁定记录 + 区间

  • 意向锁:因为InnoDB也允许使用表锁,如果没有意向锁的话,那要加一个表级别的排他锁就需要做全表扫描,看有没有行级别的排他锁,效率非常低,因此引入了意向锁。加行级别的排他锁之前先要加表级别的意向排他锁。意向锁又分为:意向共享锁:表示有事务正在或者有意向对表中的某些行加共享锁意向排他锁:表示有事务正在或者有意向对表中的某些行加排他锁

  • 插入意向锁:一种特殊的间隙锁,在多事务插入不同数据到同一索引间隙的时候,并不需要等待其它事务完成,不会发生锁等待

  • 自增锁:一种特殊的表级锁,当事务插入的数据需要AUTO_INCREMENT时,需要先获取自增锁

InnoDB使用Two-phase Locking协议(2PL,两段锁协议),即将事务分成两个阶段处理锁,加锁阶段跟解锁阶段。

  • 加锁阶段:该阶段只能进行加锁操作

  • 解锁阶段:事务只有在提交或者回滚的时候才进行解锁,在解锁的过程中不能加锁

InnoDB实现ACID的原理

原子性:Undo Log

主要通过Undo Log来实现。当事务对数据库进行修改时,InnoDB会生成对应的Undo Log,如果事务执行失败或调用了Rollback,导致事务需要回滚,便可以利用Undo Log中的信息将数据回滚到修改之前的样子。

一致性:AID

主要是通过原子性、隔离性和持久性来保证一致性;也就是说一致性是目的,AID是手段,数据库必须要实现AID三特性,才有可能实现一致性。

隔离性:MVCC + 锁

事务之间的写操作通过锁机制来保证隔离性,读操作通过MVCC来保证隔离性

持久性:Redo Log

当修改数据时,InnoDB会先在Redo Log中记录这次操作,然后再修改Buffer Pool;当事务提交时,则会调用fsync接口对Redo Log进行刷盘。如果MySQL宕机了,那么重启时会通过Redo Log对数据进行恢复。因此,InnoDB通过Redo Log保证了持久性。

InnoDB在不同隔离级别下的一致性读及锁的差异

终于要进入本篇文章的主题了:分布式事务!跟单机事务一样,分布式事务也需要保证事务的ACID特性。

分布式事务解决方案

2PC/XA

二阶段提交是一个分布式事务协议,XA是基于两阶段提交设计的一个接口标准。

首先来看看2PC的参与者:

  • Application Program:用户程序,具体的业务逻辑

  • Resource Manager:资源管理器,例如MySQL、Oracle DB等

  • Transaction Manager:事务管理器,负责事务的提交、回滚等操作

2PC的流程如下图所求:

  1. Prepare Phase:准备阶段,在这一阶段TM会给所有的RM发送Prepare消息,RM收到后会执行本地事务(写Redo Log跟Undo Log),但不提交

  2. Commit Phase:提交阶段,当所有的RM都返回Prepared时,TM就会给所有的RM发送Commit消息,当收到所有的RM的Done回复后事务就成功了

上面流程省略了Rollback,如果Prepare阶段有RM返回失败的话那TM会Rollback已经Prepared的RM。

AP跟TM通常在同一个进程(例如:Java Atomikos)。RM可以通过类似前面章节介绍的 InnoDB实现ACID的原理 来保证RM的ACID特性。TM也会生成日志记录事务操作,TM宕机重启后能通过日志来恢复事务,因此,尽量保证了分布式事务的ACID特性。

优点:

  • 原理简单,实现方便

缺点:

  • 事务执行过程中节点都处于阻塞状态,性能低下,无法满足高并发场景

  • 协调者存在单点问题,一旦协调者宕机,所有参与者都将阻塞,导致整个数据库集群都无法提供服务

  • 许多NoSQL都没有支持XA

  • 要实现一套标准的XA非常困难,所以一般都是直接使用支持XA协议的中间件,而在微服务架构中不同服务应该尽量少或者不共用中间件。因此使用XA会导致服务紧耦合

  • 数据不一致:在提交阶段,如果有RM宕机,而其它RM已经提交事务,那么此时整个分布式系统就出现了数据不一致的现象

  • 只有TM可以超时,RM没有超时机制(2PC只能做超时回滚,3PC在第二阶段才能做超时自动提交)

1PC

一阶段提交比二阶段提交少了准备阶段,即只有提交阶段:

1PC假设事务在提交前所有RM都已经准备好,因此没有准备阶段,减少了准备阶段的开销。但由于本地事务一旦提交就不能回滚,因此,1PC可能会加大数据不一致的情况

缺点:会有跟2PC一样的缺点,因此很少会有人使用

3PC

3PC(三阶段提交)是2PC的改进版本,引入超时机制,解决2PC在异常情况下的阻塞问题。3PC流程如下:

  1. CanCommit Phase:TM向所有RM发送canCommit请求,询问是否可以执行事务提交操作,如果都回复Yes则进入下一阶段;如果只有部分RM回复了Yes,那么超时后回复Yes的RM将丢弃事务操作

  2. PreCommit Phase:TM向所有RM发送PreCommit请求,RM收到请求后会执行事务操作,执行成功就返回Done;如果超时还有RM没有Done,那么TM就会丢弃事务操作

  3. DoCommit Phase:TM向所有RM发送DoCommit请求,RM收到请求后就会提交事务;如果有RM超时没有收到DoCommit请求,那么该RM也会提交事务

优点:

  • 解决了2PC中的阻塞问题,能够在TM故障后丢弃事务操作,然后继续服务

缺点:

  • 仍然存在数据不一致的问题

  • 比2PC多了一个阶段,增加了延时

TCC

TCC:Try-Confirm-Cancel,是一种分布式事务的解决方案,可用于解决分布式服务下的数据一致性问题。

可以看出,TCC可以说是服务化的2PC模型。事务资源管理者都需要提供三个幂等的接口:

  • Try:负责检查和预留资源

  • Confirm:确认执行事务

  • Cancel:取消Try阶段预留的资源

在第一阶段,TM会调用各个RM的Try方法,检查跟预留资源;在第二阶段,如果满足条件就Confirm提交事务,如果不满足条件就Cancel回滚事务。Try、Confirm跟Cancel都应该是幂等的,因为在超时或者宕机等情况下需要重试。

优点:

  • 支持跨服务的分布式事务

  • 事务执行不会锁住整个资源,只需要预留就好了,大大提高了并发

缺点:

  • 各个接口都需要是幂等的,开发难度大,成本高

  • 维护成本高

  • 各服务可以说紧耦合,只要有一个服务Try接口出问题,都有可能导致业务主流程不可用

MQ

基于MQ来实现分布式事务,使各RM解耦。事务发起方在完成本地事务就可以往MQ丢一个消息,让其它RM消费MQ来执行本地事务。

优点:

  • 异步,通过MQ与各业务解耦

  • 扩展性强

  • 性能较好

  • 对可靠性跟一致性要求不高的话很容易实现

缺点:

  • 推送MQ消息的可靠性难保证

  • 消费MQ消息的可靠性难保证

  • 消费方处理MQ消息只能顺序处理,失败了难以重试

可以看出,使用MQ有很多坑需要处理。很难保证数据强一致性。对于消息生产方来说,如果要保证消息一定要推到MQ的话需要将消息保存下来,对于推送失败的要重试,然后消息消费方还需要保证幂等性。但使用MQ性能较好,因此对于不需要强一致性的场景很适合使用。

Saga

Saga是一种长事务解决方案,会将一个长事务分成多个小事务,每个本地事务有相应的执行模块和补偿模块。如下图所求:

如果执行事务T2失败了,那么会调用事务1的补偿模块C1。

目前主要有两种实现方式:

  • 集中式的实现方式(Orchestration-based coordination):集中式协调器负责服务调用以及事务协调

  • 分布式的实现方式(Choreography-based coordination):通过事件驱动的方式来进行事务协调

缺点:

  • 只提供ACD保证,不具备隔离性,如果多个事务同时操作同一资源会导致更新丢失

  • 实现复杂,需要事务调用链跟踪

  • 如果使用事件驱动的方式的话需要考虑幂等性

具体可以参考:Apache ServiceComb Pack的Saga模式

Seata

Seata(Simple Extensible Autonomous Transaction Architecture)是一款开源的分布式事务解决方案,目前主要提供了以下三种模式:

  • AT模式:提供无侵入自动补偿的事务模式

  • TCC模式:可以支持与AT混用,灵活度高

  • Saga模式:长事务解决方案

Seata的架构如下:

主要有三种角色:

  • Transaction Coordinator:事务协调器,主要负责维护全局事务跟分支事务的状态,提交跟回滚全局事务

  • Transaction Manager:定义全局事件

  • Resource Manager:资源管理器

前面已经介绍过TCC跟Saga模式了,接下来就简单介绍下AT模式。

AT模式需要在事务链涉及的服务的数据库中新建undo_log表用来存储Undo Log信息。AT模式主要也是分成两个阶段,第一个阶段会通过SQL拦截,生成修改数据前后的快照Undo Log,和业务修改一同在一个本地事务中提交。第二阶段如果执行成功那么只是异步删除Undo Log,如果失败则通过Undo Log生成反向SQL语句回滚第一阶段的修改。Seata AT模式通过全局锁(由表名+操作行的主键来组成)来保证隔离性,RM在第一阶段提交事务前会先询问TC是否有全局存锁存在,如果有则抛出异常,RM循环等待并重试。

Percolator

Percolator是Google在Bigtable上实现的分布式事务协议,是对2PC二阶段协议的一个优化改进。Percolator实现了SSI(Serializable Snapshot Isolation)隔离级别。我们先来看看Snapshot Isolation。

Snapshot Isolation

Snapshot Isolation是在MVCC出现之后才开始流行起来的。Snapshot Isolation是一个和Repeatble Read差不多强度的隔离级别,但Repeatable Read有幻读的问题,而Snapshot Isolation没有幻读问题。Snapshot Isolation也解决了Two-phase Locking带来的读写性能低下的问题。Snapshot Isolation有如下特点:

  • 每一个数据都有多个版本,读写能够并发进行

  • 读不需要加锁

  • 写需要加锁,只有拿到锁的事务才能执行(可以看出只有在事务提交的时候才能加锁)

所以为了解决Write Skew的问题,又出了Serializable Snapshot Isolation。

Serializable Snapshot Isolation

Serializable Snapshot Isolation通过判断一个事务读的数据的最近一个版本的提交时间是否早于事务的开始时间来解决Write Skew的问题。

Percolator事务提交流程

事务冲突处理方式:读时遇到锁就等锁超时或者锁被释放;写时遇到锁就直接回滚,返回失败让客户端进行重试。因此采用的是乐观锁事务模型。具体流程如下:

Percolator主要有三个组件:

  • Client:协调者,负责发起事务、回滚事务等操作

  • TSO:全局授时服务,提供全局唯一且递增的时间戳

  • Bigtable:一个分布式的数据存储系统

在Percolator中,每行数据都会有三个隐藏列:

  1. data.lock_ts:写数据锁,记录事务开始时间

  2. data.write_ts:记录事务的提交时间

  3. data.primary:记录Primary,用于Failover时定位到primary

Percolator Prewrite 流程:

  1. 从TSO获取start_ts

  2. 预写Primary Data,会做如下检查,满足以下两个条件之一就会回滚:

  3. 预写Secondary Data,处理流程跟预写Primary Data一样

Percolator Commit 流程:

  1. 从TSO获取commit_ts

  2. 提交Primary Data,释放锁并记录事务提交时间:data.lock_ts = nulldata.write_ts = commit_ts

  3. 提交Secondary Data,处理流程跟提交Primary Data一样

Percolator Failover流程:

从上图可以看出,Percolator会先选一个数据做Primay,在Prewrite阶段先Prewrite Primary,在Commit阶段先Commit Primary。这样做的好处是可以以这个Primary数据来做事务的状态判断(即相当于存储事务的状态到Primary数据)。Primary Committed意味着事务Committed,Primary Rollback意味着事务Rollback。Percolator事务的Failover采用的是Lazy的方式,一个事务的Failover由另一个事务的读操作触发。读数据时如果发现数据被上锁了且锁已经失效,那么就会取出data.primary,然后查询Primary判断事务是否已经提交,如果事务提交则将这个数据也提交;如果事务已经Rollback,那么就将这个数据的锁清除。

参考

Consensus on Transaction Commit

Google I/O 2009 - Transactions Across Datacenters

分布式系统常见同步机制

深入学习MySQL事务:ACID特性的实现原理

InnoDB的read view,回滚段和purge过程简介

InnoDB MVCC 详解

MySQL InnoDB MVCC实现

Checkpointing

庖丁解InnoDB之REDO LOG

MySQL分布式事务(XA事务)

Distributed Transaction Processing: The XA Specification

TDSQL XA的事务隔离级别

阿里分布式事务框架 GTS 全解析

CAP 理论十二年回顾:"规则"变了

分布式事务之TCC服务设计和实现注意事项

基于RabbitMQ消息队列的分布式事务解决方案 - MQ分布式消息中间件实战

Saga分布式事务解决方案与实践

Seata AT 模式分布式事务源码分析

Codis作者首度揭秘TiKV事务模型,Google Spanner开源实现!

MVCC事务机制:Snapshot Isolation

Percolator 和 TiDB 事务算法

发布于: 2020 年 06 月 17 日 阅读数: 18
用户头像

Chank

关注

还未添加个人签名 2019.02.06 加入

邮箱:fangliquan@qq.com

评论

发布
暂无评论
分布式事务解析