【深入浅出 Seata 原理及实战】「入门基础专题」探索 Seata 服务的 AT 模式下的分布式开发实战指南(2)
承接上文
上一篇文章说到了 Seata 为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。那么接下来我们将要针对于 AT 模式下进行分布式事务开发的原理进行介绍以及实战。
Seata AT 模式
在 AT、TCC、SAGA 和 XA 这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
使用 AT 模式的前提
任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:
必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用。
Seata 安装使用
下载地址
Seata 服务进行下载的地址:https://seata.io/zh-cn/blog/download.html,访问之后可以看到下面的资源中,可以直接进行下载,如下图所示。
但是由于官方维护的稍微缓慢,所以并不是最新的版本,如果你想要下载较新的版本,可以去官方的 Git 仓库中进行下载对应的版本文件包。地址为:https://github.com/seata/seata/releases,可以看到下面的最新版本已经到了1.6.1了
我们选择下载对应的可执行包即可。
创建 UNDO_LOG 表
SEATA AT 模式需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。不同数据库在创建 UNDO_LOG 表时会略有不同,以 MySQL 为例,其 UNDO_LOG 表的创表语句如下:
启动服务
下载服务器软件包后,将其解压缩。主要通过脚本进行启动 Seata 服务
Seata Server 目录中包含以下子目录:
bin:用于存放 Seata Server 可执行命令。
conf:用于存放 Seata Server 的配置文件。
lib:用于存放 Seata Server 依赖的各种 Jar 包。
logs:用于存放 Seata Server 的日志。
Seata Server 的执行脚本
seata-server.sh:主要是为 Linux 和 Mac 系统准备的启动脚本。执行
sh seata-server.sh
启动服务。seata-server.bat:主要是为 Windows 系统准备的启动脚本。执行
cmd seata-server.bat
启动服务。
其中参数的选择范围如下所示
例如执行 shell 脚本
AT 模式的工作机制
Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。
整体机制
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
AT 模式一阶段
Seata AT 模式一阶段的工作流程如下图所示
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
第一子阶段-获取 SQL 的基本信息
Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(INSERT/UPDATE/DELETE)、表名(tableXXX)、判断条件(where condition = value)等相关信息。
第二子阶段-查询并备份【执行之前】的数据快照
根据得到的业务 SQL 信息,生成“前镜像查询语句”。
执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。
第三子阶段-执行业务操作的 SQL 语句
执行业务 SQL,例如(update tableXX set parameter = 'value' where condition = value;),将这条记录的进行修改。
第四子阶段-查询业务操作之后的数据,并且保存下来
查询后镜像:根据“前镜像数据”的主键(id : X),生成“后镜像查询语句”。
执行“后镜像查询语句”,得到执行业务操作后的数据,并将其保存为“后镜像数据(afterImage)”。
第五子阶段-插入保存回滚日志记录到 undo_log 表中
将前后镜像数据和业务 SQL 的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中,示例回滚日志如下。
提交前需要获取申请本地锁
提交前,向 TC 注册分支:申请 TableXXX 表中,id 主键等于 N 的记录的全局锁 。需要确保先拿到全局锁 。
拿不到全局锁 ,不能提交本地事务。
拿到全局锁,会被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
示例说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放全局锁 。tx2 拿到全局锁提交本地事务
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。
数据库隔离级别
在数据库本地事务隔离级别,读已提交(Read Committed)或以上的基础上,Seata(AT 模式)的默认全局隔离级别是读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
本地事务提交
业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交,将本地事务提交的结果上报给 TC。
AT 模式二阶段-回滚操作
收到 TC 的分支回滚请求,开启一个本地事务。
通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
AT 模式二阶段-提交操作
收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
版权声明: 本文为 InfoQ 作者【洛神灬殇】的原创文章。
原文链接:【http://xie.infoq.cn/article/d9e53cd74e2bd3828df7f73f3】。文章转载请联系作者。
评论