dedecms农业种植网站模板,六安网络营销,wordpress登陆死循环,如何查看网站是否被做跳转分布式事务 Seata#xff0c;之前叫做Fescar#xff0c;是一个开源的分布式事务解决方案#xff0c;它主要致力于提供高效和简单的分布式事务服务。Seata主要用于解决微服务架构下的数据一致性问题。 Seata 的基本原理是基于两阶段提交 (2PC) 以及三阶段提交 (3PC)#xff…分布式事务 Seata之前叫做Fescar是一个开源的分布式事务解决方案它主要致力于提供高效和简单的分布式事务服务。Seata主要用于解决微服务架构下的数据一致性问题。 Seata 的基本原理是基于两阶段提交 (2PC) 以及三阶段提交 (3PC)但它对这些经典的分布式事务协议进行了扩展和优化以适应微服务场景 1.分布式事务问题
1.1.本地事务
本地事务也就是传统的单机事务。在传统数据库事务中必须要满足四个原则 1.2.分布式事务
分布式事务就是指不是在单个服务或单个数据库架构下产生的事务例如
跨数据源的分布式事务跨服务的分布式事务综合情况
在数据库水平拆分、服务垂直拆分之后一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例包括下面几个行为
创建新订单扣减商品库存从用户账户余额扣除金额
完成上面的操作需要访问三个不同的微服务和三个不同的数据库。 订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务可以保证ACID原则。
但是当我们把三件事情看做一个业务要满足保证“业务”的原子性要么所有操作全部成功要么全部失败不允许出现部分成功部分失败的现象这就是分布式系统下的事务了。
此时ACID难以满足这是分布式事务要解决的问题
1.3.演示分布式事务问题
我们通过一个案例来演示分布式事务的问题
1创建数据库 2假设有三个微服务
微服务结构如下 其中
seata-demo父工程负责管理项目依赖
account-service账户服务负责管理用户的资金账户。提供扣减余额的接口storage-service库存服务负责管理商品库存。提供扣减库存的接口order-service订单服务负责管理订单。创建订单时需要调用account-service和storage-service打开nacos确保微服务各自可以互相发现 3启动nacos、所有微服务
4测试下单功能发出Post请求
请求如下
curl --location --request POST http://localhost:8082/order?userIduser202103032042012commodityCode100202003032041count20money200业务整理lp: 对订单服务发起请求,由于订单服务里面包含其他的业务,数据库独立,订单业务又会通过feign去执行用户账户扣除和上商店库存扣除
如图 其中传递了几个参数 用户id 和扣款项,以及库存 ,但是商店库存不够,所以返回异常 但是金额200 是已经被扣除了
这个时候就出现数据执行不同步的操作,并且因为不是一个数据库 没有办法回滚
测试发现当库存不足时如果余额已经扣减并不会回滚出现了分布式事务问题。
2.理论基础
解决分布式事务问题需要一些分布式系统的基础知识作为理论指导。
2.1.CAP定理
1998年加州大学的计算机科学家 Eric Brewer 提出分布式系统有三个指标。 Consistency一致性Availability可用性Partition tolerance 分区容错性 它们的第一个字母分别是 C、A、P。
Eric Brewer 说这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
2.1.1.一致性
Consistency一致性用户访问分布式系统中的任意节点得到的数据必须一致。
比如现在包含两个节点其中的初始数据是一致的 当我们修改其中一个节点的数据时两者的数据产生了差异 要想保住一致性就必须实现node01 到 node02的数据 同步 2.1.2.可用性
Availability 可用性用户访问集群中的任意健康节点必须能得到响应而不是超时或拒绝。
如图有三个节点的集群访问任何一个都可以及时得到响应 当有部分节点因为网络故障或其它原因无法访问时代表节点不可用 2.1.3.分区容错
Partition分区因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接形成独立分区。 Tolerance容错在集群出现分区时整个系统也要持续对外提供服务
2.1.4.矛盾
在分布式系统中系统间的网络不能100%保证健康一定会有故障的时候而服务有必须对外保证服务。因此Partition Tolerance不可避免。
当节点接收到新的数据变更时就会出现问题了 如果此时要保证一致性就必须等待网络恢复完成数据同步后整个集群才对外提供服务服务处于阻塞状态不可用。
如果此时要保证可用性就不能等待网络恢复那node01、node02与node03之间就会出现数据不一致。
也就是说在P(分区情况下)一定会出现的情况下A和C之间只能实现一个。
2.2.BASE理论
BASE理论是对CAP的一种解决思路包含三个思想
Basically Available 基本可用分布式系统在出现故障时允许损失部分可用性即保证核心可用。**Soft State软状态**在一定时间内允许出现中间状态比如临时的不一致状态。Eventually Consistent最终一致性虽然无法保证强一致性但是在软状态结束后最终达到数据一致。
2.3.解决分布式事务的思路
分布式事务最大的问题是各个子事务的一致性问题因此可以借鉴CAP定理和BASE理论有两种解决思路 AP模式各子事务分别执行和提交允许出现结果不一致然后采用弥补措施恢复数据即可实现最终一致。 CP模式各个子事务执行后互相等待同时提交同时回滚达成强一致。但事务等待过程中处于弱可用状态。
但不管是哪一种模式都需要在子系统事务之间互相通讯协调事务状态也就是需要一个事务协调者(TC) 这里的子系统事务称为分支事务有关联的各个分支事务在一起称为全局事务。
3.初识Seata
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务为用户打造一站式的分布式解决方案。
官网地址http://seata.io/其中的文档、播客中提供了大量的使用说明、源码分析。 3.1.Seata的架构
Seata事务管理中有三个重要的角色 TC (Transaction Coordinator) - **事务协调者**维护全局和分支事务的状态协调全局事务提交或回滚。 TM (Transaction Manager) - **事务管理器**定义全局事务的范围、开始全局事务、提交或回滚全局事务。 RM (Resource Manager) - **资源管理器**管理分支事务处理的资源与TC交谈以注册分支事务和报告分支事务的状态并驱动分支事务提交或回滚。
整体的架构如图 Seata基于上述架构提供了四种不同的分布式事务解决方案
XA模式强一致性分阶段事务模式牺牲了一定的可用性无业务侵入TCC模式最终一致的分阶段事务模式有业务侵入AT模式最终一致的分阶段事务模式无业务侵入也是Seata的默认模式SAGA模式长事务模式有业务侵入
无论哪种方案都离不开TC也就是事务的协调者。
3.2.部署TC服务
1.首先我们要下载seata-server包地址在http/seata.io/zh-cn/blog/download.html 2.修改配置文件config-registr.config 3.修改 registry代表的是注册中心把type(注册中心类型)修改为nacos nacos默认是下面的描述,如果不是就要自定义修改,其余修改为和nacos对应组,集群,namespace…
值得注意的是,配置文件也应该交给nacos管理 ,作为分布式中的一个微服务,应该把配置文件放在服务中 改为nacos 完整·配置
registry {# tc服务的注册中心类这里选择nacos也可以是eureka、zookeeper等type nacosnacos {# seata tc 服务注册到 nacos的服务名称可以自定义application seata-tc-serverserverAddr 127.0.0.1:8848group DEFAULT_GROUP #服务之间应该在一个组namespace
# cluster SHusername nacospassword nacos}
}config {# 读取tc服务端的配置文件的方式这里是从nacos配置中心读取这样如果tc是集群可以共享配置type nacos# 配置nacos地址等信息nacos {serverAddr 127.0.0.1:8848namespace group SEATA_GROUPusername nacospassword nacosdataId seataServer.properties}
}目前的注册中心只有服务列表并且还没有上面配置文件指定的配置存放在nacos的配置 在ncos上添加配置 文件id和注册配置文件上的一至 配置内容
# 数据存储方式db代表数据库 还支持redisa 缓存保存
store.modedb
store.db.datasourcedruid
store.db.dbTypemysql
#sql8以上记得使用cj新驱动 并且设置时区characterEncodingUTF-8serverTimezoneAsia/Shanghai
store.db.driverClassNamecom.mysql.jdbc.Driver
store.db.urljdbc:mysql://127.0.0.1:3306/seata_demo?useUnicodetruerewriteBatchedStatementstrue
store.db.userroot
store.db.password111111
store.db.minConn5
store.db.maxConn30
store.db.globalTableglobal_table
store.db.branchTablebranch_table
store.db.queryLimit100
store.db.lockTablelock_table
store.db.maxWait5000
# 事务、日志等配置
server.recovery.committingRetryPeriod1000
server.recovery.asynCommittingRetryPeriod1000
server.recovery.rollbackingRetryPeriod1000
server.recovery.timeoutRetryPeriod1000
server.maxCommitRetryTimeout-1
server.maxRollbackRetryTimeout-1
server.rollbackRetryTimeoutUnlockEnablefalse
server.undo.logSaveDays7
server.undo.logDeletePeriod86400000# 客户端与服务端传输方式
transport.serializationseata
transport.compressornone
# 关闭metrics功能提高性能
metrics.enabledfalse
metrics.registryTypecompact
metrics.exporterListprometheus
metrics.exporterPrometheusPort98984.在配置文件中指定的数据库添加表 记录全局事务,分支事务,全局锁事务 SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS 0;-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS branch_table;
CREATE TABLE branch_table (branch_id bigint(20) NOT NULL,xid varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,transaction_id bigint(20) NULL DEFAULT NULL,resource_group_id varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,resource_id varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,branch_type varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,status tinyint(4) NULL DEFAULT NULL,client_id varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,application_data varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,gmt_create datetime(6) NULL DEFAULT NULL,gmt_modified datetime(6) NULL DEFAULT NULL,PRIMARY KEY (branch_id) USING BTREE,INDEX idx_xid(xid) USING BTREE
) ENGINE InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT Compact;-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS global_table;
CREATE TABLE global_table (xid varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,transaction_id bigint(20) NULL DEFAULT NULL,status tinyint(4) NOT NULL,application_id varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,transaction_service_group varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,transaction_name varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,timeout int(11) NULL DEFAULT NULL,begin_time bigint(20) NULL DEFAULT NULL,application_data varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,gmt_create datetime NULL DEFAULT NULL,gmt_modified datetime NULL DEFAULT NULL,PRIMARY KEY (xid) USING BTREE,INDEX idx_gmt_modified_status(gmt_modified, status) USING BTREE,INDEX idx_transaction_id(transaction_id) USING BTREE
) ENGINE InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT Compact;SET FOREIGN_KEY_CHECKS 1;执行查询 创建成功分支事务和全局事务 最后一步启动seata-server,找到解压文件的bin(执行目录) 点击批处理文件bat 打开游览器 访问nacos 发现其实已经启动成功了 nacos的高可用 ,如果担心一个服务挂了 可以启多个同名服务 ,这里seata服务也是一样的,这样nacos会根据健康状态选择(水平扩展) 此时seata Tc服务已经注册
3.3.微服务集成Seata
我们以order-service 订单服务进行演示(服务调用另外俩个服务开启事务)。
3.3.1.引入依赖
首先在order-service中引入依赖
!--seata--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-seata/artifactIdexclusions!--版本较低1.3.0因此排除-- exclusionartifactIdseata-spring-boot-starter/artifactIdgroupIdio.seata/groupId/exclusion/exclusions
/dependency
dependencygroupIdio.seata/groupIdartifactIdseata-spring-boot-starter/artifactId!--seata starter 采用1.4.2版本--version${seata.version}/version
/dependency3.3.2.配置TC地址
在order-service中的application.yml中配置TC服务信息通过注册中心nacos结合服务名称获取TC地址
seata:registry: # TC服务注册中心的配置微服务根据这些信息去注册中心获取tc服务地址type: nacos # 注册中心类型 nacosnacos:server-addr: 127.0.0.1:8848 # nacos地址namespace: # namespace默认为空 命名空间会进行隔离group: DEFAULT_GROUP # 分组默认是DEFAULT_GROUPapplication: seata-tc-server # seata服务名称username: nacospassword: nacostx-service-group: seata-demo # 事务组名称service:vgroup-mapping: # 事务组与cluster的映射关系 应该把集群配置到意思seata-demo: default #关联的集群名其中 Seata分布式事务解决方案中的配置文件中的事务组tx-service-group是用于标识事务组的名称。事务组用于将一组相关的业务服务划分为一个事务单元以便协调它们的分布式事务。这个名称在Seata中是一个重要的标识它用于唯一标识一个事务组。 Seata配置文件中tx-service-group的值为seata-demo这表示你的Seata实例将用于协调与名为seata-demo的事务组相关的业务服务的分布式事务。 关于配置文件中的vgroup-mapping部分这是用于映射事务组与集群Cluster之间的关系。在分布式系统中可以有多个不同的集群用于提供不同的部署和容量选项。通过vgroup-mapping你可以将事务组映射到特定的集群上。这对于在不同环境中例如开发、测试、生产使用不同的集群或配置选项非常有用。如果你没有显式地配置vgroup-mappingSeata将使用默认的集群。
在配置文件中一定要详细写清楚服务的命名空间,处于哪个服务分组,又是哪个集群里面的 在Nacos中有一些概念可以帮助你实现不同级别的服务隔离和路由。这包括Namespace、Group和Cluster。虽然它们有一些相似之处但它们在用途和功能上是不同的 Namespace命名空间命名空间是Nacos的最高层级的隔离单位。它允许你在同一Nacos集群中创建多个独立的命名空间每个命名空间都有自己的配置、服务和命名空间专属的信息。通常命名空间用于隔离不同的环境如开发、测试和生产环境。不同命名空间的服务通常是完全隔离的它们无法直接调用其他命名空间中的服务。Group分组分组是在同一个命名空间内进行服务分组的方式。你可以将同一类型的服务分为不同的组。这对于实现服务分组路由和管理可以很有用。分组通常用于将相似的服务划分为不同组以实现负载均衡和故障隔离。不过不同分组之间的服务仍然可以直接调用。Cluster集群集群是一组提供相同服务的实例。Nacos可以用于负载均衡和故障切换。通过将相同服务的实例分布在不同的集群中可以实现故障隔离。当一个集群中的服务实例不可用时请求可以路由到其他集群中的实例。 Namespace主要用于环境隔离Group用于对服务进行分组而Cluster则用于实现负载均衡和故障隔离。它们在不同的层面提供了服务隔离和路由的功能可以根据具体的需求来灵活配置。 服务分级
微服务如何根据这些配置寻找TC的地址呢
我们知道注册到Nacos中的微服务确定一个具体实例需要四个信息
namespace命名空间group分组application服务名cluster集群名
以上四个信息在刚才的yaml文件中都能找到 namespace为空就是默认的public
结合起来TC服务的信息就是publicDEFAULT_GROUPseata-tc-serverSH这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。
3.3.3.其它服务
其它两个微服务也都参考order-service的步骤来做完全一样。 启动服务查看seata控制台 显示注册成功 并且有完整的注册名
4.动手实践
下面我们就一起学习下Seata中的四种不同的事务模式。
4.1.XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理DTPDistributed Transaction Processing标准XA 规范 描述了全局的TM与局部的RM之间的接口几乎所有主流的数据库都对 XA 规范 提供了支持。
4.1.1.两阶段提交
XA是规范目前主流数据库都实现了这种规范实现的原理都是基于两阶段提交。
正常情况 异常情况 一阶段
事务协调者通知每个事物参与者执行本地事务本地事务执行完成后报告事务执行状态给事务协调者此时事务不提交继续持有数据库锁
二阶段
事务协调者基于一阶段的报告来判断下一步操作 如果一阶段都成功则通知所有事务参与者提交事务如果一阶段任意一个参与者失败则通知所有事务参与者回滚事务
是一种强一致性的标准
4.1.2.Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造以适应自己的事务模型基本架构如图 RM一阶段的工作
① 注册分支事务到TC
② 执行分支业务sql但不提交
③ 报告执行状态到TC
TC二阶段的工作 TC检测各分支事务执行状态 a.如果都成功通知所有RM提交事务 b.如果有失败通知所有RM回滚事务
RM二阶段的工作
接收TC指令检查每一个事务的状态,最后全局提交或回滚事务
4.1.3.优缺点
XA模式的优点是什么
事务的强一致性满足ACID原则。常用数据库都支持实现简单并且没有代码侵入
XA模式的缺点是什么
因为一阶段需要锁定数据库资源等待二阶段结束才释放性能较差,完好的事务需要一直等待,一直占用分布式锁依赖关系型数据库实现事务 如果是redis这种缓存库就无法实现这种模式
4.1.4.实现XA模式
Seata的starter已经完成了XA模式的自动装配实现非常简单步骤如下
1修改application.yml文件每个参与事务的微服务开启XA模式
seata:
#对于datasource 进行拦截 数据库的事务交给seata的xa模式代理data-source-proxy-mode: XA2给发起全局事务的入口方法(全局事务的入口 就类似微服务请求链中的第一个请求 a.getxx-b.creaxx-c.service 这里a服务的getxx就是该事务的全局请求 入口, 而事务就是服务链中第一个参生事务操作的就是入口) 添加GlobalTransactional注解:
本例中是OrderServiceImpl中的create方法最先开启事务. 将transactional 注解改为全局的 3重启服务并测试 依旧是库存服务只有10 但是订单需要扣除20 此时成功,数据中没有执行sql操作 ,三个服务都保障了业务一致 查看被分支业务 之前账户服务是会被扣款成功的,现在日志报回滚log
重启order-service再次测试发现无论怎样三个微服务都能成功回滚。
4.2.AT模式
AT模式同样是分阶段提交的事务模型不过缺弥补了XA模型中资源锁定周期过长的缺陷。
4.2.1.Seata的AT模型
基本流程图 阶段一RM的工作
注册分支事务记录undo-log数据快照执行业务sql并提交报告事务状态
阶段二提交时RM的工作
删除undo-log即可
阶段二回滚时RM的工作
根据undo-log恢复数据到更新前 事务执行成功 删除快照 事务执行失败 同一回复到快照 4.2.2.流程梳理
我们用一个真实的业务来梳理下AT模式的原理。
比如现在又一个数据库表记录用户余额
idmoney1100
其中一个分支业务要执行的SQL为
update tb_account set money money - 10 where id 1AT模式下当前分支事务执行流程如下
一阶段
1TM发起并注册全局事务到TC
2TM调用分支事务
3分支事务准备执行业务SQL
4RM拦截业务SQL根据where条件查询原始数据形成快照。
{id: 1, money: 100
}5RM执行业务SQL提交本地事务释放数据库锁。此时 money 90
6RM报告本地事务状态给TC
二阶段
1TM通知TC事务结束
2TC检查分支事务状态
a如果都成功则立即删除快照
b如果有分支事务失败需要回滚。读取快照数据{id: 1, money: 100}将快照恢复到数据库。此时数据库再次恢复为100
流程图 4.2.3.AT与XA的区别
简述AT模式与XA模式最大的区别是什么
XA模式一阶段不提交事务锁定资源AT模式一阶段直接提交不锁定资源。XA模式依赖数据库机制实现回滚AT模式利用数据快照实现数据回滚。XA模式强一致AT模式最终一致 中间状态是软提交
4.2.4.脏写问题
在多线程并发访问AT模式的分布式事务时有可能出现脏写问题如图 假如多个线程操作同一事务,其中一个线程先拿到数据锁执行完sql 提交事务以后释放锁,另一个线程就能拿到锁在进行一次sql执行,并且线程一刚好完at模式中有个事务失败需要回滚,这个时候数据就脏写了 解决思路就是引入了全局锁的概念。 在释放DB锁之前先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。 4.2.5.优缺点
AT模式的优点
一阶段完成直接提交事务,释放数据库资源性能比较好利用全局锁实现读写隔离没有代码侵入框架自动完成回滚和提交
AT模式的缺点
两阶段之间属于软状态属于最终一致框架的快照功能会影响性能但比XA模式要好很多 XA事务的强一致性是通过事务等待来实现的,分支事务等待所有事务完成才能进行提交,提交后才能释放锁sql锁,对性能一致处于消耗,AT模式是直接先进性提交 然后释放sql锁,最后靠全局锁来实现防止脏数据,且回复靠快照,无序等待事务链中所有数据。 4.2.6.实现AT模式
AT模式中的快照生成、回滚等动作都是由框架自动完成没有任何代码侵入因此实现非常简单。
只不过AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。
1导入数据库表记录全局锁
其中lock_table导入到TC服务关联的数据库undo_log表导入到微服务关联的数据库
/*Navicat Premium Data TransferSource Server : localSource Server Type : MySQLSource Server Version : 50622Source Host : localhost:3306Source Schema : seata_demoTarget Server Type : MySQLTarget Server Version : 50622File Encoding : 65001Date: 20/06/2021 12:39:03
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS 0;-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS undo_log;
CREATE TABLE undo_log (branch_id bigint(20) NOT NULL COMMENT branch transaction id,xid varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT global transaction id,context varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT undo_log context,such as serialization,rollback_info longblob NOT NULL COMMENT rollback info,log_status int(11) NOT NULL COMMENT 0:normal status,1:defense status,log_created datetime(6) NOT NULL COMMENT create datetime,log_modified datetime(6) NOT NULL COMMENT modify datetime,UNIQUE INDEX ux_undo_log(xid, branch_id) USING BTREE
) ENGINE InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT AT transaction mode undo table ROW_FORMAT Compact;-- ----------------------------
-- Records of undo_log
-- ------------------------------ ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS lock_table;
CREATE TABLE lock_table (row_key varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,xid varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,transaction_id bigint(20) NULL DEFAULT NULL,branch_id bigint(20) NOT NULL,resource_id varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,table_name varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,pk varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,gmt_create datetime NULL DEFAULT NULL,gmt_modified datetime NULL DEFAULT NULL,PRIMARY KEY (row_key) USING BTREE,INDEX idx_branch_id(branch_id) USING BTREE
) ENGINE InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT Compact;SET FOREIGN_KEY_CHECKS 1;注意实际开发,seata的配置数据和实际开发的业务数据肯定不是在一个库,这里为了演示放在一个库 2修改application.yml文件将事务模式修改为AT模式即可
seata:data-source-proxy-mode: AT # 默认就是AT3重启服务并测试
此时创建了一个订单数据后,库存剩余8,monney剩下700 将测试参数数据刚好多一个
数据库无改变 库存服务应该粗库没有那么多,所以肯定无法提交事务报错
扣款金额表 先成功 然后回滚 总结: AT和X A比需要多添加俩张表,一张全局锁,一张快照表 ,其余大致相同 都是添加全局事务注解标记监控管理的事务入口,springcloud底层自动完成事务揽件监控,无序二次实现 4.3.TCC模式
TCC模式与AT模式非常相似每阶段都是独立事务不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法 Try资源的检测和预留 Confirm完成资源操作业务要求 Try 成功 Confirm 一定要能成功。 Cancel预留资源释放可以理解为try的反向操作。 整个要点看完,发现并没有使用到锁的机制,这就使得这种模式对于性能省去了性能的消耗适合对性能有要求的事务处理 4.3.1.流程分析
举例一个扣减用户余额的业务。假设账户A原来余额是100需要余额扣减30元。
阶段一 Try 检查余额是否充足如果充足则冻结金额增加30元可用余额扣除30
初识余额 余额充足可以冻结 此时总金额 冻结金额 可用金额数量依然是100不变。事务直接提交无需等待其它事务。
阶段二Confirm)假如要提交Confirm则冻结金额扣减30
确认可以提交不过之前可用金额已经扣减过了这里只要清除冻结金额就好了 此时总金额 冻结金额 可用金额 0 70 70元
阶段二(Canncel)如果要回滚Cancel则冻结金额扣减30可用余额增加30
需要回滚那么就要释放冻结金额恢复可用金额 可以发现该模式和AT XA不同没有等待也没有全局锁和快照回复,有的是数据冻结然后进行操作的,如果需要回滚就根据log反向操作,无序等待
4.3.2.Seata的TCC模型
Seata中的TCC模型依然延续之前的事务架构如图 4.3.3.优缺点
TCC模式的每个阶段是做什么的
Try资源检查和预留 (一阶段)Confirm业务执行和提交 (二阶段)Cancel预留资源的释放 (二阶段)
TCC的优点是什么
一阶段完成直接提交事务释放数据库资源性能好相比AT模型无需生成快照无需使用全局锁性能最强不依赖数据库事务而是依赖补偿操作可以用于非事务型数据库Redis mongdb
TCC的缺点是什么
有代码侵入需要人为编写try、Confirm和Cancel接口太麻烦软状态事务是最终一致,中间状态肯定是会业务数据不一致的需要考虑Confirm和Cancel的失败情况做好幂等处理
4.3.4.事务悬挂和空回滚
1空回滚
当某分支事务的try阶段阻塞时可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作这时cancel不能做回滚(没有做资源预留)就是空回滚。
如图 执行cancel操作时应当判断try是否已经执行如果尚未执行则应该空回滚。
2业务悬挂
对于已经空回滚的业务之前被阻塞的try操作恢复继续执行try就永远不可能confirm或cancel 事务一直处于中间状态这就是业务悬挂。 执行try操作时应当判断cancel是否已经执行过了如果已经执行应当阻止空回滚后的try操作避免悬挂 所以需要当这个时候的状态持久化在库中,给seata进行判断
4.3.5.实现TCC模式
解决空回滚和业务悬挂问题必须要记录当前事务状态是在try、还是cancel 1思路分析
这里我们定义一张表
CREATE TABLE account_freeze_tbl (xid varchar(128) NOT NULL,user_id varchar(255) DEFAULT NULL COMMENT 用户id,freeze_money int(11) unsigned DEFAULT 0 COMMENT 冻结金额,state int(1) DEFAULT NULL COMMENT 事务状态0:try1:confirm2:cancel,PRIMARY KEY (xid) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8 ROW_FORMATCOMPACT;
其中
xid是全局事务idfreeze_money用来记录用户冻结金额state用来记录事务状态 那此时我们的业务开怎么做呢
Try业务 记录冻结金额和事务状态到account_freeze表扣减account表可用金额 Confirm业务 根据xid删除account_freeze表的冻结记录 Cancel业务 修改account_freeze表冻结金额为0state为2修改account表恢复可用金额 如何判断是否空回滚 cancel业务中根据xid查询account_freeze如果为null则说明try还没做需要空回滚 如何避免业务悬挂 try业务中根据xid查询account_freeze 如果已经存在则证明Cancel已经执行拒绝执行try业务
接下来我们改造account-service利用TCC实现余额扣减功能。
2声明TCC接口
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明
我们在account-service项目中的cn.itcast.account.service包中新建一个接口声明TCC三个接口
package cn.itcast.account.service;import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;LocalTCC
public interface AccountTCCService {/* Try逻辑TwoPhaseBusinessAction中的name属性要与当前方法名一致用于指定Try逻辑对应的方法 */TwoPhaseBusinessAction(name deduct, commitMethod confirm, rollbackMethod cancel)void deduct(BusinessActionContextParameter(paramName userId) String userId,BusinessActionContextParameter(paramName money)int money);boolean confirm(BusinessActionContext ctx);boolean cancel(BusinessActionContext ctx);
}3编写实现类
在account-service服务中的cn.itcast.account.service.impl包下新建一个类实现TCC业务
Service
Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {Autowiredprivate AccountMapper accountMapper;Autowiredprivate AccountFreezeMapper freezeMapper;OverrideTransactionalpublic void deduct(String userId, int money) {// 0.获取事务idString xid RootContext.getXID();
// 获取到事务id或对冻结表进行查询,如果该事务已经执行过回滚if (freezeMapper.selectById(xid) ! null){// 说明预留表有数据,之间做过预留操作或者回滚操作 直接拒绝在进行预留操作return; }// 1.扣减可用余额accountMapper.deduct(userId, money);// 2.记录冻结金额事务状态AccountFreeze freeze new AccountFreeze();freeze.setUserId(userId);freeze.setFreezeMoney(money);
// 记录状态freeze.setState(AccountFreeze.State.TRY);freeze.setXid(xid);freezeMapper.insert(freeze);}Overridepublic boolean confirm(BusinessActionContext ctx) {// 1.获取事务idString xid ctx.getXid();// 2.根据id删除冻结记录int count freezeMapper.deleteById(xid);return count 1;}Overridepublic boolean cancel(BusinessActionContext ctx) {// 0.查询冻结记录String xid ctx.getXid();AccountFreeze freeze freezeMapper.selectById(xid);if (freeze null) {
// 发现冻结表没有数据 说明没有执行try 进行冻结预留 进行空回滚String userId ctx.getActionContext(userId).toString();//获取用户id 之前植入的参数freeze.setXid(xid);freeze.setUserId(userId);
// 回滚中设置冻结数据为0freeze.setFreezeMoney(0);//设置状态freeze.setState(AccountFreeze.State.CANCEL);freezeMapper.insert(freeze);return true;}//判断冻结数据状态 如果冻结预留表的记录是cancel 说明之前就已经回滚过一次了if (freeze.getState() AccountFreeze.State.CANCEL){// 无序重复处理return true;}// 1.恢复可用余额accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());// 2.将冻结金额清零状态改为CANCELfreeze.setFreezeMoney(0);freeze.setState(AccountFreeze.State.CANCEL);int count freezeMapper.updateById(freeze);return count 1;}
}其中对冻结表的mapper就是基础的Mpmapper 扣款的数据maper要做对应的扣除数据和恢复
/*** author 虎哥*/
public interface AccountMapper extends BaseMapperAccount {Update(update account_tbl set money money - ${money} where user_id #{userId})int deduct(Param(userId) String userId, Param(money) int money);Update(update account_tbl set money money ${money} where user_id #{userId})int refund(Param(userId) String userId, Param(money) int money);
}设置冻结表数据的状态是映射做的枚举 修改将controller中调用的service改为实现TCC模式的接口 配置文件中的代理模式可以不用改,改模式可以和其他模式混用,只是修改了其中一个业务还原的逻辑 TCC模式不涉及配置文件的修改因为它是一种在应用程序代码级别实现的事务模式与代理服务器或数据交换的配置无关。在TCC模式下您需要编写适当的业务逻辑来实现Try、Confirm和Cancel阶段以确保分布式事务的一致性。这与配置文件中的数据代理模式没有直接关联。 重启测试 先执行正常创建订单扣库存业务 发现成功 限制测试扣除超过本地数量的数据 通过日志可以知道虽然执行了扣款,但是最后进行了反向逻辑又加了回来,代替了以前的回滚逻辑
4.4.SAGA模式
Saga 模式是 Seata 即将开源的长事务解决方案将由蚂蚁金服主要贡献。
其理论基础是Hector Kenneth 在1987年发表的论文Sagas。
Seata官网对于Saga的指南https://seata.io/zh-cn/docs/user/saga.html
4.4.1.原理
在 Saga 模式下分布式事务内有多个参与者每一个参与者都是一个冲正补偿服务需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中依次执行各参与者的正向操作如果所有正向操作均执行成功那么分布式事务提交。如果任何一个正向操作执行失败那么分布式事务会去退回去执行前面各参与者的逆向回滚操作回滚已提交的参与者使分布式事务回到初始状态。 Saga也分为两个阶段
一阶段直接提交本地事务二阶段成功则什么都不做失败则通过编写补偿业务来回滚
和tcc很想,但是TCC操作的是冻结的预留数据,这个是直接操作数据
4.4.2.优缺点
优点
事务参与者可以基于事件驱动实现异步调用吞吐高一阶段直接提交事务无锁性能好不用编写TCC中的三个阶段实现简单
缺点
软状态持续时间不确定时效性差没有锁没有事务隔离会有脏写
4.5.四种模式对比
我们从以下几个方面来对比四种实现
一致性能否保证事务的一致性强一致还是最终一致隔离性事务之间的隔离性如何代码侵入是否需要对业务代码改造性能有无性能损耗场景常见的业务场景
如图 5.高可用
Seata的TC服务作为分布式事务核心一定要保证集群的高可用性。
5.1.高可用架构模型
搭建TC服务集群非常简单启动多个TC服务注册到nacos即可。
但集群并不能确保100%安全万一集群所在机房故障怎么办所以如果要求较高一般都会做异地多机房容灾。
比如一个TC集群在上海另一个TC集群在杭州 微服务基于事务组tx-service-group)与TC集群的映射关系来查找当前应该使用哪个TC集群。当SH集群故障时只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。
5.2.实现高可用
1.模拟异地容灾的TC集群
计划启动两台seata的tc服务节点
节点名称ip地址端口号集群名称seata127.0.0.18091SHseata2127.0.0.18092HZ
之前我们已经启动了一台seata服务端口是8091集群名为SH。
现在将seata目录复制一份起名为seata2
修改seata2/conf/registry.conf内容如下
registry {# tc服务的注册中心类这里选择nacos也可以是eureka、zookeeper等type nacosnacos {# seata tc 服务注册到 nacos的服务名称可以自定义application seata-tc-serverserverAddr 127.0.0.1:8848group DEFAULT_GROUPnamespace cluster HZusername nacospassword nacos}
}config {# 读取tc服务端的配置文件的方式这里是从nacos配置中心读取这样如果tc是集群可以共享配置type nacos# 配置nacos地址等信息nacos {serverAddr 127.0.0.1:8848namespace group SEATA_GROUPusername nacospassword nacosdataId seataServer.properties}
}进入seata2/bin目录然后运行命令
seata-server.bat -p 8092打开nacos控制台查看服务列表 点进详情查看 2.将事务组映射配置到nacos
接下来我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。
新建一个配置 配置的内容如下
# 事务组映射关系
service.vgroupMapping.seata-demoSHservice.enableDegradefalse
service.disableGlobalTransactionfalse
# 与TC服务的通信配置
transport.typeTCP
transport.serverNIO
transport.heartbeattrue
transport.enableClientBatchSendRequestfalse
transport.threadFactory.bossThreadPrefixNettyBoss
transport.threadFactory.workerThreadPrefixNettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefixNettyServerBizHandler
transport.threadFactory.shareBossWorkerfalse
transport.threadFactory.clientSelectorThreadPrefixNettyClientSelector
transport.threadFactory.clientSelectorThreadSize1
transport.threadFactory.clientWorkerThreadPrefixNettyClientWorkerThread
transport.threadFactory.bossThreadSize1
transport.threadFactory.workerThreadSizedefault
transport.shutdown.wait3
# RM配置
client.rm.asyncCommitBufferLimit10000
client.rm.lock.retryInterval10
client.rm.lock.retryTimes30
client.rm.lock.retryPolicyBranchRollbackOnConflicttrue
client.rm.reportRetryCount5
client.rm.tableMetaCheckEnablefalse
client.rm.tableMetaCheckerInterval60000
client.rm.sqlParserTypedruid
client.rm.reportSuccessEnablefalse
client.rm.sagaBranchRegisterEnablefalse
# TM配置
client.tm.commitRetryCount5
client.tm.rollbackRetryCount5
client.tm.defaultGlobalTransactionTimeout60000
client.tm.degradeCheckfalse
client.tm.degradeCheckAllowTimes10
client.tm.degradeCheckPeriod2000# undo日志配置
client.undo.dataValidationtrue
client.undo.logSerializationjackson
client.undo.onlyCareUpdateColumnstrue
client.undo.logTableundo_log
client.undo.compress.enabletrue
client.undo.compress.typezip
client.undo.compress.threshold64k
client.log.exceptionRate1003.微服务读取nacos配置
接下来需要修改每一个微服务的application.yml文件让微服务读取nacos中的client.properties文件
seata:config:type: nacosnacos:server-addr: 127.0.0.1:8848username: nacospassword: nacosgroup: SEATA_GROUPdata-id: client.properties重启微服务现在微服务到底是连接tc的SH集群还是tc的HZ集群都统一由nacos的client.properties来决定了。