Seata的学习—解决分布式事务方案
前提
使用seata
需要本地支持ACID事务支持的关系型数据库
JAVA应用,通过JDBC连接数据库
流程
注释1解释:
- 分布式事务中,我们会在对应需要增强的方法上增加注解@GlobalTransactionnal注解,发现这个注解之后,TM会发起全局事务处理
- TC接收到全局事务处理后会往global_table中插入一条数据,生成一个全局事务ID ,即XID,这个XID会在分支事务中传递,保证所有的分支事务属于同一个全局事务
- 之后进入一阶段提交(图中注释1部分),首先RM进程开启事务,会解析sql语句,得到需要更改的表名,sql语句的type(增,删,改,不包含查询操作),条件(where条件),以及修改的数据信息
- 之后,通过上述得到的信息生成一条查询的sql语句去查询目标表,得到前镜像,这个镜像是我们后面作为事务回滚的依据
- 随后 获取全局锁,通过全局锁和目标表数据的主键id绑定 执行sql语句(这里是考虑到了高并发的场景下,防止一个线程在修改数据时,另一条线程进来修改数据,形成脏写),注意这里不同于之前在spring中的事务处理,这里的sql语句是实实在在执行了
- 以上步骤执行后,通过主键id再次查询,得到后镜像
- 最重要的一步,将前镜像,后镜像,分支事务id,全局事务id,sql语句type等业务sql所需的信息组合成为一条json数据,插入表UNDO_LOG中
- 提交前,向 TC 注册分支:申请操作对象表中,主键值的记录的 全局锁
- 上面开启事务修改数据之后,本地事务提交,业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交
- 将本地事务提交的结果上报给 TC,至此一阶段提交完毕
注释2解释:
- TM的事务边界没有发生任何异常,TM向TC发起全局事务的提交,若TM的事务边界发生异常,TM向TC发起全局事务回滚
全局事务提交
- RM收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC(这是出于性能上的考虑,以免线程等待)
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
- 释放全局锁(无论提交或回滚都要释放全局锁)
全局事务回滚
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句(由此可见所谓的全局事务回滚就是一条恢复数据的sql语句)
- 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC
- 释放全局锁(无论提交或回滚都要释放全局锁)
搭建Seata Server服务
1. 下载Seata Server
下载地址: http://Tags.seata/seata.GitHub
2. 配置Seata Server
在seata的文件夹中找到conf文件夹,其中都是seata的配置文件
找到如上图所示两个文件: file.conf 和 registry.conf
2.1 file.conf 文件配置
将储存模式改为我们需要的模式,seata支持的有: file , db , redis,我们这里设置为db(database数据库)
之后在对应的配置处修改自己数据库的信息
2.2 registry.conf 文件配置
这个文件主要分为两个部分:
- registry 注册部分: 用于找到seata的服务地址(因为我们的seata也需要运行在nacos上,所以要把seata注册到nacos上,才能发现其他微服务)
- config 配置部分: 用于配置seata的配置信息
- registry 部分配置文件
application设置seata的服务名
serverAddr设置seata的地址(我们是在nacos上运行的seata)
group设置在nacos中的组
namespace设置命名空间
cluster集群设置
username和password是nacos的用户名和密码
- config 部分的配置文件
配置seata的内容
serverAddr : seata配置文件的地址(在nacos上)
namespace: seata配置文件所在的命名空间
group: seata配置文件所在的组
username和password: nacos 的用户名密码
dataId : seata配置文件名
2.3 seataServer.properties配置文件(目前配置文件只支持properties格式的)
- 下载地址: https://github.com/seata/seata/tree/develop/script/config-center
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.fengmi_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db
store.publicKey=
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=778329
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
properties中注意如下配置:
service.vgroupMapping.fengmi_tx_group=default // seata在不同项目中的隔离以组隔离
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true // 需要预先在数据库中创建seata的数据库
store.db.user=root
store.db.password=XXXXXX
2.4 Mysql中创建seata数据库
在上面的流程中可以发现,seata管理分布式全局事务,当TM向TC发起全局事务时,需要在一个global_table的表中创建一条记录,产生一个xid,其与我们的事务操作的主键捆绑形成一个全局事务id,这个全局事务id会在各分支事务中传播,所以这些seata所需的表是必须的,官方已提供这些表的格式,下面我们就来看看
seata中的表
- branch_table : 用于分支事务相关信息记录
- global_table : 用于全局事务相关信息记录(最重要的就是生成xid,即全局id)
- lock_table : 全局锁表,用于记录全局锁的信息
- 创建表:
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
2.5 启动Seata Server服务
- 本地seata中找到seata文件夹下的bin目录,运行seata-server.bat,可通过cmd指定-p端口和-h地址,但地址必须要与我们的微服务保持一致,否则其他微服务无法注册到seata上(这里最好不要使用127.0.0.1 直接使用局域网真实ip地址)
- 别忘了seata是在哪运行的,nacos必须先启动!!!
- 看到以上画面,seata启动成功,也可以到nacos上看看我们的seata服务是否上线(默认端口8091)
至此,seata配置启动完毕,seata服务上线
使用Seata解决分布式事务
明确几点:
- seata需要配置在我们的微服务中,其本身也是一个微服务
- 在流程中的undo_log表,用于记录前镜像,后镜像,作为回滚依据的,要在此时创建,位置在我们操作的数据库中
- 在分布式的事务中,所有的微服务都是seata的客户端,seata作为服务端,那么就要告诉我们的微服务seata的地址,这就需要在微服务启动的配置文件中去配置了
1. 创建undo_log表
- 下载地址: https://github.com/seata/seata/tree/develop/script/client/at/db
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
- 注意: 此表的位置在我们操作的数据库中,而非上面的seata数据库
从此表的结构中也可看出他是作为前镜像,后镜像的储存和回滚依据的
brand_id : 分支事务id
xid: 全局事务id
2. 在服务中依赖seata
2.1 pom依赖
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
注意seata的版本,我们这里使用的是1.4.2的版本,所以需要在依赖中更换版本,可以从maven中查看版本信息,务必要与自己使用的版本一致
2.2 配置seata信息
seata:
# 开启seata
enabled: true
# properties中设定的组名,多个项目以组名隔离
tx-service-group: fengmi_tx_group
# 开启datasource的代理,用于产生前镜像,后镜像
enable-auto-data-source-proxy: true
config:
type: nacos
nacos:
# 告知微服务如何配置seata的地址
server-addr: localhost:8848 # seata配置文件的地址
username: nacos # 因为在nacos上要告知nacos的用户名和密码
password: nacos
group: DEFAULT_GROUP # 配置所在的组
data-id: seataServer.properties # 配置文件名
namespace: pro # 配置文件所在的命名空间
registry:
type: nacos
nacos:
# 告知微服务seata的服务地址
application: seata-server # seata的服务名
namespace: pro # seata服务的命名空间
group: DEFAULT_GROUP # seata服务的组
username: nacos
password: nacos
server-addr: localhost:8848 # seata服务的地址
可以看到,seata与其他的微服务并不在一个组内,在我们之前学习nacos时,只有同组才可以调用,这里不同组为什么还能调用呢?这就必须明确上面的配置中的一项配置
registry.nacos.group: DEFAULT_GROUP 这个配置项,是在告知我们的微服务从哪里去找到seata的服务,也就是虽然不同组,但依然能通过此配置去找到我们的seata,从而注册到seata上
2.3 启动微服务
启动微服务后可以在启动日志或seata中发现我们的微服务已经注册到seata上了
2.3.1 goods微服务
- 操作的表——goods
- pom依赖
<dependencies>
<!-- 每一个微服务都是一个独立的进程,需要提供接口http访问 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 端点监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 配置中心起步依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- sentinel 起步依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- push模式依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- 链路追踪场景依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>cloud-entity</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- openfeign 远程调用依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.qf</groupId>
<artifactId>cloud-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
</dependencies>
- mapper层
public interface GoodsMapper extends BaseMapper<Goods> {
}
- service层实现类
@Service
public class GoodsService extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {
@Override
public ResultVo operationForGoodsStock(String goodsId, Integer buyNum) {
// 非空判断
if (StringUtils.isEmpty(goodsId)) {
return new ResultVo(false, "参数不合法");
}
// 根据商品id查询商品信息
Goods goods = this.baseMapper.selectById(goodsId);
// 非空判断
if (goods == null) {
return new ResultVo(false, "商品不存在!!!");
}
// 商品存在,对比库存是否匹配
if (goods.getGoodsStock() < buyNum) {
return new ResultVo(false, "商品库存不足!!!");
}
// 库存满足购买量,操作减少库存
goods.setGoodsStock(goods.getGoodsStock() - buyNum);
// 获取总价
Double totalPrice = goods.getGoodsPrice()* buyNum;
// 操作到数据库中
int i = this.baseMapper.updateById(goods);
return new ResultVo(i > 0 ? true : false, i > 0 ? "商品库存扣减成功" : "商品库存扣减失败",totalPrice);
}
}
- 响应实体类ResultVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVo<T> {
@NonNull
private boolean success;
@NonNull
private String msg;
private T data;
public ResultVo(@NonNull boolean success, @NonNull String msg) {
this.success = success;
this.msg = msg;
}
}
- controller层
@RestController
@RequestMapping("goods")
public class GoodsController {
// 模拟商品库存操作场景
@RequestMapping("operationStock/{goodsId}/{buyNum}")
public ResultVo operationStock(@PathVariable String goodsId, @PathVariable Integer buyNum) {
return goodsService.operationForGoodsStock(goodsId,buyNum);
}
}
- 由于goods微服务需要被其他服务调用,需要做openfeign的接口声明,在统一的模块中进行声明即可
@FeignClient("cloud-goods")
@RequestMapping("goods")
public interface GoodsApi {
@RequestMapping("operationStock/{goodsId}/{buyNum}")
public ResultVo operationStock(@PathVariable String goodsId, @PathVariable Integer buyNum);
}
还是那句话:
OpenFeign的接口声明记得分包!!!
2.3.2 order微服务
- pom依赖与之前的goods一样,这里就不赘叙了
- 操作表—user_order
- mapper层
public interface OrderMapper extends BaseMapper<UserOrder> {
}
- service层实现类
@Service
public class OrderService extends ServiceImpl<OrderMapper, UserOrder> implements IOrderService {
// 注入openfeign远程接口
@Autowired
private GoodsApi goodsApi;
@Override
@GlobalTransactional // seata提供全局事务注解
public ResultVo saveOrder(String goodsId, Integer buyNum) {
// 非空判断
if (StringUtils.isEmpty(goodsId)) {
return new ResultVo(false, "参数不合法");
}
// 1. 操作库存
ResultVo resultVo = goodsApi.operationStock(goodsId, buyNum);
// 2. 插入订单信息
UserOrder order = new UserOrder();
order.setCreateTime(new Date().toString());
order.setOrderBuynum(buyNum);
order.setOrderId(UUID.randomUUID().toString());
order.setOrderAddress("湖北省武汉市XXX");
order.setOrderUsername("XXX");
order.setOrderTotalprice((Double) resultVo.getData());
this.baseMapper.insert(order);
return new ResultVo(true, "订单下单成功");
}
}
此处加上的 @GlobalTransactional 正是seata提供的,用于处理分布式全局事务,这个方法即我们TM的边界
- controller层
@RestController
@RequestMapping("order")
@RefreshScope
public class OrderController {
// 模拟场景,下订单,思路:
// 1. 根据商品id去查询商品,查看库存和购买量是否匹配
// 2. 操作商品表,减去对应库存
// 3. 订单模块往数据库添加一条记录
// 上述两个操作要在一个事务中
@RequestMapping("saveOrder/{goodId}/{buyNum}")
public ResultVo saveOrder(@PathVariable String goodId, @PathVariable Integer buyNum) {
return orderService.saveOrder(goodId, buyNum);
}
}
2.3.3 启动日志
2021-10-17 11:05:34.629 INFO [cloud-goods,,,] 70600 --- [ main] com.alibaba.nacos.client.naming : new ips(1) service: DEFAULT_GROUP@@seata-server@@default -> [{"instanceId":"192.168.54.108#8091#default#DEFAULT_GROUP@@seata-server","ip":"192.168.54.108","port":8091,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"default","serviceName":"DEFAULT_GROUP@@seata-server","metadata":{},"ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000,"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple"}]
2021-10-17 11:05:34.632 INFO [cloud-goods,,,] 70600 --- [ main] com.alibaba.nacos.client.naming : current ips:(1) service: DEFAULT_GROUP@@seata-server@@default -> [{"instanceId":"192.168.54.108#8091#default#DEFAULT_GROUP@@seata-server","ip":"192.168.54.108","port":8091,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"default","serviceName":"DEFAULT_GROUP@@seata-server","metadata":{},"ipDeleteTimeout":30000,"instanceHeartBeatTimeOut":15000,"instanceHeartBeatInterval":5000,"instanceIdGenerator":"simple"}]
2021-10-17 11:05:34.636 INFO [cloud-goods,,,] 70600 --- [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 192.168.54.108:8091
2021-10-17 11:05:34.637 INFO [cloud-goods,,,] 70600 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://127.0.0.1:3306/cloud-demo
2021-10-17 11:05:34.639 INFO [cloud-goods,,,] 70600 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:192.168.54.108:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/cloud-demo', applicationId='cloud-goods', transactionServiceGroup='fengmi_tx_group'} >
2021-10-17 11:05:35.285 INFO [cloud-goods,,,] 70600 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xf9b85a1a, L:/192.168.54.108:59048 - R:/192.168.54.108:8091]
2021-10-17 11:05:35.291 INFO [cloud-goods,,,] 70600 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 48 ms, version:1.4.2,role:RMROLE,channel:[id: 0xf9b85a1a, L:/192.168.54.108:59048 - R:/192.168.54.108:8091]
截取部分微服务的启动日志,可以看出:我们的微服务找到了seata的服务地址,并且以RM的角色注册到了seata上,那么我们的分布式全局事务就可以实现了
- 也可以再seata的运行日志中看到我们的客户端上线,及其角色
2.4 测试
在orderservice中加入异常 int i = 1/0 ; 然后通过查看数据库是否变化,来测试我们的seata是否为我们解决了分布式事务,此处就不做测试展示了
Seata的写隔离
- 通过官网提供的图解,具体分析Seata的写隔离:
此图展示的是在并发的情况下,Seata是如何通过写隔离来解决脏写的问题的,流程如下:
- 对于原本的数据:id:1 , m:1000; tx1 , tx2 两个全局事务都想将值减少100
- 假设 tx1 事务先进入程序,获取到本地锁,他会先进行设置sql 语句,但是如我们最开始的流程中所说的那样,要执行修改sql的前提是要获取全局锁,这时 tx2 事务还未进入,tx1 获取到全局锁,执行了sql , 释放了本地锁
- 此时 tx2 事务进入程序,也想要修改数据,于是去获取到本地锁,设置sql语句,但此时的tx1 还未进行全局提交或回滚,全局锁依然在tx1 处, tx2 想要执行sql 语句就必须获取到全局锁,于是 tx2 进入自旋,等待获取全局锁
- 之后有两种情况,一种是TX1的事务边界内无异常,全局事务提交,另一种就是TX1的事务边界内有异常,全局事务回滚
- 上图展示的是tx1全局事务提交的情况,tx1 的TM发现事务边界内无异常,向TC发起全局事务提交,TC驱动RM进行分支事务的提交
- tx1 的RM分支在接收到TC的驱动后,进行分支事务提交,之后释放全局锁,tx2 分支自旋等待 tx1 释放了的全局锁,执行sql语句,进行全局事务提交 ,最后释放本地锁,再释放全局锁,数据改为800,两个全局事务操作都成功
接上面,
- 若 tx1 的全局事务中发生异常,那么 tx1 的TM会向TC发起全局事务回滚,TC驱动RM进行分支回滚,这时 tx1 需要进行回滚,通过branch_id (分支id) 和 xid (全局id)在 undo_log 中获取前镜像数据,生成反向补偿的sql语句, 而sql 语句的执行需要本地锁,此时的本地锁被 tx2 的进程持有,这就会形成: tx1 需要本地锁执行回滚sql语句, tx2 需要全局锁进行提交
- 而全局锁是有一个等待超时时间的,细心的小伙伴可以看到global_table 的表中有一个timeout的字段,那个字段就是设置全局锁的超时时间
- tx2 等待全局锁必定时间超时,此时 tx2 会放弃全局提交,sql语句并未执行,同时释放本地锁, tx1 重新获取本地锁, 进行全局事务的回滚,最终数据还是1000,未被改变,保证了事务的一致性 ,有效防止了脏写的问题
Seata的读隔离
- 在我们之前学习的Mysql数据库中,默认的隔离级别是 可重复度读, oracle 的默认隔离级别是 读已提交,而Seata AT模式在本地数据库的隔离级别是读已提交或以上时,全局事务默认隔离级别是 读未提交
- 看看官方给出的图解:
此图展示的是: 在高并发的情况下,解决Seata的脏读问题,目前只能通过SELECT FOR UPDATE 的sql语句来让Seata代理,将全局事务的隔离级别升到读已提交
- 由图可知 tx1 是一个写事务, tx2 是一个读事务
- tx1 进入程序,要改变数据,从1000 改到 900 ,他会先获取本地锁,之后获取全局锁,进行写操作持久化,此时, tx2 进入程序,由于我们的sql 语句后面添加了"FOR UPDATE" ,Seata会捕捉到此条sql,这条sql不会立马执行,而是由Seata进行代理执行
- tx1 事务进行了持久化操作之后,全局锁依然在 tx1 处,tx1 释放本地锁, 而Seata代理的sql要执行先获取了本地锁,查询需要全局锁,从而tx2 释放本地锁,等待全局锁
- 接下来还是分为两种情况,tx1 的接收到全局提交或全局回滚,进行相应的操作,全局提交就直接提交,之后释放全局锁,tx2 再获取全局锁,读到的就是修改之后的数据
- 图中展示了 tx1 发生异常,需要全局回滚的情况,与之前的写隔离不同,由于tx2 是一个读事务,释放了本地锁,这里 tx1 可以获取到 本地锁,进行全局事务的回滚,之后释放全局锁,数据未发生改变
- 之后 tx2 获取到全局锁,执行sql ,读取数据,是回滚后的数据,由此,避免了脏读的问题