分布式事务

分布式事务

参考:知乎分享:分布式事务的4种模式
4种模式(AT、TCC、Saga、XA)的分布式事务实现,均为 2PC(2 phase commit),内部划分为事务参与者和协调者

  • AT(Automatic Transaction)模式,业务无侵入,需要全局的行锁
    • 阿里系 seata、ByteTX
  • TCC 需要业务自己实现 Try Confirm Cancel 三个操作,保证 Try 成功 Confirm 一定能成功,性能高于 AT。内部分为管理者 manager 和协调者 coordinator
    • 蚂蚁金服
  • Saga,长事务解决方案,分为正向服务和补偿服务,需要业务实现,参与者之间异步执行,由事件驱动
    • 业务场景:业务流程长且需要保证事务最终一致性的业务系统
    • 正向服务和补偿服务的实现需保证:空补偿、防悬挂、幂等
  • XA 接口函数由数据库厂商提供。XA 规范的基础是两阶段提交协议2PC,很多数据库和中间件等工具本地支持,一个[全局]协调器作为 TM,各分支事务作为 RM,
    • 缺点:事务粒度大

ByteTX分享

0. 一些概念

  • ACID:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)
  • LWW:最后写入者胜出
  • 事务隔离级别:Read uncommitted 、Read committed 、Repeatable read 、Serializable。脏读又称 read uncommitted
  • 并发导致的数据库读取问题:脏读、不可重复读、幻读
  • 乐观锁悲观锁:这不是真正意义上面的锁,而是一种并发思想:乐观锁的实现方式主要有两种:CAS机制和版本号机制。悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的 select ... for update 的排它锁)。
    • 乐观锁本身不加锁,而是通过对比某个字段的值(版本)来判断是否是原值。会引入 ABA 问题(A 读取,B 写入新值,B|C 写回原值,A 写回原值。这里的A 是认为数据没有发生过变化的。有的场景不能接受此问题)。
    • 悲观锁本神需要加锁。会降低性能来保证原子性,会引入死锁问题。涉及:两阶段锁协议、死锁检测预防等。
  • MVCC:Multiversion concurrency control 多版本并发控制

1. 事务发展:单机事务->分布式事务

  • 单机事务
    • 业务的最初形态,数据全部放在同一个数据库中,利用数据库提供的能力,保证ACID。
    • 业务逻辑中,使用同一个connection先执行A业务表的变更,再执行B业务表的变更。
    • 因为数据库是单个节点,很容易保证A业务和B业务两张表的更新同时成功或者失败。
    • 提供不同的分布式事务接入模式,包括AT,SAGA,TCC等,以满足不同场景下分布式事务的需求。
  • 分布式事务
    • 随着业务不断壮大,单机数据库瓶颈逐步显现。A业务和B业务被拆开放在不同的数据库上,无法再利用单机能力完成A和B业务的事务。
    • A业务因为量级巨大,将数据库进行了分库分表。自此,A业务数据被分散到多个节点,自身多个数据的变更也无法保证事务性。
    • 经过进一步的架构演进,A和B形成了两个不同的微服务,通过RPC进行调用。这下A和B彻底分道扬镳,不但数据不存放在一起,业务服务也被分离开。
    • 以上场景下,A业务和B业务两张表需要保证事务性变更,形成了分布式事务的需求。

2. 解决方案:业务补偿、业务事务、架构事务

  • 业务补偿
    • 简单理解:A 服务内部通过事务完成改动,通知 B 服务,如果失败则重试,进行补偿,保证最终一致性。
    • B 服务保证重入幂等
    • 补偿方案适合对一致性要求较低的业务,可以忍受数据在一段时间后达到一致的场景。
  • 业务事务
    • 采用 TCC 或者 SAGA 方案
    • TCC(Try-Confirm-Cancel)事务,在第一阶段尝试预留变更需要的所有资源(prepare),根据第一阶段的结果,第二阶段对预留的资源进行操作(提交)或者释放预留的资源(回滚)。
    • SAGA事务,每个参与者都定义一个正向行为(变更)和一个反向行为(补偿),分布式事务按照既定顺序执行正向行为直到全部成功(提交),如果中间发生错误,则逆序执行对应的反向行为(回滚)。
    • TCC和SAGA的每个操作必须保证幂等性,以期应对弱网中的操作可能出现的重试和乱序现象。
    • 不论是TCC还是SAGA事务,都需要引入事务观察和协调者,一旦事务中途中断,需要介入推进事务的提交或者回滚
  • 架构事务
    • 上述业务事务实现繁琐,成本高:1. 需要保证每个操作的幂等性,在业务相对复杂的情况下,幂等性代码的编写对业务同学提出了较高的要求。2. 代码复用性低,正向和逆向的代码需要根据不同场景的逻辑独立编写。
    • ByteTX主要从基础架构角度出发,将分布式事务的影响从存储层面做切面,以中间件的形式提供使用简单、易验证的分布式事务解决方案。
    • ByteTX 支持的下游:RDS,REDIS和ABASE

3. 架构图

ByteTX架构
  • 接入层-TM(transaction manager)
    • 全局事务启动入口,负责开启一个全局事务或者继承上层调用者传递下来的全局事务。
    • 执行业务自定义的事务函数txFunc,该函数中包含全局事务需要执行的各个操作。
    • 根据txFunc的执行结果,进行全局事务的提交或者回滚。
  • 接入层-RM(resource manager)
    • 查询某个分支事务执行前,涉及的数据镜像作为undolog保存到undo日志表中。
    • 执行某个分支事务提交的SQL,并将结果返回给调用者。
    • 分支事务信息注册,包括涉及的锁信息。
  • 接入层-API
    • 查询某个全局事务的详细信息,包括涉及的分支事务,对应的undo等。
    • 强制对某个全局事务执行提交操作或者回滚操作。
  • 控制层-TC(transaction coordinator)
    • 接受注册全局事务的请求,生成全局事务id并且持久化该全局事务基础内容。
    • 接受注册分支事务的请求,并持久化该分支事务的基础内容。
    • 判断某个分支事务是否存在锁冲突,生成全局锁并持久化。
  • 控制层-SV(superviser)
    • 周期性检查超时状态的全局事务,推进全局事务提交或者回滚。
    • 周期性检查处于异常状态的全局事务,发出告警信息。
      控制层-GATE
    • 负责多租户信息的管理,指定某个业务的某个场景,由哪个集群提供服务。
  • 控制层-RMS(resouce manager server)
    • 接受提交某个分支事务的请求,删除保存在业务数据库上的undolog
    • 接受回滚某个分支事务的请求,根据业务数据库上的undolog,恢复数据到执行前的内容。
  • 数据库-TD(transaction database)
    • 全局事务表,用于保存全局事务注册信息和全局事务状态。
    • 分支事务表,用于保存分支事务注册信息和分支事务状态。
    • 全局锁表,用于保存全局锁信息。
  • 数据库-RD(route database)
    • 场景表,用于保存业务场景和ByteTX集群的映射关系,实现场景粒度的隔离。
  • 数据库-BD(business database)
    • 业务表,业务数据中库所有的业务表。
    • undolog表,用于保存该业务库上的分支事务对应的undolog信息。
拓扑结构图

4. 事务隔离性

写冲突

本地事务执行步骤:1. select ... for update 获取本地锁,超时返回失败;2. 执行 sql 语句;3. (释放本地锁之前请求 TC 获取全局锁);4. 释放本地锁;5. (执行全局事务);6. (释放全局锁)。如果一定次数内获取全局锁失败,则回滚。
*注:redis没有本地锁,使用全局锁来实现。*

提交时冲突:多个事务会等待或者重试获取全局锁和本地锁,最终预防冲突
回滚时冲突:多个事务会由于全局锁被占用,未获取到全局锁的事务失败,回滚,释放本地锁,最终获取到全局锁的事务能够得到本地锁,并执行回滚。

提交时冲突
  • 假设表tb中有一行记录,id=1, a=0。
  • 事务1和事务2同时开始,他们先声明开启一个本地事务。
  • 假设事务1拿到本地锁,这时事务2就会处于等锁状态。
  • 事务1先更新数据(a = a + 10,a值变为10,但不可见),再执行全局锁的申请,之后提交本地事务,释放本地锁(此时a的值为10,可见)。
  • 事务2发现本地锁被释放,拿到本地锁后开始事务2的操作。
  • 事务2先更新数据(a = a + 10,a值变为20,但不可见),再执行全局锁的申请,此时因为全局锁被事务1持有,这里会失败重试。
  • 事务1全局事务提交,然后释放全局锁。
  • 事务2重试后成功获取全局锁,之后提交本地事务,释放本地锁(此时a的值为20,可见)。
  • 最终事务1和事务2对应的分支事务都执行提交,此时a的值为20,符合预期。
回滚时冲突
  • 假设表tb中有一行记录,id=1, a=0。
  • 事务1和事务2同时开始,他们先声明开启一个本地事务。
  • 假设事务1拿到本地锁,这时事务2就会处于等锁状态。
  • 事务1先更新数据(a = a + 10,a值变为10,但不可见),再执行全局锁的申请,之后提交本地事务,释放本地锁(此时a的值为10,可见)。
  • 事务2发现本地锁被释放,拿到本地锁后开始事务2的操作。
  • 事务2先更新数据(a = a + 10,a值变为20,但不可见),再执行全局锁的申请,此时因为全局锁被事务1持有,这里会失败重试。
  • 事务1全局事务回滚,首先申请本地锁,这时因为因为本地锁被事务2持有,事务1会处于等锁状态。(事务1持有全局锁)
  • 事务2重试获取全局锁失败,回滚本地事务,同时释放本地锁。
  • 事务1发现本地锁被释放,拿到本地锁后继续执行回滚操作。(此时事务1同时持有本地所和全局锁)
  • 事务1先更新数据(a = 0,a值变为0,但不可见),之后提交本地事务,释放本地锁。(此时a的值为0,可见)
  • 最终事务1和事务2对应的分支事务都执行回滚,此时a的值为0,符合预期。

脏读

ByteTX 对于读请求的默认隔离级别是RU(Read Uncommitted),一个读请求拿到的数据,可能会是其他全局事务执行的中间态数据。(举例:A 服务器执行本地事务成功,通知到 B 服务器,B 服务器执行本地事务中,AB 两者构成全局事务。A 服务器得到的数据是 A 本地事务修改的值,但是全局事务 AB 并没有成功全部 commit,如果 B 失败,需要全局回滚,A 本地也会被回滚,已经读到的数据失效)

  • 原因:ByteTX每个分支事务完成,对于单个数据库来说,数据就已经提交并且可见(而不是在全局事务提交后才可见),因为全局事务如果回滚,那么在分支事务提交和回滚这段时间内,其他并发读到的数据就属于脏读数据。
  • 考量:绝大多数场景下脏读并不影响业务,就像用户看到的电商剩余库存数量不准的一样,真正需要保护的是写入的一致性。

强一致读

对于不能接受脏读的业务场景,ByteTX 提供一致性读功能(从全局事务来看,隔离级别是RC),可以通过提供的util方法修饰ctx来告知驱动,会在读请求前对数据加全局锁。

强一致读
  • 假设表tb中有一行记录,id=1, a=0。
  • 事务1和事务2同时开始,事务1要更新数据,事务2要对数据进行一致性读。
  • 假设事务1拿到本地锁,这时事务2就会处于等锁状态。
  • 事务1先更新数据(a = a + 10,a值变为10,但不可见),再执行全局锁的申请,之后提交本地事务,释放本地锁(此时a的值为10,可见)。
  • 事务2发现本地锁被释放,拿到本地锁并且读取到对应的数据a=10。
  • 事务2尝试获取全局锁,此时全局锁仍被事务1持有,事务2获取全局锁失败,然后回到上个步骤进行重试。
  • 事务1全局事务回滚,获取本地锁,将数据修改为id=1,a=0,释放本地锁,释放全局锁。
  • 事务2重试中再次获取本地锁,并且读取到对应的数据a=0。
  • 事务2获取全局锁成功,然后释放本地锁,将读到的数据返回调用方。
  • 最终事务1执行了回滚,事务2读到的数据是事务1回滚后的值,此时a的值为0,符合预期。

5. 异常处理

全局事务状态机

全局事务状态机

特殊状态

  • 正常情况下,TM驱动全局事务,使其在状态中沿着成功/失败两个线路推进,最终达到最右侧的终态。
  • 因为服务中的任何一个组件都可能出现异常,这样可能导致某个全局事务处于中间态。
  • 另外,终态中提交失败、回滚失败、超时回滚失败三个状态,我们称之为异常状态

SV 监督者

  • 当全局事务处于中间状态,或者全局事务处于异常状态时,需要有一个组件发现并进行处理。
  • SV模块就是这样一个组件,它周期性地检测最近一段时间范围内的全局事务状态,并根据不同状态驱动不同事件发生。
  • 当发现事务长期处于中间状态时,SV会推进其从当前状态按照超时线路前进。
  • 当发现事务处于异常状态时,会发出lark告警消息,需要进行人工干涉。

人工干预

  • 人工干预,主要是当事务处于异常状态时,系统无法判断如何继续驱动事务,这时就需要人工进行驱动。
  • API模块提供查询全局事务,分支事务,undo日志,业务当前数据的功能,为运维人员的干预方向提供数据支持。
  • API模块提供强制提交事务,强制回滚事务,强制结束事务的功能,作为运维人员干预的基本手段。
  • 所谓的强制指的是将全局事务状态修改为提交中,回滚中或者结束,然后驱动该事务从当前状态继续前进的功能。

6. 可用性

  • Gate(所有租户共用)
    • Gate服务,多租户管理入口,是部署于TCE的无状态服务,多实例运行保证HA,并且仅在业务启动时提供一次查询服务。
    • Gate数据库,仅保存不同租户对应的集群信息,逻辑极简,同时RDS有多个从库保证可用性。
  • API(所有租户共用)
    • api服务,仅作为人工干涉的入口,提供查询和强制驱动事务的接口,部署于TCE,多实例,无状态。
  • TC(租户隔离)
    • TC服务,ByteTX中核心的事务协调者,是部署于TCE的无状态服务,多实例运行保证HA。
    • TC数据库,保存全局事务,分支事务,全局锁等核心信息,RDS读写主库,可做分库分表,无热点。
  • RMS(租户隔离)
    • RMS服务,ByteTX中核心的资源管理者服务,是部署于TCE的无状态服务,多实例运行保证HA。
  • SV(租户隔离)
    • SV服务,ByteTX中用于周期性检测异常的服务,是部署于TCE的有状态服务,多实例运行,但只有一个实例会执行真实操作。
    • SV数据库,和TC数据库同库,保存多个SV实例的心跳信息和master信息,SV服务利用这个信息做服务协调,保证有且仅有一个实例真实执行操作。

7. 性能

数据大小

  • ByteTX服务,业务服务,DB都处于空闲状态时进行测试。
  • 全局事务耗时,包含1个分支事务,100%提交的场景。
数据大小性能对比

回滚比例

  • ByteTX服务,业务服务,DB都处于空闲状态时进行测试。
  • 全局事务耗时,包含5个分支事务,每行数据大小为800字节。
回滚比例性能对比

8. 未来发展

  • AT模式增强
    • 增加支持的SQL范围,除了常见更新一行数据的请求外,增加对更新多行数据的支持。(已支持)
    • 增加支持的存储范围,除了目前支持的RDS外,后续会增加更多存储类型的支持。(redis,abase已支持)
  • 丰富事务模式
    • TCC模式在有热点高并发场景下具备天然优势,增加TCC模式事务支持。(已支持)
    • SAGA模式在长事务的场景下更为合适,增加对SAGA模式事务的支持。
    • 其他事务模型,比如XA,根据业务需求增加支持。
  • 自助管理平台
    • 推进平台建设,让业务接入和运维更加简单准确。
  • 事务状态订阅
    • 以事务状态变更为事件推送给希望订阅的业务,满足异步事件驱动需求。
    • 异常状态事务事件推送,以自动化处理异常状态事务。

你可能感兴趣的:(分布式事务)