什么是事务

事务是由一组操作构成,如果这一组操作中的任意一个步骤发生错误,那么久需要回滚之前已经完成的操作,也就是事务作为一个整体,要么全部正确执行,要么不执行。

事务的四大特性ACID

  • 原子性:事务是不可分割的执行单元,事务中的所有操作要么全部执行,要么全部不执行

  • 一致性:事务在开始前和结束后,数据库的完整性约束没有被破坏

  • 隔离性:事务的执行是相互独立的,它们不会互相干扰,一个事务不会看到另一个正在运行过程中的事务的数据

  • 持久性:一个事务完成之后,事务的执行结果必须持久化保存的,即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

事务的隔离级别

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。

事务并发执行会出现的问题

在不同的隔离级别下,数据库可能会出现的问题:

  1. 更新丢失:当有两个并发执行的事务更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。当数据库没有任何锁操作的情况下回发生

  2. 脏读:一个事务读到另一个尚未提交的事务中的数据,该数据可能会被回滚从而失效

  3. 不可重复读:一个事务对另一行数据读了两次,却得到了不同的结果

    1. 虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录

    2. 幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化

数据库的四种隔离级别

  1. read uncommitted 未提交读:在该级别下,一个事务对另一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。不会出现更新丢失,但会出现脏读、不可重复读

  2. read committed 提交读:在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务访问该行数据,因此会出现不可重复读的情况

  3. repeatable read 重复读:在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据,且写事务禁止其他一切事务

  4. serializable 序列化:改级别要求所有的事务必须串行执行,因此能避免一切因并发引起的问题

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

什么是分布式事务

当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,这里说的分区指的是物理分区,分区之后可能不同的库就处于不同的服务器上了,这个时候单个数据库的ACID已经不能适应这种情况了,而在ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者是即时能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是CAP理论

CAP定理

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

  • 一致性(Consistency):客户端知道一系列的操作都会同时发生

  • 可用性(Availability):每个操作都必须以可预期的响应结束

  • 分区容错性(Partition tolerance):即使出现单个组件不可用,操作依然可以完成

具体地讲在分布式系统中,在任何数据库设计中,一个web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性和可用性之间做出选择。

BASE理论

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

  • Basically Available(基本可用) 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。

功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

  • Soft state(软状态) 相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。

软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

  • Eventually consistent(最终一致性)

因果一致性(Causal consistency)
因果一致性指的是:如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。于此同时,和节点A无因果关系的节点C的数据访问则没有这样的限制。
读己之所写(Read your writes)
读己之所写指的是:节点A更新一个数据后,它自身总是能访问到自身更新过的最新值,而不会看到旧值。其实也算一种因果一致性。
会话一致性(Session consistency)
会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现 “读己之所写” 的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。
单调读一致性(Monotonic read consistency)
单调读一致性指的是:如果一个节点从系统中读取出一个数据项的某个值后,那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。
单调写一致性(Monotonic write consistency)
单调写一致性指的是:一个系统要能够保证来自同一个节点的写操作被顺序的执行。

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

分布式事务协议

1、两阶段提交协议-2PC

两阶段提交算法成立基于以下假设:

  1. 该分布式系统中,存在一个节点作为协调者,其他节点作为参与者,且节点之间可以进行网络通信

  2. 所有节点都采用预写式日志,且日志被写入后即被保存在可靠的存储设备上,即使节点损坏也不会导致日志数据的丢失

  3. 所有节点不会永久性损坏,即使损坏后仍然可以恢复

第一阶段(投票阶段)

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应

  2. 参与者节点执行询问发起为止的所有事务操作,并将undo和redo信息写入日志

  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点事务操作实际成果,则它返回一个“同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个“终止”消息

第二阶段(提交执行阶段)
当所有节点获得的消息都为“同意”时

  1. 协调者节点向所有参与者节点发出“正式提交”请求

  2. 参与者节点正式完成操作,并释放在整个事务期间占用的资源

  3. 参与者节点向协调者节点发送“完成”消息

  4. 协调者节点向所有参与者节点反馈“完成”消息后,完成事务

如果任一参与者节点第一阶段返回的响应消息为“终止”,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

  1. 协调者节点向所有参与者节点发出“回滚操作”的请求

  2. 参与者节点利用之前写入的undo信息执行回滚,并释放在整个事务期间占用的资源

  3. 参与者节点向协调者节点发送“回滚完成”消息

  4. 协调者节点收到所有参与节点反馈的“回滚完成”消息后,取消事务

和上一节提到的数据库XA事务一样,两阶段提交就是使用XA协议的原理,两阶段提交这种解决方案牺牲了一部分可用性来换取一致性
优点:尽量保证了数据的强一致性,适合对数据强一致要求很高的关键领域
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发性能场景
同步阻塞、单点故障、数据不一致
分布式事务 - 图1

2、三阶段提交协议-3PC

与两阶段提交不同的是,三阶段提交有两个改动点。

引入超时机制。同时在协调者和参与者中都引入超时机制。 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

2.1 CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

2.2 PreCommit阶段

协调者根据参与者的反应情况来决定是否可以继续事务的PreCommit操作。根据响应情况,有以下两种可能。 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

发送中断请求 协调者向所有参与者发送abort请求。

中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

2.3 doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

2.3.1 执行提交

发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。 响应反馈 事务提交完之后,向协调者发送Ack响应。 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

2.3.2 中断事务

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

发送中断请求 协调者向所有参与者发送abort请求

事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

分布式事务解决方案

在分布式系统中,要实现分布式事务,无外乎那几种方案。

1、全局事务

全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色:

  • AP:Application 应用系统它就是我们开发的业务系统,在我们开发的过程中,可以使用资源管理器提供的事务接口来实现分布式事务。

  • TM:Transaction Manager 事务管理器

    • 分布式事务的实现由事务管理器来完成,它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。

    • 事务管理器还管理着所有的资源管理器,通过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。

    • DTP只是一套实现分布式事务的规范,并没有定义具体如何实现分布式事务,TM可以采用2PC、3PC、Paxos等协议实现分布式事务。

  • RM:Resource Manager 资源管理器

    • 能够提供数据服务的对象都可以是资源管理器,比如:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。

    • 资源管理器能够提供单数据库的事务能力,它们通过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。

    • XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。

    • DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。

2、补偿事务(TCC)

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

  • Try阶段:主要是对业务系统做检测及资源预留

  • Confirm阶段:主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段不会出错。即只要Try成功,Confirm一定成功

  • Cancel阶段:主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源资源

优点:跟2PC比起来,实现以及流程相对简单一些,但数据的一致性比2PC也要差一些
缺点:缺点还是比较明显的,confirm和cancel阶段都可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理

3、本地消息表(异步确保)

本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路来源于ebay。
基本思路就是:
消息产生方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据需要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并执行自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发一遍。如果有靠谱的自动对账补偿逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,这是这几种方案里面比较适合实际业务场景的,即不会出现2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性
缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会比较麻烦

4、MQ事务消息

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上的一些主流的MQ都是不支持事务消息的,比如RabbitMQ和Kafka都不支持。
以RocketMQ为例,其思路大致为:
第一阶段prepared消息,会拿到消息的地址
第二阶段执行本地事务
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态

也就是说在业务方法内想要消息队列提交两次请求,一次发生消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
优点:实现了最终一致性,不需要依赖本地数据库事务
缺点:实现难度大,主流MQ不支持

5、Sagas事务模型

Saga事务模型又叫长时间运行的事物,由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的情况下解决分布式系统中复杂的业务事务问题。
我们这里说的是一种基于Saga机制的工作流事务模型,这个模型的相关理论目前来说还是比较新的
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由sagas工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
由于理论较新,没有太多资料。

总结
通过以上描述,我们了解到两个分布式系统理论,分别是CAP和BASE理论,同时我们也总结并对比了几种分布式解决方案的优缺点,分布式事务本身是一个技术难题,没有一种完美的解决方案应对所有场景,具体根据业务场景进行选择,