事务及分布式事务

2021-04-25 11:01:48 浏览数 (1)

事务(Transaction)是并发控制的单位,是用户定义的一个操作序列。 这些操作要么都做,要么都不做,是一个不可分割的工作单位。

本地事务

本地事务要求符合ACID的特性:

1. A:原子性(Atomicity)

原子,是指不能分解成更小的部分的东西。在多线程编程同时访问相同的数据会发生什么情况,这种问题是由隔离性ACID的原子性是在描述当用户进行多次写入,并且在一些写操作出现故障的情况,例如进程崩溃、网络连接中断,磁盘变满或者某种完整性约束被违反。多个操作被分到一个原子事务中,要不全部完成,要么全部回滚。如果回滚,可以确定应用程序本次操作没有带来任何改变,所以可以安全地进行重试。在Mysql中原子性的实现是主要依靠其undo log来实现的。Mysql的undo log记录了事务修改操作之前的数据,用于在当前事务发生回滚的时候,使该条数据状态恢复到事务开始前的状态。

2. C:一致性(Consistency)

一致性的概念是:“对数据的一组特定约束必须始终成立,即不变量(invariants)”。例如,在银行账户的转账操作中,收款方和转账方的总余额是永远不变的。但是一致性的这种概念取决于应用程序对于不变量的观念。应用程序负责正确定义事务,并保持一致性。这并不是数据库可以保证的事情,数据库只负责存储,至于存储的是不是脏数据,需用它的用户来定义。应用程序需要依赖数据库提供的AID来实现C,但C并不属于数据库,所以ACID中的C有点拼凑的嫌疑。

3. I:隔离性(Isolation)

如在原子性的表述。这里的隔离性是数据库用来解决竞态条件(race conditions)问题的。隔离性的意思是:在数据库中同时执行的多个事务是相互隔离的,他们彼此间不会相互影响。

Mysql中实现隔离性主要是通过加锁(排它锁和共享锁等)以及MVCC(全称Multi-Version Concurrency Control,即多版本并发控制)来实现的。

Mysql中的Innodb引擎支持事务,有4个隔离级别:

  1. 读-未提交 读-未提交是说一个事务(假设A)可以读到其他事务(假设B)尚未提交的数据。如果事务B发生回滚,则事务A读到的数据被称为脏数据,这个过程称为脏读
  2. 读-已提交 读-已提交是隔离级别中最基本的存在。它保证事务只能看到已经提交的数据。这样就有效避免了脏读的问题。
  3. 可重复读不可重复读是指在一个事务中多次读取一条数据,由于中间存在其他事务修改该数据而导致的前后的结果不同。读-已提交虽然解决了脏读的问题,但却不能解决不可重复读的问题。Mysql中的可重复读通过引入多版本并发控制(MVCC)解决了这个问题。MVCC在读-已提交和可重复读两个隔离级别生效,主要用于解决读写/写读冲突。是一种无锁并发控制,原理是通过为事务分配单项增长的事务ID,并为每行数据保存一个版本链,版本链上的数据与修改它的事务ID相关联。
  4. 串行化 串行化是一种最高的隔离级别,指各个事务之间的操作看上去就像是串行执行的一样。
4. D:持久性(Durability)

持久性是指:一旦事务成功完成,即使发生硬件故障或者数据库崩溃,写入的任何数据也不会丢失。在单节点的数据库中,持久性通常意味着数据已经被写入非易失性存储设备,如硬盘或者SSD。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点,为了提供持久性保证,数据库必须等到这些写入或者复制完成之后,才能报告事务成功提交。最真实的是“完美的持久性是不存在的”:如果所有硬盘都被损坏,那依然没有任何数据库能够救得了你。

事务的一个关键特性是指:如果发生了意外,所有操作被终止,之后可以安全地重试。ACID数据库基于这样的一个理念:如果存在违反原子性、隔离性或者持久性的风险,则完全放弃整个事务,而不是部分放弃。从这一点上说,支持安全的重试机制甚至说支持安全的容错机制,并且尽可能地把这些与使用数据库的客户端隔离开来,才是事务的职责所在。分布式事务也可以说是沿着这个思路,尝试建立可以让分布式应用忽略内部各种问题的抽象机制。

分布式事务

1. CAP定理

CAP理论又叫Brewer理论,是加州大学伯克利分校的埃里克.布鲁尔(Eric Brewer)教授提出的一个猜想,后来麻省理工学院的赛斯.吉尔伯特(Seth Glibert)和南希.林奇(Nancy Lynch)就以严谨的数学推理证明了这个CAP猜想。在这之后CAP理论在分布式领域盛行一时。

CAP定理是以下三个特性最多只能其中两点,不可能三者兼顾。

  • 一致性(Consistency) 代表在任何时刻,任何分布式节点中我们所看到的数据都是没有矛盾的。这里的一致性与ACID中的含义不同,ACID强调数据库状态的一致性或者说任何时刻数据库状态不可以违反客户端定义的约束条件。而这里的一致性指分区一致性;
  • 可用性(Availability) 代表系统不间断地提供服务的能力,常说的“高可用”中的“可用”也是这个意思。
  • 分区容错性(Partition Tolerance) 代表分布式环境中,当部分节点因网络原因而彼此失联时,系统仍能正确地提供服务的能力。

CAP这三个特性中分区容错性是不可以放弃的,因为网络可能永远可靠,那么CAP就变成了CP或者AP间的博弈。如果放弃可用性(CP),那么一旦发生分区不一致问题,系统将无限期停止对外提供服务直至一致性达成。这等同于2PC/3PC的解决方案。如果**放弃一致性(AP)**则表示当发生分区问题,节点之间所提供的数据可能不一致。但是一致性是事务的最终目标,这里放弃一致性有点难以接受,所以后边又引入了“最终一致性”,也就是BASE理论。

2.BASE 理论。

ACID译为"酸",对应BASE译为"碱"。BASE是由eBay的系统架构师丹 · 普利切特(Dan Pritchett)提出,主要有三个特性:

  • Basically Available(基本可用) 基本可用并不是完全不可用,是指系统在出现不可预知的故障的时候,允许损失部分可用性。例如响应时间上略微增加、由于流量高峰而主动触发的部分功能降级等等;
  • Soft State(软状态) 软状态也称为弱状态,与硬状态相对。是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响到系统的整体个可用性,或者说允许系统在不同节点的数据副本进行数据同步的过程存在延迟;
  • Eventually Consistent(最终一致性) 最终一致性意味着如果停止更新数据库,并等待一段时间(长度未知)之后,最终所有读请求会返回相同的内容。换句话说,不一致现象是暂时的,最终会达到一致(假设故障会被修复)。

3.分布式事务解决方案

3.1. 两阶段提交(Two-phase Commit Protocol,2PC)

提到2PC,不得不提XA规范。XA规范是X/Open组织定义的异构环境下实施两阶段提交的一个工业标准,于1991年推出并得到广泛推广。目前,许多传统关系数据库(包括PostgreSQL、MySql、DB2、SQL Server和Oracle)和消息队列(ActiveMQ等)都支持XA。XA规范使用两阶段提交来保证所有资源同时提交或者回滚,并规定了事务管理器(TM)和资源管理器(RM)接口。事务管理器相当于协调者,负责各个本地资源的提交和回滚;而资源管理器就是分布式事务的参与者,通常为数据库。

两阶段提交通过引入事务管理器(也称协调者)将事务的提交分为了投票(canCommit)和提交(DoCommit)两个阶段,这或许也是其名称的由来。

  • 第一阶段:投票(CanCommit)

协调者会向事务的资源管理器发起执行操作的CanCommit请求,并阻塞等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不修改数据库中的数据),待参与者执行成功,向协调者发送“YES”消息,表示同意操作,若不成功,则发送“NO”消息,表示终止操作。

  • 第二阶段:提交(DoCommit)

当所有参与者都返回了操作结果之后,系统进入第二阶段——提交阶段。在第二阶段中,协调者会对所有返回消息进行判定,如果所有消息均为“YES”,则持久化事务状态为“Commit”,并向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后,完成剩余操作,释放之前锁定资源,然后向协调者返回“HaveCommited”消息;若协调者从参与者收到的消息包含“NO”,则持久化事务状态为“Abort”,并向所有参与者发送“DoAbort”消息。此时所有之前发送“YES”消息的参与者再收到“DoAbort”消息后进行回滚,然后向协调者发送“HaveCommitted”消息。基于XA的两阶段提交算法尽可能地保证了数据的强一致性,且实现成本较低,但依然有些不足:

  • 性能问题2PC在执行过程中,所有参与节点都是事务阻塞的。也就是说,当本地资源管理器占有临界资源的时候,会阻塞其他资源管理器访问。而且在这个期间,需要经历2次远程服务调用、3次数据持久化操作(投票阶段写redo日志、协调者做状态持久化、提交阶段在日志写入Commit Record),也因此2PC不支持高并发场景。
  • 单点故障问题在2PC中,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器故障,会导致被资源管理器锁定的资源无法释放,进而导致整个系统的阻塞;
  • 数据不一致问题如果第二阶段发生网络故障,会导致部分参与者接收到了"DoCommit"消息并完成事务提交,而部分未能接收到消息也就无法完成事务提交,进而带来数据的不一致问题。
3.2. 三阶段提交(Three-phase Commit Protocol,3PC)

3PC是2PC的改进版本。为了解决2PC存在的问题,3PC引入了超时机制和预提交阶段。2PC也有超时机制,只不过是只有协调者才有,改进后的3PC在协调者和参与者均引入了超时机制,这时如果参与者或者协调者在规定的时间内没有接收到其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,从而减少了整个集群的阻塞时间,在一定程度上也减少了2PC中出现的性能问题单点问题

三阶段提交协议有CanCommit、PreCommit、DoCommit三个阶段,相当于把投票阶段拆分成了CanCommit和PreCommit两个阶段。

  • 新的CanCommit功能减弱,通过一次简单询问来判断参与者的状态是否支持事务顺利完成。
  • 原有的投票阶段压力很大,一旦协调者发出投票阶段的消息,所有的参与者都将开始写redo日志,并且相关数据资源被锁住。如果其中任何一个参与者无法完成提交,则功亏一篑。所以提前增加一个询问阶段,相当于降低了这种风险发生的系数。因此3PC在需要回滚的情况下性能会好很多,但是在正常情况下,由于增加了一次交互,性能还是要差一些。

然而在数据一致性的问题上,3PC并没有改善:因为在DoCommit阶段引入了超时预判机制,当协调者发送出的最终指令是“Abort”时并且恰巧碰到了网络问题,这个时候会导致一部分接收到消息的参与者回滚,没收到消息的由于超时机制会提交事务,导致不一致问题。

3.3. 可靠事件队列

上边阐述的2PC和3PC是一种追求强一致性的解决方案,而可靠消息队列则是最终一致性的解决方案。借用我之前做过的一个例子来阐述可靠事件队列,上家公司是家门户网站,我所在广告投放部门有这样一个需求:需要对广告主开放支持投放另一媒体(涉密,这里称媒体B)的广告。具体就是说广告主可以在我们的平台录入投放另一媒体的广告信息,主要包括广告组、广告、创意等,然后需要我们通过消息同步的方式将数据同步到对方的投放存储中。这里我们使用的解决方案就是类似于可靠事件队列的方式。

上图是我根据可靠事件队列的模式画出来的序列图,但是当时实际研发有一点不同的是我们并没有使用消息队列,一个是因为我们隶属不同公司,再一个业务初创没有必要。我们是使用mysql任务表,外加一个定时服务不停去扫描任务表中的未同步成功的任务去做同步处理(当然这里有一定上限设定)。

在这个模型实现中,广告主在广告管理平台中向本地的Mysql完成新增/修改操作,会在事务中同步新增一个任务表(或者同步任务内容到消息队列),直到定时任务扫描到任务并且同步成功,最终达到两方的数据一致性。这里需要注意的是任务的同步要求具有幂等性,通常采用携带事务ID或者版本号的方式,以保证一个事务中的操作只会被执行一次。

当同步重试超过一定的上限,可能是由于某些约束条件实在无法满足,需要人工介入,当时我们的项目有另外一个可视化页面可以对失败的任务情况进行监视并且手动重试。

这种靠着持续重试的方式来保证一致性的操作,有个专业的名字叫“最大努力交付(Best-Effort Delivery)”,比如TCP协议中的可靠性保障,就属于最大努力交付。

3.4. TCC(Try-Confim-Cancel)

TCC是除可靠消息队列外的另一种常见的分布式事务机制,由数据库专家帕特 · 赫兰德(Pat Helland)提出。使用可靠消息队列的方式解决了最终一致性的问题,但是对于隔离性的问题却没有保证。对于有些扣费业务,当多个商户同时操作的时候,容易发生超扣的现象。这个时候可以考虑TCC解决方案。

TCC的实现相对较为复杂,业务入侵性也较高,要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。主要分三个阶段:

  • Try 尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有资源(保障隔离性);
  • Confim 确认执行阶段,不进行任何业务检查,直接使用Try阶段预留的资源来完成业务。Confim阶段如果失败,会重复执行,所以操作需要支持幂等性;
  • Cancel 取消执行阶段,释放Try阶段预留的业务资源。Cancel阶段也可能重复执行,因此也需要满足幂等性;

TCC类似于上文中的2PC,但又有所不同:TCC位于用户代码层面,而非基础设施层面。它以较强的业务入侵性带来了较高的灵活性,让我们可以根据需要设计资源锁定的粒度。

3.5. SAGA

SAGA历史较为悠久,是普林斯顿大学的赫克托·加西亚·莫利纳(Hector Garcia Molina)和肯尼斯·麦克米伦(Kenneth Salem)发明的。

SAGA提供了一种基于数据补偿来代替回滚的解决思路:把一个大事务分解为可以交错运行的一系列子事务的集合,由2部分组成。

一部分是把大事务拆分为若干个小事务,将整个分布式事务T分解为n个子事务,我们命名T1,T2,...,Ti,...,Tn。每个子事务都应该、或者能被看做是原子行为。如果分布式事务T能够正常提交,那么它对数据的影响(最终一致性)就与连续按顺序成功提交子事务T等价。

另一部分是每一个子事务对应的补偿操作,我们命名为C1,C2,...,Ci,...,Cn。Ti与Ci满足如下条件:

  • Ti与Ci都是幂等的;
  • Ti与Ci满足交换律,即TiCi = CiTi,先执行Ti或者先执行Ci,结果一样;
  • Ci必须要执行成功,即Ci如果执行失败,需要一直重试或者需要人工介入。

SAGA的思路也可以用在前面广告信息同步上,我们可以把广告组、广告、创意依次同步到对方媒体的数据存储中,如果创意数据在对方的数据库违反了约束条件,则需要依赖对方提供的删除接口删除广告、广告组的数据。

3.6. AT

AT事务是对XA 2PC的一个升级版本。

它的大致做法是在业务数据提交时,自动拦截所有SQL,分别保存SQL对数据修改前后结果的快照,生成行锁,通过本地事务一起交到操作的数据源中,这就相当于自动记录了redo日志和undo日志。所以,基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。AT事务这种异步提交的模式,相比2PC极大地提升了系统的吞吐量。而使用的代价就是大幅度牺牲了隔离性,甚至于影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚不一定总是成功。

对于脏写(写覆盖)的处理,需要借助于分布式锁的实现来实现写隔离,甚至如果要限制脏读,也可以这样做,但是会对性能造成很大影响。

4. 总结

本文从本地事务的ACID引申到分布式事务的解决方案:从开始的2PC到3PC的升级点,再到后来CAP理论的引入让我们的目光从强一致性的追求上解脱出来,去考虑C(一致性)与A(可用性)的取舍。C作为事务的目标,舍弃往往是难以接受的。所以BASE引入了最终一致性。本文同样也叙述了使用最终一致性的可靠消息队列、TCC、SAGA以及TA 四种解决方案。如同,计算机内时间和空间的博弈一样,这里没有胜负。有的是我们针对不同业务场景,在不同解决方案中的选择。

0 人点赞