后端架构师必知必会系列:分布式事务与幂等性

发布于:2023-10-25 ⋅ 阅读:(90) ⋅ 点赞:(0)

作者:禅与计算机程序设计艺术

1.背景介绍

在互联网+、大数据和云计算的背景下,系统架构的演进至少发生了四次变革。但随着业务规模的扩大,单体应用逐渐面临边界瓶颈。于是,微服务架构、SOA架构、Serverless架构出现并蓄势待发。然而,随着微服务架构的流行,分布式系统架构也越发显得重要起来。特别是在互联网+、移动互联网等新兴领域,传统单体架构已难以满足需求。因此,如何构建具有弹性、容错、高性能、可伸缩的分布式系统架构成为重中之重。而分布式事务(Distributed Transaction)、幂等性(Idempotence)则是解决这些问题的关键。

通过本文,你将可以理解什么是分布式事务、为什么需要它?以及其中的具体原理及运作方式。最后还会涉及到一些常见问题和解答。希望能够帮助你消化吸收知识点,加强自我学习能力,提升职场竞争力。

2.核心概念与联系

分布式事务(Distributed Transaction)

分布式事务(Distributed Transaction)是指两个或多个事务管理器之间存在着复杂的协调关系,使得它们要么全都成功,要么全都失败的一种处理方式。它是用来确保在不同数据库之间或者跨越网络时多个交易一致运行的机制。简单的说,就是保证多条数据库操作(比如更新某个数据)的完整性和一致性。

事务的特性(ACID属性)

分布式事务有ACID属性,即原子性、一致性、隔离性和持久性。

原子性:一个事务是一个不可分割的工作单位,事务中包括对数据库的读写操作,要么全部成功,要么全部失败。

一致性:事务必须是数据库从一个正确状态转换成另一个正确状态。如果事务成功,那么所有的数据都必须保持一致,否则系统处于不一致的状态。

隔离性:当多个用户并发访问数据库时,一个用户的事务不被其他用户影响,各个用户看到的数据是一致的。即一个事务不能被其他事务所干扰,多个用户并发事务相互独立。

持久性:持续性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作和数据获取都依赖这个事务。只有在事务提交之后,才算真正完成了持久化工作。

总结:ACID是指数据库事务的四个特性,任何一次对数据库的操作,在执行前都必须满足这些条件,否则就会造成数据的不一致性。

两阶段提交协议(Two-Phase Commit Protocol)

为了实现分布式事务,最经典的协议是两阶段提交协议。该协议是1981年由经典论文“Efficient Atomic Broadcast in the Presence of Network Failures”提出的。

第一阶段:准备阶段(prepare phase)

准备阶段主要是通知所有的参与者(即资源管理器)事务将要执行,试图获得锁。参与者的反应一般是同意,也可以表示否决,反映在投票结果上。假设有一个事务参与者执行失败,则整个事务回滚;否则,事务进入第二阶段。

第二阶段:提交阶段(commit phase)

提交阶段是通知所有的参与者事务已经成功,提交更改。

优缺点

优点
  • 简单
  • 可靠性高
  • 支持异步提交(两阶段提交允许一方无需等待另一方的响应直接提交)
缺点
  • 需要依赖超时机制:任何一个事务在进行过程中,都有可能由于各种原因发生超时,而导致系统资源浪费。
  • 同步阻塞效率低:任何节点的失败都会导致所有结点等待,甚至引起拖累。
  • 数据不一致:长事务占用资源过多,可能会导致其他事务无法继续执行。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

概念阐述

幂等性(Idempotence),又称为源于亚里士多德的一个概念。一个函数f(x)在多次调用中若始终保持相同的输入参数x,并且每次调用都产生相同的输出值,则称此函数f(x)是幂等的。也就是说,任意多次重复该函数调用所得到的结果都是相同的。举例来说,对同一个账户取钱,若多次取款的金额和次数没有变化,则每次取款的金额都是相同的。

操作流程图

算法原理概括

  • 检查数据是否存在:在数据保存之前,先检查该数据是否已经存在。如若不存在,则创建数据,然后进入事务阶段。
  • 提交事务请求:当所有检查都完成后,事务发起方会向所有参与者发送Commit请求。
  • 执行事务:在收到所有参与者的确认消息后,参与者开始执行事务。
  • 提交事务:当事务顺利完成,所有参与者都提交事务,同时返回事务提交信息给事务发起方。
  • 事务结束:事务发起方接收到所有参与者的提交信息后,事务结束。

时序图

  1. 客户端C1向数据库请求插入一条记录R1。
  2. 服务端S1发现该表中尚不存在该记录,于是给客户端S1发送通知消息。
  3. C1收到消息后,对R1进行预处理,生成一份预备记录PR1。
  4. C1通知其他客户端等待。
  5. S1接收到通知后,向所有服务端发送请求,准备提交事物。
  6. S1收到所有其他服务端的确认消息,然后将R1和PR1分别写入到主表和备份表中。
  7. C1收到所有服务端的回复后,判断两份记录的哈希值是否一致,一致则表示事务成功,否则代表事务失败。

模型公式

一阶段提交模型(One-Phase Commit Model)

只要事务参与者中恰好有一台机器crash,那么所有的参与者都只能接受一次“提交”消息,且其余参与者必须全部退避。举例来说,银行在进行转账操作时,若一方发生崩溃,导致转账失败,则整个过程中的资金都不会受损失。

二阶段提交模型(Two-Phase Commit Model)

二阶段提交模型在一阶段提交模型的基础上,加入了一个协调者角色。协调者的作用是接收各参与者的提交/中止请求,并最终决定是否要让分布式事务成功或失败。

4.具体代码实例和详细解释说明

Spring事务配置

首先引入Spring事务相关的jar包。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>${spring.version}</version>
</dependency>

定义事务管理器。

@Bean
public PlatformTransactionManager transactionManager() {
    JpaTransactionManager tm = new JpaTransactionManager();
    tm.setEntityManagerFactory(entityManagerFactory());
    return tm;
}

配置JpaTransactionManager。

<jpa:properties>
  ...
   <property name="hibernate.dialect" value="${hibernate.dialect}"/>
  ...
</jpa:properties>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  <!--设置实体管理器工厂-->
  <property name="entityManagerFactory" ref="entityManagerFactory"/>
  <!--设置默认的事务传播行为-->
  <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
  <!--设置默认的隔离级别-->
  <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
  <!--设置数据库连接池-->
  <property name="dataSource" ref="dataSource"/>
</bean>

配置事务传播行为。

<!-- 默认的事务传播行为是PROPAGATION_REQUIRED -->
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
<!-- 支持当前事务,如果当前没有事务,则开启新的事务 -->
<property name="propagationBehaviorName" value="PROPAGATION_SUPPORTS"/>
<!-- 如果当前存在事务,则加入该事务,如果不存在事务,则创建一个新的事务 -->
<property name="propagationBehaviorName" value="PROPAGATION_MANDATORY"/>
<!-- 如果有事务,则支持当前事务,如果没有事务,则以非事务的方式继续运行 -->
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRES_NEW"/>
<!-- 以非事务的方式运行,忽略当前事务上下文 -->
<property name="propagationBehaviorName" value="PROPAGATION_NOT_SUPPORTED"/>
<!-- 以非事务的方式运行,抛出异常 -->
<property name="propagationBehaviorName" value="PROPAGATION_NEVER"/>
<!-- 如果外围存在事务,则暂停内层事务,否则和REQUIRED相同 -->
<property name="propagationBehaviorName" value="PROPAGATION_NESTED"/>

配置事务隔离级别。

<!-- 默认的隔离级别是ISOLATION_DEFAULT -->
<property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
<!-- 使用最低的隔离级别,通常为READ_COMMITTED或SERIALIZABLE -->
<property name="isolationLevelName" value="ISOLATION_SERIALIZABLE"/>
<!-- 使用Read committed isolation level -->
<property name="isolationLevelName" value="ISOLATION_READ_COMMITTED"/>
<!-- 使用REPEATABLE_READ isolation level -->
<property name="isolationLevelName" value="ISOLATION_REPEATABLE_READ"/>
<!-- 使用可重复读隔离级别 -->
<property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>

SQL脚本示例

假设订单插入SQL如下:

INSERT INTO orders (order_id, order_date) VALUES (:orderId, :orderDate);

Spring事务注解申明如下:

@Transactional
public void insertOrder(String orderId, Date orderDate) {
    // 根据订单ID查询订单是否存在
    if (!orderDao.existsById(orderId)) {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderId(orderId);
        orderEntity.setOrderDate(orderDate);

        orderDao.saveAndFlush(orderEntity);
    } else {
        throw new IllegalArgumentException("订单ID已经存在!");
    }
}

此时,如果两个线程同时调用insertOrder方法,第一个线程查询到数据不存在,插入一条订单并提交事务,第二个线程也查询到数据不存在,插入一条订单,由于数据库的唯一索引约束,第二条语句报错:Duplicate entry 'xxx' for key 'orders_pkey'.

为了避免这种情况的发生,我们可以在事务管理器(JpaTransactionManager)中配置JpaDialect。

// 配置JpaDialect
jpaDialect = new MySQL5InnoDBDialect();
this.hibernateProperties().put(Environment.DIALECT, jpaDialect);

这样,事务的传播行为和隔离级别可以由JpaDialect指定,具体请参考Hibernate文档。

5.未来发展趋势与挑战

大规模集群环境下的分布式事务问题

随着互联网和云计算的普及,企业应用的规模越来越大,单机应用的限度越来越小。这种情况下,如何有效地处理海量的事务需求,使应用能承载更高的并发和吞吐量成为重点问题。为此,分布式事务算法应运而生。

目前,业界有两种分布式事务算法:Google Percolator事务和Facebook OceanBase TangentTM。

Percolator是谷歌公司推出的分布式事务框架。其算法基于两阶段提交协议。该算法将提交过程拆分成两个阶段,第一阶段协调器询问参与者是否可以提交,第二阶段协调器发送通知提交。其中,协调器(也叫事务管理器)在第一阶段询问每个参与者是否准备好提交,每个参与者在第二阶段提交自己的数据。一旦其中任何一个参与者失败,协调器将发送通知回滚整个事务。

TangentTM是Facebook推出的分布式事务框架。该框架用于处理与MySQL数据库的分布式事务。TangentTM利用两阶段提交协议和两阶段 locking协议。TangentTM认为,一个事务需要经历以下三个阶段:准备阶段、提交阶段、完成阶段。

  1. 准备阶段:协调者发送事务begin消息,然后发送事务准备消息,参与者检查本地事务日志,确认是否满足事务要求(例如不会读取到中间状态的数据)。
  2. 提交阶段:协调者接收到所有参与者准备好提交消息,然后发送提交事务消息,参与者将自己的事务日志标记为已提交。
  3. 完成阶段:协调者接收到所有参与者提交成功消息,然后发送事务完成消息。参与者将事务标识为已完成。

在实践中,TangentTM采用两阶段 locking协议。两阶段 locking协议是一种基于锁的事务处理机制。在准备阶段,事务仅占有必要的资源锁(例如要插入的行,要修改的行等)。如果事务遇到冲突,则暂停,直到事务释放锁为止。在提交阶段,事务释放所有资源锁,这样其它事务就可以访问被锁住的数据。

与传统的两阶段提交协议相比,两阶段 locking协议减少了资源锁的开销,提升了事务的处理速度。但是,两阶段 locking协议并不是无可替代的。如果出现死锁、锁等待时间太长、资源饥饿等问题,仍然会影响事务的处理。另外,TangetTM存在单点故障的问题,在某些场景下,需要冗余集群以避免单点故障。

更多算法

除了传统的两阶段提交协议外,还有基于三阶段提交协议、基于消息队列的最终一致性算法、基于快照隔离技术、基于逻辑时钟技术的两阶段提交算法、基于Paxos算法的分布式一致性算法、以及多种协调器的策略优化算法等。

6.附录常见问题与解答

为何分布式事务需要提供自动恢复机制?

分布式事务虽然提供了强一致性,但仍然存在单点故障的问题。因此,为了避免单点故障,分布式事务一定需要自动恢复机制。对于传统的两阶段提交协议,如果协调者宕机,所有参与者只能等超时才知道。如果有机器宕机,则可以依靠选举算法进行重新选举。不过,像两阶段锁协议这种无锁的方法,由于无法进行锁释放,所以宕机恢复的时间也会增加。

Spring事务如何实现全局传播?

Spring事务通过AOP拦截目标方法,对目标方法调用前后的增删改查操作进行拦截,生成事物日志,根据事物日志对各个数据源做相应的增删改查操作。若目标方法调用抛出异常,则根据事物日志对各个数据源做相应的回滚操作。若事物日志解析失败,则回滚整个事务。

分布式事务和微服务架构的关系?

微服务架构是分布式系统架构模式的一种变形,它将单体应用按照业务功能模块划分成独立的服务。分布式事务在微服务架构中起到的作用主要是将分布式系统中的事务统一管理。一般来说,微服务架构下,分布式事务需要采用Saga事务模型,它通过补偿机制,保证最终一致性。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到