分布式事务的基本概念
分布式环境的事务复杂性
当本地事务要扩展到分布式时,它的复杂性进一步增加了。
存储端的多样性。
首先就是存储端的多样性。本地事务的情况下,所有数据都会落到同一个DB中,但是,在分布式的情况下,就会出现数据可能要落到多个DB,或者还会落到Redis,落到MQ等中。
存储端多样性, 如下图所示:
事务链路的延展性
本地事务的情况下,通常所有事务相关的业务操作,会被我们封装到一个Service方法中。而在分布式的情况下,请求链路被延展,拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。在这种情况下,事务无疑变得更复杂。
事务链路延展性, 如下图所示:
基于上述两个复杂性,期望有一个统一的分布式事务方案,能够像本地事务一样,以几乎无侵入的方式,满足各种存储介质,各种复杂链路,是不现实的。
至少,在当前,还没有一个十分成熟的解决方案。所以,一般情况下,在分布式下,事务会被拆分解决,并根据不同的情况,采用不同的解决方案。
什么是分布式事务?
对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数要么一起成功,要么一起失败,必须是一个整体性的事务。
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务。对于分布式事务而言,即使不能都很好的满足,也要考虑支持到什么程度。
典型的分布式事务场景:
1. 跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过一个相对比较复杂的业务,一个业务中同时操作了9个库。
下图演示了一个服务同时操作2个库的情况:
2. 分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。
如下图,将数据库B拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。
如,对于sql:insert into user(id,name) values (1,“tianshouzhi”),(2,“wangxiaoxiao”)。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。
3. 微服务化
微服务架构是目前一个比较一个比较火的概念。例如上面笔者提到的一个案例,某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构:
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
分布式事务实现方案必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
分布式环境的事务复杂性
当本地事务要扩展到分布式时,它的复杂性进一步增加了。
存储端的多样性。
首先就是存储端的多样性。本地事务的情况下,所有数据都会落到同一个DB中,但是,在分布式的情况下,就会出现数据可能要落到多个DB,或者还会落到Redis,落到MQ等中。
存储端多样性, 如下图所示:
事务链路的延展性
本地事务的情况下,通常所有事务相关的业务操作,会被我们封装到一个Service方法中。而在分布式的情况下,请求链路被延展,拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。在这种情况下,事务无疑变得更复杂。
事务链路延展性, 如下图所示:
基于上述两个复杂性,期望有一个统一的分布式事务方案,能够像本地事务一样,以几乎无侵入的方式,满足各种存储介质,各种复杂链路,是不现实的。
至少,在当前,还没有一个十分成熟的解决方案。所以,一般情况下,在分布式下,事务会被拆分解决,并根据不同的情况,采用不同的解决方案。
什么是分布式事务?
对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数要么一起成功,要么一起失败,必须是一个整体性的事务。
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务。对于分布式事务而言,即使不能都很好的满足,也要考虑支持到什么程度。
典型的分布式事务场景:
1. 跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过一个相对比较复杂的业务,一个业务中同时操作了9个库。
下图演示了一个服务同时操作2个库的情况:
2. 分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。
如下图,将数据库B拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。
如,对于sql:insert into user(id,name) values (1,“tianshouzhi”),(2,“wangxiaoxiao”)。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。
3. 微服务化
微服务架构是目前一个比较一个比较火的概念。例如上面笔者提到的一个案例,某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构:
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
分布式事务实现方案必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
分布式环境的事务复杂性
当本地事务要扩展到分布式时,它的复杂性进一步增加了。
存储端的多样性。
首先就是存储端的多样性。本地事务的情况下,所有数据都会落到同一个DB中,但是,在分布式的情况下,就会出现数据可能要落到多个DB,或者还会落到Redis,落到MQ等中。
存储端多样性, 如下图所示:
事务链路的延展性
本地事务的情况下,通常所有事务相关的业务操作,会被我们封装到一个Service方法中。而在分布式的情况下,请求链路被延展,拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。在这种情况下,事务无疑变得更复杂。
事务链路延展性, 如下图所示:
基于上述两个复杂性,期望有一个统一的分布式事务方案,能够像本地事务一样,以几乎无侵入的方式,满足各种存储介质,各种复杂链路,是不现实的。
至少,在当前,还没有一个十分成熟的解决方案。所以,一般情况下,在分布式下,事务会被拆分解决,并根据不同的情况,采用不同的解决方案。
什么是分布式事务?
对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数要么一起成功,要么一起失败,必须是一个整体性的事务。
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务。对于分布式事务而言,即使不能都很好的满足,也要考虑支持到什么程度。
典型的分布式事务场景:
1. 跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过一个相对比较复杂的业务,一个业务中同时操作了9个库。
下图演示了一个服务同时操作2个库的情况:
2. 分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。
如下图,将数据库B拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。
如,对于sql:insert into user(id,name) values (1,“tianshouzhi”),(2,“wangxiaoxiao”)。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着一个分布式事务的问题。
3. 微服务化
微服务架构是目前一个比较一个比较火的概念。例如上面笔者提到的一个案例,某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构:
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
分布式事务实现方案必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
title: “分布式事务核心概念详解” date: 2025-01-09 11:52:00 +0800 categories:
- distributed author: deathwhispers —
一、事务基础
事务(Transaction)是数据库操作的一个执行单元,它包含了一系列操作,这些操作要么全部成功,要么全部失败。一个标准的事务应该具备四个核心特性,即 ACID。
1. 事务的 ACID 特性
- 原子性(Atomicity):事务是一个不可分割的原子操作单元。事务中的所有操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency):事务的执行不能破坏数据库的完整性和一致性。在事务开始前和结束后,数据库都必须处于一致的状态。
- 隔离性(Isolation):在并发环境中,多个事务的执行是相互隔离的,一个事务的执行不应被其他事务干扰。
- 持久性(Durability):一旦事务提交,其对数据库中数据的状态变更就应该是永久性的,即使系统崩溃也能恢复。
2. MySQL 本地事务实现原理
以 MySQL 的 InnoDB 存储引擎为例,其通过日志和锁机制来保证事务的 ACID 特性:
- 隔离性:通过数据库锁机制保障。
- 持久性:通过 Redo Log(重做日志) 保障。Redo Log 记录新数据的备份,在事务提交前持久化,系统崩溃后可用于恢复数据。
- 原子性:通过 Undo Log(撤销日志) 保障。在修改数据前,先将旧数据备份到 Undo Log,当事务回滚或出错时,可用于将数据恢复到事务开始前的状态。
- 一致性:由原子性、持久性和隔离性共同保障。
3. 并发事务带来的问题
在多个事务并发执行时,若缺乏有效的隔离机制,可能会引发以下问题:
脏读(Dirty Read):一个事务读取了另一个事务未提交的数据。如果后续那个事务回滚,那么读取到的就是无效的“脏”数据。
不可重复读(Non-Repeatable Read):在同一个事务内,两次读取同一行数据,结果却不一致。这是因为在两次读取之间,有另一个事务修改了该行数据并提交了。
幻读(Phantom Read):在同一个事务内,两次执行相同的范围查询,第二次查询的结果集包含了第一次查询中未出现的行。这是因为在两次查询之间,有另一个事务插入了新的数据。
不可重复读 vs 幻读
- 不可重复读 侧重于数据内容的变更(
UPDATE)。- 幻读 侧重于记录数量的增减(
INSERT或DELETE)。
4. SQL 事务隔离级别
为了解决上述并发问题,SQL 标准定义了四种事务隔离级别。隔离级别越高,数据一致性越好,但并发性能越差。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 (Read Uncommitted) | 可能 | 可能 | 可能 |
| 读已提交 (Read Committed) | 不可能 | 可能 | 可能 |
| 可重复读 (Repeatable Read) | 不可能 | 不可能 | 可能 |
| 串行化 (Serializable) | 不可能 | 不可能 | 不可能 |
- 读未提交:性能最好,但隔离性最差,实际应用很少。
- 读已提交:大多数数据库的默认隔离级别(如 Oracle)。避免了脏读。
- 可重复读:MySQL 的默认隔离级别。避免了脏读和不可重复读,但仍可能出现幻读。
- 串行化:最严格的隔离级别,强制事务串行执行,避免所有并发问题,但性能开销最大。
在 MySQL 的
REPEATABLE READ级别下,可以通过SELECT ... FOR UPDATE(排他锁)或SELECT ... LOCK IN SHARE MODE(共享锁)来锁定查询范围,从而避免幻读。
二、分布式事务理论
当业务从单体架构演进到分布式系统时,事务的复杂性也随之增加。
1. 分布式环境的复杂性
- 存储多样性:数据可能分布在多个数据库、缓存(如 Redis)或消息队列(MQ)中。
- 链路延展性:一个业务请求可能跨越多个微服务,通过 RPC 调用串联起来,事务链路被拉长。
2. 什么是分布式事务?
分布式事务是指事务的参与者、服务器、资源和管理器分布在不同的节点上。它要求跨多个节点的操作要么全部成功,要么全部失败,以保证数据的一致性。
典型场景:
- 跨库事务:单个服务需要操作多个数据库。
- 分库分表:数据被水平拆分到多个数据库中。
- 微服务化:一个完整的业务流程由多个独立的服务协同完成。
3. X/Open DTP 模型与 XA 规范
X/Open DTP(Distributed Transaction Process)是一个经典的分布式事务模型,它定义了三个核心角色:
- AP (Application):应用程序,负责定义事务边界。
- TM (Transaction Manager):事务管理器,负责协调和管理全局事务,是核心调度者。
- RM (Resource Manager):资源管理器,通常是数据库或消息队列等。
XA 规范则定义了 TM 和 RM 之间的通信接口,它采用两阶段提交(2PC)协议来保证事务的原子性。
- 第一阶段(准备阶段):TM 通知所有 RM 准备提交事务。RM 执行本地事务操作,但不提交,并锁定资源。
- 第二阶段(提交/回滚阶段):如果所有 RM 都准备就绪,TM 通知它们提交事务;否则,通知它们回滚。
XA 的主要限制:
- 同步阻塞:在整个过程中,资源被锁定,属于长事务,性能较差。
- 依赖支持:要求所有数据源都支持 XA 协议(例如,MySQL 的 InnoDB 引擎支持)。
4. 2PC 与 3PC
- 两阶段提交(2PC):是 XA 规范的核心,但存在同步阻塞、单点故障(TM 宕机)和数据不一致(TM 在第二阶段部分通知后宕机)等问题。
- 三阶段提交(3PC):是 2PC 的改进版,通过引入“预询价”阶段和超时机制,减少了阻塞范围并降低了数据不一致的风险,但实现更复杂。
三、分布式事务实现方案
目前主流的分布式事务解决方案可以分为几大类。
1. 刚性事务:基于 2PC/XA 的方案
这类方案追求强一致性,通常基于 XA 规范实现。
- JTA (Java Transaction API):Java 平台上的事务规范,定义了支持 XA 事务的接口。常见的实现有 JBoss 的 Narayana、Atomikos 等。
- Seata XA 模式:Seata 框架提供的 XA 模式,对 XA 协议进行了封装。
2. 柔性事务:追求最终一致性
柔性事务允许系统在一定时间内存在数据不一致,但最终会通过补偿等机制恢复一致。
a. Seata AT 模式
Seata AT 模式是增强型的 2PC,它通过以下方式优化了性能:
- 一阶段:Seata 会解析业务 SQL,生成回滚日志(
undo_log),并与业务数据在同一个本地事务中提交。这使得本地锁可以快速释放。 - 二阶段:
- 如果全局提交,Seata 会异步删除回滚日志。
- 如果全局回滚,Seata 会根据回滚日志生成补偿 SQL,实现数据回滚。
b. TX-LCN 框架
TX-LCN 是一个开源的分布式事务框架,它本身不创建事务,而是作为本地事务的协调者。它支持 LCN(2PC 补偿型)、TCC 和 TXC(类似 Seata AT)多种模式。
c. 通知型事务(异步事务)
通知型事务通过引入消息队列(MQ)将同步的事务操作解耦为异步执行,从而提升性能。核心在于保障本地事务执行与消息发送的一致性。
异步确保型事务
适用于内部系统,对数据一致性要求较高。主要有两种实现方式:
1. MQ 事务消息(半消息)
依赖 MQ 提供的半消息机制(如 RocketMQ 支持)。
- 流程:
- 发送方先向 MQ 发送一条“半消息”。
- 半消息发送成功后,执行本地事务。
- 根据本地事务的结果,向 MQ 确认提交或回滚半消息。
- 如果提交,MQ 将消息投递给消费方;如果回滚,MQ 丢弃该消息。
- MQ 提供反查机制,当长时间未收到半消息确认时,会主动向发送方查询事务状态。
2. 本地消息表
不依赖 MQ 的特殊功能,是业界常用的方案。
- 流程:
- 在业务数据库中创建一张“消息表”。
- 将业务操作和“向消息表插入一条消息”放在同一个本地事务中。
- 一个独立的任务(或线程)定时扫描消息表,将状态为“待发送”的消息投递到 MQ。
- 投递成功后,更新或删除消息表中的记录。
- 消费方处理消息,并需要保证幂等性,因为消息可能重复投递。
最大努力通知
适用于外部系统或跨企业交互(如支付回调),对数据实时性要求不高。
- 核心思想:发起方尽最大努力将业务结果通知给接收方,但不保证一定成功。
- 特点:
- 允许消息丢失。
- 提供阶梯式重试机制(如 5 分钟、10 分钟、1 小时后重试),达到次数上限后不再通知,转为人工处理。
- 接收方必须提供幂等的服务接口。
- 发起方通常会提供一个校对查询接口,供接收方在未收到通知时主动查询业务结果。
3. 方案对比与选型
| 方案类型 | 一致性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| XA/2PC | 强一致性 | 差 | 中 | 对一致性要求极高、并发量不大的内部系统 |
| Seata AT | 最终一致性 | 较好 | 低 | 微服务架构下的中低并发场景 |
| TCC | 最终一致性 | 好 | 高 | 核心业务、高并发场景 |
| MQ 事务消息 | 最终一致性 | 好 | 中 | 异步化、需要解耦的业务 |
| 本地消息表 | 最终一致性 | 好 | 中 | 不依赖特定 MQ 功能的异步业务 |
| 最大努力通知 | 最终一致性 | 极好 | 低 | 跨平台、跨企业的外部通知业务 |
在选择方案时,应综合考虑业务场景对一致性、性能和开发成本的要求,没有一种方案是万能的。 本地消息表优缺点:
优点:
- 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
- 无需提供回查方法,进一步减少的业务的侵入。
- 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
缺点:
- 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
- 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的
MQ事务消息 VS 本地消息表
二者的共性:
1、 事务消息都依赖MQ进行事务通知,所以都是异步的。2、 事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递率,实现更友好的消息投递去重。3、 事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。
二者的区别:
MQ事务消息:
- 需要MQ支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理;
- 具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能;
DB本地消息表:
- 使用了数据库来存储事务消息,降低了对MQ的要求,但是增加了存储成本;
- 事务消息使用了异步投递,增大了消息重复投递的可能性;
最大努力通知
最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方。
最大努力通知型的最终一致性:
本质是通过引入定期校验机制实现最终一致性,对业务的侵入性较低,适合于对最终一致性敏感度比较低、业务链路较短的场景。
最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景;
而异步确保型事务主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,比如订单和购物车、收货与清算、支付与结算等等场景。
普通消息是无法解决本地事务执行和消息发送的一致性问题的。因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
所以,通知型事务的难度在于: 投递消息和参与者本地事务的一致性保障。
因为核心要点一致,都是为了保证消息的一致性投递,所以,最大努力通知事务在投递流程上跟异步确保型是一样的,因此也有两个分支:
- 基于MQ自身的事务消息方案
- 基于DB的本地事务消息表方案
MQ事务消息方案
要实现最大努力通知,可以采用 MQ 的 ACK 机制。
最大努力通知事务在投递之前,跟异步确保型流程都差不多,关键在于投递后的处理。
因为异步确保型在于内部的事务处理,所以MQ和系统是直连并且无需严格的权限、安全等方面的思路设计。最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:
- 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
- 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
- 业务被动方提供幂等的服务接口,防止通知重复消费。
- 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
- 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。
- 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。
- 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
- 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
- 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。
特点
- 用到的服务模式:可查询操作、幂等操作;
- 被动方的处理结果不影响主动方的处理结果;
- 适用于对业务最终一致性的时间敏感度低的系统;
- 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;
本地消息表方案
要实现最大努力通知,可以采用 定期检查本地消息表的机制 。
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
- 消息会发到消息消费方,如果发送失败,即进行重试。
- 生产方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:
- 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
- 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
- 业务被动方提供幂等的服务接口,防止通知重复消费。
- 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
最大努力通知事务 VS 异步确保型事务
最大努力通知事务在我认知中,其实是基于异步确保型事务发展而来适用于外部对接的一种业务实现。他们主要有的是业务差别,如下:• 从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互;异步确保型事务更适用于同网络体系的内部服务交付。• 从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知;而异步确保型事务只需要消息消费者主动去消费。• 从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。
通知型事务的问题
通知型事务,是无法解决本地事务执行和消息发送的一致性问题的。
因为消息发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败、或者超时的情况。超时有可能发送成功了,有可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务,都有可能不一致性出现。
消息发送一致性
消息中间件在分布式系统中的核心作用就是异步通讯、应用解耦和并发缓冲(也叫作流量削峰)。在分布式环境下,需要通过网络进行通讯,就引入了数据传输的不确定性,也就是CAP理论中的分区容错性。
消息发送一致性是指产生消息的业务动作与消息发送动作一致,也就是说如果业务操作成功,那么由这个业务操作所产生的消息一定要发送出去,否则就丢失。
常规MQ消息处理流程和特点
常规的MQ队列处理流程无法实现消息的一致性。所以,需要借助半消息、本地消息表,保障一致性。
消息重复发送问题和业务接口幂等性设计
对于未确认的消息,采用按规则重新投递的方式进行处理。
对于以上流程,消息重复发送会导致业务处理接口出现重复调用的问题。消息消费过程中消息重复发送的主要原因就是消费者成功接收处理完消息后,消息中间件没有及时更新投递状态导致的。如果允许消息重复发送,那么消费方应该实现业务接口的幂等性设计。






















