分布式事务和分布式锁

1.分布式事务

关注分布式场景下处理事务,事务的参与者,支持事务的服务器,存储资源分布位于分布式系统的不同节点上。是一个业务操作,由多个细分操作完成,细分操作又分布于不同服务器上。
分布式事务伴随系统拆分出现的,主要来源于存储和服务的拆分。

  • 存储层拆分
    数据库拆分到多库多表,业务进行跨表跨库更新时要保持数据一致性,产生分布式事务问题
  • 服务层拆分:服务独立出来

2.解决方案

2.1 2PC俩阶段提交

Two-phase Commit Protocol 经典的强一致性,中心化的原子提交协议。
基于以下假设:

  • 该分布式系统有一个节点作为协调者Coordinator,其他节点作为参与者,且节点之间可以进行网络通信
  • 所有节点都采用预写式日志,日志被写入后保存在可靠的存储设备上,即使节点损坏也不会导致日志数据丢失
  • 所有节点不会永久性损坏,即使损坏后仍然可以恢复

2.1.1 协调者统一调度

保证所有节点的数据写操作,要么全部都执行,要么全部都不执行。但是一台机器执行本地事务无法知道其他机器的结果。
2pc和3pc都是引入协调者组件来统一调度所有分布式节点的执行,让当前节点知道其他节点任务执行状态,通过通知和表决的方式决定执行Commit还是Rollback

2.1.2 流程

  • 提交请求阶段:协调者通知事务参与者准备提交事务,然后进入表决过程,在表决过程中,参与者将告知协调者自己的决策:同意或取消,在第一阶段,参与节点没有commit
  • 提交协议:协调者将基于第一个阶段投票结果进行表决:提交或取消这个事务,这个结果的处理,必须当且仅当所有参与者同意提交。协调者才会通知各个参与者提交事务,否则通知取消事务,参与者在接受到协调者发来的消息后将执行相应操作。

2.1.3 问题

2.1.4 应用

关系型数据库采用2pc来提交协议完成分布式事务处理,MySQL的XA规范,MySQL Cluster内部数据同步
MySQL主从复制

  • 二进制日志是server层,主要来主从复制和即时恢复
  • 事务日志:Redo Log 是INNODB存储引擎层,保证事务安全。
    在数据库运行中,要保证binlog和redo log一致性,如果顺序不一致意味着Master-slave可能不一致
    Mysql使用2pc保证binlog和redo log一致性,内部自动将普通实物当做XA事务来处理:commit会自动分为prepare和commit阶段。binlog作为事务协调者,binlog event当做协调者日志

2.2 3PC三阶段提交

2pc之上拓展的提交协议,为了解决俩阶段提交的阻塞问题,从原来俩个阶段拓展为三个阶段,增加超时机制

2.2.1 流程

  • CanCommit:协调者向参与者发送commit请求,如果可以提交返回YES,否则No
  • PreCommit:
    协调者得到YES,进行事务预执行:发送预提交请求,协调者向参与者发送precommit请求,进入prepared阶段。事务预提交,参与者接受到precommit,执行事务操作。响应反馈,参与者成功执行事务,返回ACK,同时等待最终指令。
    协调者得到No或等待超时,就中断事务:发送中断请求,协调者向所有参与者发送abort。中断事务,参与者得到abort后执行事务中断。
  • DoCommit:
    执行提交:发送提交请求,协调者接收到ACK后,从预提交变为提交状态,向所有参与者发送doCommit请求。事务提交:参与者接收到doCommit请求后,执行正式事务提交,完成事务后释放所有事务资源。响应反馈:事务提交完后,向协调者发送ACK响应。完成事务:协调者接受到参与者的ACK响应后,完成事务。
    中断事务:协调者没有收到ACK,可能发送不是ACK,或超时,执行中断事务
    超时提交:参与者没有收到协调者通知,超时后执行Commit

2.2.2 改进

  • 引入超时机制:2pc只有协调者有超时机制,3pc都引入
  • 添加预提交阶段:2pc准备和提交阶段加入预提交,作为缓冲,保证在最后提交阶段之前各参与者状态是一致的

2.2.3 问题

参与者接收到precommit消息后,出现不能与协调者正常通信问题,参与者仍然会事务提交,可能出现数据不一致

2.3 TCC分段提交

TCC是事务处理模型,将事务过程拆分为Try,Confirm/Cancel俩个阶段,在保证强一致性的同时,最大限度提高系统的可伸缩性和可用性。
基于业务层面的事务定义,锁粒度完全由业务控制,以解决复杂业务中,跨表跨库等大颗粒资源锁定问题。
本质是吧数据库2pc提交上升到微服务来实现,从而避免2pc长事务引起的低性能风险。

2.3.1 各个阶段


try阶段失败可以cancel,confirm和cancel阶段失败怎么办:TCC 中会添加事务日志,如果 Confirm 或者 Cancel 阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等

2.3.2 优缺点

  • 优点:解决了跨服务的业务操作原子性问题,比如下订单减库存。可以让应用自己定义数据库操作的粒度,降低锁冲突,提供系统业务吞吐量。
  • 缺点:对微服务侵入性强,TCC需要对事务系统进行改造,业务逻辑的每个分支都需要实现try,confirm,cancel三个操作,并且confirm,cancel必须保证幂等。TCC事务管理器需要记录事务日志,也会损耗一定性能。

2.3.3 业务改造

  • Try:锁定某个资源,设置一个预备的状态,冻结部分数据
  • Confirm:Try锁定的资源提交,类似commit
  • Cancel:业务上回滚处理,类比rollback
    TCC感知到各个服务Try成功,执行各个服务confirm,否则cancel

2.3.4 对比2pc

  • 2pc是数据库或资源层面事务,实现强一致性,在2pc提交的过程中,一直持有数据库锁。
  • TCC关注业务层的正确提交和回滚,在Try阶段不涉及加锁,是业务层的分布式事务,关注最终一致性,不会一直持有各个业务资源的锁。

2.4 基于消息补偿的最终一致性

异步化在分布式系统设计中随处可见,基于消息队列的最终一致性就是异步事务机制,在具体实现上,基于消息补偿的一致性主要由本地消息表和第三方可靠消息队列等。

  • 本地消息表是将分布式事务拆分为本地事务进行处理,通过消息日志的方式来异步执行,本地消息表是业务耦合的设计,消息生产方需要额外建一个事务消息表,并记录消息发送状态,消息消费方需要处理这个消息,并完成自己的业务逻辑,另外有个异步机制来定期扫描未完成的消息,确保最终一致性。

2.5 不要求最终一致性的柔性事务

最大努力通知,这种方式适合可以接受不一致的业务场景

3.开源组件

  • Seata:Fescar 前身是TXC和GTS

    全局事务对分支事务的协调基于2pc,类似与数据库的XA规范,XA规范定义了组件来协调分布式事务:AP应用程序,TM事务管理器,RM资源管理器,CRM通信资源管理器。

3.1 全局事务

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

  • AP: Application应用系统(微服务)
  • TM: Transaction Manager事务管理器(全局事务管理)
  • RM: Resource Manager资源管理器(数据库)
    整个事务分成两个阶段:
  • 阶段一:表决阶段,所有参与者都将本事务执行预提交,并将能否成功的信息反馈发给协调者。
  • 阶段二:执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚。

3.2 可靠消息服务

基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。
假设有A和B两个系统,分别可以处理任务A和任务B。此时存在一个业务流程, 需要将任务A和任务B在同一个事务中处
理。就可以使用消息中间件来实现这种分布式事务。

1.事务消息发送及提交
(1)发送消息(half消息)
(2)服务端响应消息写入结果
(3)根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
(4)根据本地事务状态执行Commit或者Rollback(Commit操作生产消息索引,消息对消费者可见)
2.事务补偿
(1)对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次回查”
(2) Producer收到回查消息,检查回查消息对于的本地事务的状态
(3)根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用户解决消息Commit或者Rollback发生超时或者失效的情况
3.事务消息状态
事务消息共有三种状态,提交状态,回查状态,中间状态:

  • TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息
  • TransactionStatus.RollbackTransaction: 回滚事务,它代表消息将被删除,不允许被消费
  • TransactionStatus.Unknown:中间状态,它代表需要消息队列来确认状态

3.3 TCC事务

TCC即为Try Confifirm Cancel,它属于补偿型分布式事务。TCC实现分布式事务一共有三个步骤:

  • Try: 尝试待执行的业务
    这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
  • Confifirm: 确认执行业务
    确认执行业务操作,不做任何业务检查,只使用Try阶段预留的业务资源。 通常情况下,采用TCC
    则认为Confifirm阶段是不会出错的。即:只要Try成功, Confifrm- -定成功。若Confifirm阶段真的
    出错了,引入重试机制或人工处理。
  • Cancel: 取消待执行的业务
    取消Try阶段预留的业务资源。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。 若
    Cancel阶段真的出错了,引入重试机制或人工处理。

TCC事务的优缺点:

  • 优点:把数据库层的二阶段提交上提到了应用层来实现,规避了数据库层的2PC性能低下问题。
  • 缺点:TCC的Try、Confifirm和Cφncel操作功能需业务提供,开发成本高。

3.4 TTC异常处理

  • 空回滚
    Try方法未执行,Cancel执行了
    出现原因:
    1.Try超时
    2.分布式事务回滚,触发Cancel
    3.未收到Try,收到Cancel
    解决方案: Cancel方法需要识别出是否执行Try方法,如果执行了就正常执行Cancel,如果没有就直接结束
    增加事务日志表来实现这个功能

  • 幂等
    多次调用二阶段方法
    出现原因:
    ●网络异常
    ●分支事务所在服务器宕机
    解决方案:做幂等性处理

  • 悬挂

1.5 TTC流程


4.MySQL实现XA规范

binlog同步是XA规范的一个应用,那么XA规范是如何

4.1 一致性日志

  • redo log 重做日志:每当有操作执行前,在数据真正更正前,会先把相关操作写入redo日志,当后续任务无法完成时,待系统恢复后,可以继续完成这些更改
  • undo log 回滚日志:记录事务开始前数据的状态,当一些更改在执行一半时,发生意外无法完成,可以根据撤销日志恢复到更改之前的状态
  • binlog 二进制日志:MySQLserver层维护的一种二进制日志,是MYSQL最重要的日志之一,他记录所有DDL和DML语句,除了数据查询语句select,show等,还包含执行消耗时间。主要目的是复制和恢复,用来记录对MYSQL数据更新或潜在发生更新的SQL语句,并以实物日志的形式保存在磁盘中,binlog主要应用于MYSQL的主从复制中,MYSQL集群在Master端开启binlog,Master吧它的二进制日志传递给salves节点,再从节点回放来达到master-slave数据一致的目的

4.2 XA规范

  • 事务协调者Transaction Manager:XA事务是基于2pc提交协议,需要一个协调者,来保证所有事务参与者都完成准备工作,也就是2pc的第一阶段。如果事务协调者收到所有参与者都准备好的消息会通知所有事务都可以提交,即第二阶段。在分布式系统中,俩台机器理论上无法达到一致状态,需要引入一个单点进行协调,也就是TM,控制着全局事务,管理事务生命周期,并协调资源。
  • 资源管理器Resource Manager:负责控制和管理实际资源,比如数据库或JMS队列。目前主流数据库都提供对XA支持,在JMS规范中,Java Message Service中,也基于XA定义了对事务的支持

4.3 XA事务流程

  • Prepare阶段:TM向所有RM发送prepare指令,RM接受指令后,执行数据修改和日志记录等操作,然后返回可以提交或者不提交的消息给TM,如果事务协调者TM收到所有参与者准备好消息,所有事务提交,进入第二阶段
  • Commit阶段:TM接受所有RM的prepare结果,如果有RM返回是不可提交或者超时,那么向所有RM发送Rollback命令,如果所有RM都返回可以提交,那么向所有RM发送Commit命令,完成一次事务操作。

4.4 XA类型

事务发生在MYSql服务器单机上,还是发生多个外部节点上

  • 内部XA:Mysql同时维护binlog和innoDB的redo log,为了保证这俩个日志一致性使用XA,在MYSQL单机上工作,由binlog作为协调者。
  • 外部XA:分布式事务,主要发生数据库代理层,实现对MYSQL分布式事务支持,比如TDDL,Cobar。一般是针对跨多mysql的分布式事务,在代码中决定提交或回滚。

4.5 Binlog的Xid

binlog会添加一个XID_EVENT作为事务结束,该事件记录事务ID也就是Xid,在MYSQL进行崩溃恢复时根据binlog中提交的情况来决定如何恢复。
binlog数据类型

  • statement:基本语句,包含commit
  • row:基于行
  • mixed:日志记录使用混合模式

4.6 binlog同步过程

  • InnoDB进入Prepare阶段,并且write/sync redo log,写redo log,将事务XID写入redo日志中,binlog不做任何操作
  • 进行write/sync Binlog,写binlog日志,也会把XID写入Binlog
  • 调用InnoDB引擎的Commit完成事务的提交,将Commit信息写入redo日志中。
    第一二步失败,整个事务回滚。第三步失败,Mysql重启后检查XID是否已提交,没提交,事务重新执行,在存储引擎中再执行一次提交操作,保障redo log 和binlog数据一致性,防止数据丢失。

    在实际执行中,牵扯到OS缓存buffer何时同步到文件系统中,MYSQL支持用户自定义在Commit时如何将log buffer日志刷到log file中,通过变量innodb_flush_log_at_trx_commit的值来决定,在log buffer的内容称为脏日志

5.分布式锁

保证多线程下处理共享数据的安全性,需要保证同一时刻只有一个线程能处理共享数据。Java语言提供线程锁,开放处理锁机制的API,比如Synchronized,Lock等。当一个锁被某个线程持有时,其他线程尝试获取锁就会失败或阻塞,直到持有锁线程释放,单台服务器,通过线程加锁的方式来同步,避免并发问题。

5.1 实现

5.1.1 基于数据库

依赖数据库唯一性来实现资源锁定,比如主键和唯一索引。
创建锁表,记录方法或资源名,失效时间等字段。同时对加锁信息添加唯一索引,当要锁住某个方法时,就插入一条数据,成功则表示获取到,释放锁就在数据库删除。

  • 单点故障风险:依赖数据库高可用,一旦挂掉,业务就不可用
  • 超时无法失效:释放锁失败,就会一直无法获取,可以添加独立定时任务,对比时间戳删除超时数据。
  • 不可重入:Synchronize,Lock支持重入,实现可重入,需要改造加锁方法,额外判断存储和线程信息,不阻塞获得锁的线程再次请求加锁。
  • 对阻塞操作不友好:其他线程请求方法时,插入失败直接返回不会阻塞,如果要阻塞,只能不断insert,直到成功。

5.1.2 Redis

Redis使用setnx和expire来实现。setnx为set if not exists

  • setnx返回1,说明key不存在,该线程获得锁
  • 返回0,获得失败,进程不能进入临界区。
    业务处理完del来释放锁

Setex支持setnx和exire组合的原子操作,解决加锁失败问题。
但是在加锁和释放锁之间业务逻辑执行太长超过锁的超时限制,缓存将key删除,其他线程获得锁,出现加锁资源并发操作问题。
获取锁时设置value为随机数,在释放时进行读取和对比,确保释放的是当前线程持有的锁,一般通过redis和lua脚本方案实现。
添加完备的日志,记录上下游数据链路,当出现超时时,需要检查对应问题数据,并且人工修复。

5.1.3 Zookeeper

四种节点:

  • 持久节点
  • 持久顺序节点
  • 临时节点
  • 临时顺序节点:利用这种节点实现
    判断是否获取锁,只需要判断持有的节点是否是有序节点序号最小的一个,当释放锁的时候,将这个临时节点删除即可,这种方式可以避免服务宕机导致锁无法释放而产生死锁问题。
    算法流程:客户端连接zk,在/lock下创建临时有序子节点,第一个客户端对应的子节点为/lock/lock01/1,第二个为/lock/lock01/2。其他客户端获取/lock01下的子节点列表,判断自己创建的子节点是否为当前列表中序号最小的子节点。如果是则认为获得锁,执行业务代码,否则通过watch事件监听/lock01的子节点变更消息,获得变更通知后重复此步骤直到获得锁。完成业务流程后,删除对应子节点,释放分布式锁。

    实际开发中,应用Curator来快速实现分布式锁,对zk原生api做了抽象和封装。

5.3 分布式锁实际应用

5.3.1 特点

  • 互斥性:互斥是锁的基本特性,同一时刻只能有一个线程持有锁,执行临界操作。
  • 超时释放:超时释放锁是另一个必备特征,可以对比Mysql innoDB引擎中的innodb_lock_wait_timeout配置,通过超时释放,防止不必要线程等待和资源浪费
  • 可重入性:分布式环境下,同一个节点上的同一个线程如果获取了锁之后,再次请求还是可以成功。
  • 高性能和高可用:加锁和解锁的开销要尽可能小,同时要保证高可用,防止分布式锁失效。
  • 支持阻塞和非阻塞性:对比Java中的wait和notify操作,一般是业务代码实现,比如在获取锁时通过while或轮询来实现阻塞操作

5.3.2 集群下分布式锁问题

集群下,redis通过主从复制来实现数据同步,redis主从复制Replication是异步的,所有单节点下可用的方案在集群的环境中可能会出现问题,在故障转移Failover过程中丧失锁的安全性,假设Master节点获取到锁后在未完成数据同步的情况下,发生节点崩溃,此时其他节点仍然可以获取到锁,出现多个客户端同时获得到锁的情况。
流程执行:
A从Master节点获得锁。Master节点宕机,主从复制过程中,对应锁key还没有同步到Slave节点上,Slave升级为Master节点,于是集群丢失锁数据,其他客户端请求为新的Master节点,获取到了对应同一个资源的锁,出现多个客户端同时持有同一个资源的锁,不满足锁的互斥性。
Redlock算法集群下实现:基于N个完全独立的redis节点,一般是大于3的奇数个,假设当前集群有5个节点,运行Redlock算法的客户端依次执行下面各个步骤:

  • 客户端记录当前系统时间,以毫秒为单位
  • 依次尝试从5个redis实例中,使用相同的key获取锁。当向redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁失效时间,避免因为网络故障出现的问题
  • 客户端使用当前时间减去开始时间获取锁时间得到使用时间,当且仅当半数以上redis节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功。
  • 如果获取了锁,key真正有效时间等于有效时间减去获取锁所使用的时间,减少超时几率。
  • 如果获取锁失败,客户端应该在所以redis实例上进行解锁,防止因为服务端响应信息丢失,但是实际数据添加成功导致不一致。
posted @ 2023-10-28 20:40  lwx_R  阅读(15)  评论(0编辑  收藏  举报