暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

SpringCloud实践:Seata分布式事务

一叶扁舟 2022-03-29
1078

Nacos: 注册中心,解决服务注册与发现

Ribbon: 客户端的负载均衡器,解决服务集群的负载均衡

OpenFeign:声明式的HTTP客户端,服务远程调用

Nacos:配置中心,中心化管理配置文件

Sentinel:微服务流量卫兵,以流量为入口,保护微服务,防止出现服务雪崩

Gateway: 微服务网关,服务集群的入口,路由转发以及负载均衡(结合Sentinel)

Sleuth: 链路追踪,链路快速梳理、故障定位等

Seata: 分布式事务解决方案

image.png

目录

一、概述

1.1、事务

1.1.1、事务概述

spring中事务的使用可参考:为什么你的事务不好使?一下解决Java中N种事务失效场景

我们把需要保证原子性、一致性、隔离性、持久性的一个或多个数据库操作称为事务

【A】: 原子性(转账,转和收要么失败要么成功)undo log实现

【C】: 一致性(转账,转和收数据一致)

【I】: 隔离性(两笔转账不相互影响)加锁/mvcc实现

【D】: 持久性(数据持久化)redo log实现

1.1.2、本地事务

大多情况下,我们应用都只需要操作单一的数据库,这种情况的事务称为本地事务

本地事务的ACID特性是数据库直接提供支持

image.png

1.1.3、分布式事务

【情况一】一个服务调用多个数据库,产生了多个conn对象

image.png

【情况二】多个服务更新一个数据库,产生了多个conn对象

image.png

1.2、分布式事务解决方案

常见的分布式事务解决方案如下

1、【Seata】:阿里分布式事务框架,性能高

2、【TX-LCN】:通过协调本地事务的方式,达到事务一致性,性能比Seata低

3、【MQ】:有一些第三方的MQ是支持事务消息的,如RocketMQ。但RabbitMQ和Kafka都不支持。支持事务消息的方式也是类似于两阶段提交。

1.2.1、理论:2PC(两阶段提交协议)

image.png

2PC的问题

1、【同步阻塞】参与者在等待协调者指令时,需要等待其他参与者的响应,在此过程中是被阻塞的。如果第一阶段,参与者与协调者之间网络异常导致一直收不到协调指令,那么会导致参与者一直阻塞下去。

2、【协调者宕机】如果协调者宕机,参与者将会一直阻塞,并一直占用事务资源

3、【数据不一致】如果在第二阶段,协调者发送了提交指令,有的参与者因为网络问题没收到,这样就会导致,有的参与者commit了,有的没commit

1.2.2、方案一:AT模式

基于两段式提交协议

AT模式是一种无侵入的分布式事务解决方案

在AT模式的一阶段SQL执行、二阶段提交和回滚均有Seata框架生成,用户只需要关注自己的业务SQL,便能轻松接入分布式事务。

image.png

  • 一阶段

    在一阶段,Seata会拦截"业务sql",首先解析SQL语义,找到"业务sql"要更新的业务数据,在业务数据被更新前,获取数据为before image,然后执行"业务sql"更新业务数据,在业务数据更新后,获取数据为after image,将before image 和 after image以json格式保存到undo_log表中,最后生成行锁

    以上操作全部在一个数据库事务内完成,保证了第一阶段操作的原子性。

    image.png

  • 二阶段

    • 提交

      业务边界内未发生异常

      因为"业务SQL"在一阶段已经提交到数据库,所以Seata框架只需将一阶段保存的快照数据(undo_log)和行锁删掉,完成数据清理即可

      image.png

    • 回滚

      业务边界内发生了异常

      回滚方式是使用之前的before image还原业务数据

      但是在还原之前要先校验脏写,对比"当前业务数据"和after image,如果数据一致说明没有脏写,可以还原业务数据,如果不一致说明又脏写,出现脏写需要转人工处理

      image.png

1.2.3、方案二:TCC模式

基于两段式提交协议

TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作。

事务发起方在一阶段执行Try操作,在二阶段提交执行Confirm操作,二阶段回滚执行Cancel操作

比如下单操作,在订单环节,在try中定义update订单状态为支付中,在Confirm中定义支付完成、在Cancel中定义未支付。

【一阶段】调用订单,则先执行try操作,把订单改为支付中。

【二阶段】当整个事务没问题,则执行Confirm操作,把订单改为支付完成;当事务有问题,则执行Cancel操作,把订单改为未支付;另外,如果调用Confirm操作过程中有问题导致协调者没收到响应,那会执行Cancel操作;

image.png

【缺点】

侵入性强,需要用户自己实现事务控制逻辑(Try、Confirm和Cancel三个操作)

【优点】

在整个过程中,不需要加锁,性能更强

1.2.4、方案三:MQ保证消息最终一致性

RocketMQ是一种最终一致性的分布式事务,就是说它保证的是消息最终一致性,而不是像2PC、3PC、TCC那样强一致分布式事务

类似于TCC的理念,用户根据自己的业务场景实现类似于Try、Confirm和Cancel三个操作。

image.png

RocketMQ实现分布式事务原理

二、Seat架构原理

Seata是一款开源的分布式事务解决方案

提供了AT、TCC、SAGA、XA事务模式,阿里推荐使用AT模式

2.1、Seata架构

在Seata的架构中,一共有三个角色:

  • TC(Transaction Coordinator)-事务协调器

    Server端,要单独部署,维护全局事务的运行状态,负责协调并驱动全局事务的提交和回滚。

  • TM(Transaction Manager)-事务管理器

    Client端,控制全局事务边界,负责开启一个全局事务,并最终发起全局提交活全局回滚的决议。

  • RM(Resource Manager)-资源管理器

    Client端,由业务系统集成,控制分支事务,负责分支注册、状态会报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

image.png

2.2、Seata AT模式原理

Seata AT模式原理

参看1.2.2

Seata AT模式原理(从表的角度)

image.png

【一阶段】

1、TM请求TC开启一个全局事务。TC会生成一个XID作为该全局事务的编号(存入global_table中)。XID会在微服务调用链中传播,保证多个微服务的子事务关联在一起(意思是:开启全局事务会创建一个XID,存入global_table,同一个@GlobalTransactional修饰的事务,XID相同)。

2、RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联(意思是:RM请求TC的时候,会把其存入branch_table通过XID和grobal_table进行关联)。

3、与此同时,会在lock_table表记录分支事务操作的表、操作的记录id、XID,保证全局事务与全局事务之间隔离。全局锁,做事务隔离

【二阶段】

4、TM请求TC告诉XID对应的全局事务进行提交/回滚。

5、TC通过XID查询branch_table,驱动对应的RM对本地的事务进行提交/回滚

2.3、Seata事务隔离原理

2.3.1、写隔离

【原理】

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

【示例】

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁

  • 情况一:tx1事务顺利完成

    image.png
  • 情况二:tx1事务异常

    如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

    此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

    因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

    image.png

2.3.2、读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)(结合本地事务的已提交的隔离级别,即是,不管全局事务,只要本地事务提交,就会被查到) 。

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

image.png

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

2.3.3、QA

Q: 4.怎么使用Seata框架,来保证事务的隔离性?

A: 因seata一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。

  1. 脏读 select语句加for update,代理方法增加@GlobalLock+@Transactional或@GlobalTransaction
  2. 脏写 必须使用@GlobalTransaction
    注:如果你查询的业务的接口没有GlobalTransactional 包裹,也就是这个方法上压根没有分布式事务的需求,这时你可以在方法上标注@GlobalLock+@Transactional 注解,并且在查询语句上加 for update。 如果你查询的接口在事务链路上外层有GlobalTransactional注解,那么你查询的语句只要加for update就行。设计这个注解的原因是在没有这个注解之前,需要查询分布式事务读已提交的数据,但业务本身不需要分布式事务。 若使用GlobalTransactional注解就会增加一些没用的额外的rpc开销比如begin 返回xid,提交事务等。GlobalLock简化了rpc过程,使其做到更高的性能。

三、Seata快速上手(结合nacos)

【服务端配置思路】

1、给Seata-Server配置数据库,存储global_table、branch_table、distributed_lock三个表

2、配置Seata-Server访问数据库的链接

【客户端配置思路】

1、新建表存储undo_log

2、配置Client访问服务端的方式

3、配置开启Seata事务

image.png

Seata 部署方法

seata-server下载地址

3.1、配置启动seata-server

第一步:创建Seata数据库并建表

create database seata;

创建表的sql脚本(mysql、oracle、postgresql)

第二步:配置seata-server数据源

用于配置存储server端的表的数据库连接

目录是:seata-server-1.4.2/conf/file.conf

image.png

第三步:修改seata的注册中心

用于TM调用TC时通过nacos访问

目录是:seata-server-1.4.2/conf/registry.conf

image.png

第四步:配置文件

用于配置微服务访问seata-server的一些参数(使用file的话,需要在每个项目下的resource下面配一个file.conf, 使用nacos的话,在项目配置中指定config访问nacos即可)

1、修改配置

目录是:seata-server-1.4.2/conf/registry.conf

image.png

2、在nacos里面创建seataServer.properties文件,参考配置seata参数配置进行配置

第五步:启动

可以直接启动、可以用docker、k8s等等启动,参考Seata 部署方法

3.2、业务系统集成Client

第一步:创建undo_log表

创建undo_log表的sql脚本

第二步:添加seata依赖

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <!-- 剔除spring-cloud-alibaba默认版本,引入了自己对应的版本 --> <exclusions> <exclusion> <artifactId>io.seata</artifactId> <groupId>seata-spring-boot-starter</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency>

第三步:配置

seata: enabled: true # 开启支持 tx-server-group: fengmi_tx_group # 指定事务分组(在seataServer.properties中有配) enable-auto-data-source-proxy: true # 数据源交给seata动态代理管理 # 从配置中心拿Seata的配置文件 config: type: nacos # 也可以指定别的 nacos: server-addr: 127.0.0.1:8848 group: DEFAULT_GROUP namespace: sit username: nacos password: nacos data-id: seataServer.properties # Seata的配置文件 # 告诉微服务如何访问Seata-Server registry: type: nacos # 也可以指定别的 nacos: application: seata-server server-addr: 127.0.0.1:8848 namespace: sit group: DEFAULT_GROUP username: nacos password: nacos

3.3、使用Seata事务

只需要在需要事务的方法上加上@GlobalTransactional注解即可

@GlobalTransactional(timeoutMills = 100000) // 设置全局事务超时时间是100s,默认是60s
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论