分布式事务学习

分布式事务

参考:知乎分享:分布式事务的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,
    • 缺点:事务粒度大

一些概念

  • 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 多版本并发控制
  • CAP定理,又称布鲁尔定理,简单来说就是指在分布式系统中不可能同时满足一致性(C:Consistency)、可用性(A:Availability)、分区容错性(P: Partition Tolerance)
    • C:数据在多个副本中保持一致;
    • A:系统对外提供的服务必须一直处于可用状态;
    • P:在分布式中遇到任何网络分区故障,系统仍然能够正常对外提供服务。
    • CAP定理证明,在分布式系统中,要么满足CP、要么满足AP。不可能实现CAP或者CA,原因是网络通信并不是绝对可靠的。而在分布式系统中即便出现网络故障也需要保证系统仍然能够正常对外提供服务,Partition Tolerance 是必然存在的,所以P是一定会有的,只有C与A不能同时兼得。
    • AP :相当于放弃了强一致性,实现最终的一致性,这是很多解决分布式数据一致性的最终选择。
    • CP:放弃了高可用性,实现强一致性,之前的2PC与3PC都采用这种方案,可能会导致用户完成某个操作会等待较长时间

ByteTX分享

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,根据业务需求增加支持。
  • 自助管理平台
    • 推进平台建设,让业务接入和运维更加简单准确。
  • 事务状态订阅
    • 以事务状态变更为事件推送给希望订阅的业务,满足异步事件驱动需求。
    • 异常状态事务事件推送,以自动化处理异常状态事务。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容