上一篇文章《微服务分布式组件—Sentinel》
事务简介
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组 SQL 语句组成。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常成为 ACID 特性
原子性(Atomicity):事务是一个不可分割的工作单位,事务中包含许多操作,要么都做,要么都不做
一致性(Consistency):事务必须使数据库从一个一致性转发太变到另一个一致性状态,事务的中间状态不能被观察到
隔离性(Isolation):一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事物之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、 串行执行(serializable,解决幻读)
持久性(Durability):持久性指一个事务一旦提交,它对数据库中数据的改变应该就是永久性的。接下来的其它操作或故障不应该对其有任何影响
本地事务
@Transactional
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)本地事务的 ACID 特性是数据库直接提供支持
让我们想象一个传统的单体应用程序。它的业务由3个模块组成。他们使用单一的本地数据源
自然,数据的一致性将由本地事务来保证
在 JDBC 编程中,我们通过 java.sql.Connection
对象来开启、关闭或者提交事务
微服务架构发生了变化。上面提到的 3 个模块被设计为基于 3 个不同数据源的 3 个服务。每个服务内的数据一致性自然由本地事务保证
Seata只是解决上述问题的一种方法
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT 模式是阿里首推的模式,阿里云上有商用版本的 GTS(Global Transaction Service 全局事务服务)
官网:https://seata.io/zh-cn/index.html
源码:https://github.com/seata/seata
官方Demo
Seata 的三大角色
在 Seata 的架构中,一共有三个角色
维护全局和分支事务的状态,驱动全局事务提交或回滚
定义全局事务的范围:开始全局事务、提交或回滚全局事务
管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交和回滚
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端
常见分布式事务解决方案
他们有一个共同点,都是两阶段(2PC)
。“两阶段” 是指完成整个分布式事务,划分成两个步骤完成
分为两个阶段:Prepare 和 Commit
Yes
响应,否则返回No
响应。当然,参与者也可能宕机,从而不返回响应Ack
响应 Ack
响应后,完成事务提交在执行 Prepare 步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无发接收到所有参与者的Yes
响应。或者某个参与者返回了No
响应,此时,协调者就会进入回退流程,对事物进行回滚
Ack
响应Ack
响应后,完成事务中断同步阴塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去
单点在2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源
如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待Prepare响应的时长等),所以也无法顺利处理上一个事务
数据不一致 Commit 事务过程中 Commit 请求/Rollback请求 可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback请求 ,而其他参与者则正常收到执行了 Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了
当参与者执行 Commit/Rollback 后会向协调者发送Ack
,然而协调者不论是否收到所有的参与者的Ack
,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个 “我不确定该事务是否成功”
环境可靠性依赖 协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了
AT 模式是一种无侵入的分布式事务解决方案
在 AT 模式下,用户只需关注自己的"业务 SQL",用户的"业务 SQL"作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作
AT 模式如何做到对业务的无侵入
在一阶段中,Seata 会拦截 “业务 SQL”,首先解析SQL语义,找到 “业务 SQL” 要更新的业务数据,在业务数据被更新之前,将其保存成before image
(undo),然后执行 “业务 SQL” 更新业务数据,在业务数据更新之后,再将其保存成 after image
(redo)最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
二阶段如果是提交的话,因为 “业务 SQL” 在一阶段已经提交至数据库,所以 Seata 框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务 SQL”,还原业务数据。回滚方式便是用 “before image” 还原业务数据;但在还原前首先要校验脏写,对比 “数据库当前业务数据” 和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方法,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法
Seata 托管分布式事务的典型生命周期:
第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务 sql 进行解析,转换成 undolog,并同时入库
参考官方文档:https://seata.io/zh-cn/docs/dev/mode/at-mode.html
第二阶段
分布式事务操作成功,则 TC 通知 RM 异步删除 undolog
分布式事务操作失败,TM 向 TC 发送回滚请求,RM 收到 协调器 TC 发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
整体执行流程
性能损耗
一条 Update 的 SQL,则需要全局事务 xid
获取(与 TC 通讯),before image
(解析SQL,查询一次数据库),after image
(查询一次数据库),insert undo log
(写一次数据库)before commit
(与 TC 通讯,判断锁冲空),这些提作都需要一次远程通讯 RPC,而目是同步的,另外undo log
写入时blob
字段的插入性能也是不高的,每条写 SQL 都会增加这么多开销,粗略估计会增加5倍响应时间
性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了 20% 的交易回滚,需要将 80% 的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
全局锁
热点数据
相比 XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在 commit 前全局锁的判定也拉长了对数据锁的占有时间,这个开销比 XA 的 prepare 低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重
回滚锁释放时间
Seata 在回滚时,需要先删除各节点的 undo log
,然后才能释放 TC 内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长
死锁问题
Seata 的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不高效,也延长了对数据库锁的占有时间
@GlobalTransactional
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM 和 RM(Client端)由业务系统集成
Seata Server (TC)环境搭建
部署指南:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
Server 端存储模式(store.mode)支持三种:
root.data
,性能较高(默认)资源目录:https://github.com/seata/seata/tree/1.3.0/script
存放 client 端 sql 脚本,参数配置
各个配置中心参数导入脚本,config.txt(包含 server 和 client,原名nacos-config.txt)为通用参数文件
server 端数据库脚本及各个容器配置
db存储模式 + Nacos(注册&配置中心)部署
步骤
https://github.com/seata/seata/releases
\seata\conf
路径下修改 file.conf
文件seata
在https://github.com/seata/seata/tree/1.3.0/script获取数据库seata_server
的表
/script/server/db/mysql.sql
模式 + Nacos(注册&配置中心)部署
配置Nacos 注册中心 负责事务参与者(微服务)和 TC 通信
/seata/conf
目录下的registry.conf
文件config.txt
文件,在文件\seata\script\config-center
git bush here
(没有的需要安装git),使用 git 启动文件nacos-config.sh
,并进行参数化配置启动,启动后会将config.txt
的配置添加到 nacos 注册中心上面(如果使用本地的Nacos,直接启动这个.sh文件就好了,不需要配置任何参数)sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 7070 -g SEATA_GROUP
参数说明
SEATA_GROUP
(如果使用 Windows跳过以下步骤)
问题一:由于启动nacos-config.sh
该文件时,输出目录报错
cat: /d/Program: No such file or directory
cat: Files/seata/script/config-center/config.txt: No such
文件名不能带有" "空格
,否则就会输以上结果
问题二:在起初配置 Nacos 时,由于设置了集群模式,出现了一直尝试连接的错误
对此,我们对文件nacos-config.sh
进行修改
这里停止
这里我们使用的时 Windows 上的 Nacos 服务,而数据库使用的是本地的 MySQL
#default_tx_group 需要与客户端保持一致
#default 需要跟客户端和registry.conf中registry中的cluster值保持一致
(客户端 properties 配置:spring.cloud.alibaba.seata.tx-service-group=default_tx_group)
事务分组:异地机房停电容错机制
default_tx_group 可以自定义
TC 的异地多机房容灾
其中,projectA所有微服务的事务分组 tx-service-group 设置为:projectA,projectA正常情况下使用 guangzhou 的 TC 集群(主)
那么正常情况下,client端的配置如下所示:
seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou
假如此时 guangzhou 集群分组整个 down 掉,或者因为网络原因 projectA 暂时无法与 Guangzhou 机房通讯,那么我们将配置中心中的 Guangzhou 集群分组改为 Shanghai,如下:
seata.service.vgroup-mapping.projectA=Shanghai
并推送到各个微服务,便完成了对整个 projectA 项目的TC集群动态切换
bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
支持的启动参数
参数 | 全写 | 作用 | 备注 |
---|---|---|---|
-h | –host | 指定在注册中心注册的IP | 不指定时获取当前的IP,外部访问部署在云环境和容器中的server建议指定 |
-p | –port | 指定server启动的端口 | 默认为8091 |
-m | –storeMode | 事务日志存储方式 | 支持 file ,db ,redis ,默认为file 注意: redis 需要 seata-server 1.3 版本及以上 |
-n | –serverNode | 用于指定seata-server节点ID | 如 1,2,3…,默认为1 |
-e | –seataEnv | 指定seata-server运行环境 | 如dev ,test 等,服务启动时会使用registry-dev.conf 这样的配置 |
接入微服务应用 声明式事务实现(@GlobalTransactional)
启动 Seata server 端,Seata server 使用 nacos 作为配置中心和注册中心
配置微服务整合 seata
用户下单,整个业务逻辑由两个微服务构成
在父工程 springcloud_alibaba 下创建一个 seata 模块
创建数据库seata_order
并创建表order
;创建数据库seata_stock
并创建表stock
USE seata_order;
CREATE TABLE `order` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`product_id` INT(11) DEFAULT NULL,
`total_amount` INT(11) DEFAULT NULL,
`status` VARCHAR(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
USE seata_stock;
CREATE TABLE `stock` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`product_id` INT(11) DEFAULT NULL,
`count` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloudartifactId>
<groupId>com.vinjcentgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>seataartifactId>
<packaging>pompackaging>
<modules>
<module>springcloud-provider-order-seata-8006module>
<module>springcloud-provider-stock-seata-8007module>
modules>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.24version>
dependency>
dependencies>
project>
<build>
<resources>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*.propertiesinclude>
<include>**/*.ymlinclude>
<include>**/*.xmlinclude>
includes>
<filtering>truefiltering>
resource>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.propertiesinclude>
<include>**/*.ymlinclude>
<include>**/*.xmlinclude>
includes>
<filtering>truefiltering>
resource>
resources>
build>
/**
* @TableName order
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private Integer productId;
private Integer totalAmount;
private String status;
}
@Mapper
@Repository
public interface OrderMapper {
int addOrder(Order order);
List<Order> getAllOrders();
int deleteOrder(@Param("id") Integer id);
int updateOrder(Order order);
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vinjcent.springcloud.mapper.OrderMapper">
<insert id="addOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
insert into seata_order.order(product_id,total_amount,status)
values (#{productId},#{totalAmount},#{status})
insert>
<update id="updateOrder" parameterType="Order">
update seata_order.order set product_id= #{productId},total_amount = #{totalAmount},status = #{status}
where id = #{id}
update>
<select id="getAllOrders" resultType="Order">
select * from seata_order.order
select>
<delete id="deleteOrder" parameterType="integer">
delete from seata_order.order where id = #{id}
delete>
mapper>
public interface OrderService {
int addOrder(Order order);
List<Order> getAllOrders();
int deleteOrder(Integer id);
int updateOrder(Order order);
}
@SuppressWarnings("all")
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private OrderMapper orderMapper;
@Transactional
@Override
public int addOrder(Order order) {
int result = orderMapper.addOrder(order);
// 扣减库存
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("productId",order.getProductId());
String msg = restTemplate.postForObject("http://localhost:8007/stock/reduct",map,String.class);
// 模拟异常
int a = 1/0;
return result;
}
@Override
public List<Order> getAllOrders() {
return orderMapper.getAllOrders();
}
@Override
public int deleteOrder(Integer id) {
return orderMapper.deleteOrder(id);
}
@Override
public int updateOrder(Order order) {
return orderMapper.updateOrder(order);
}
}
@Configuration
public class ConfigBean { // @Configuration --> 相当于 spring 的 application.xml
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
@SuppressWarnings("all")
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate; // 提供多种便捷访问远程http服务的方法,简单的Restful服务模板
@Autowired
private OrderService orderService;
@RequestMapping("/add")
public String addOrder(){
Order order = new Order();
order.setProductId(9)
.setStatus("下单成功")
.setTotalAmount(100);
// 插入订单信息
orderService.addOrder(order);
return "下单成功";
}
}
application.yml
文件# 端口号
server:
port: 8006
# mybatis配置
mybatis:
# 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名
cache-enabled: true # 开启二级缓存
type-aliases-package: com.vinjcent.springcloud.pojo
mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml
# spring配置
spring:
application:
name: springcloud-provider-order-seata
datasource: # 数据源
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
@SpringBootApplication
@EnableFeignClients
public class Order_Seata {
public static void main(String[] args) {
SpringApplication.run(Order_Seata.class, args);
}
}
pom.xml
对静态资源进行过滤<build>
<resources>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*.propertiesinclude>
<include>**/*.ymlinclude>
<include>**/*.xmlinclude>
includes>
<filtering>truefiltering>
resource>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.propertiesinclude>
<include>**/*.ymlinclude>
<include>**/*.xmlinclude>
includes>
<filtering>truefiltering>
resource>
resources>
build>
/**
* @TableName stock
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Stock implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private Integer productId;
private Integer count;
}
@Mapper
@Repository
public interface StockMapper {
int addStock(Stock stock);
List<Stock> getAllStocks();
int deleteStock(@Param("id") Integer id);
int updateStock(@Param("productId") Integer productId);
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vinjcent.springcloud.mapper.StockMapper">
<insert id="addStock" parameterType="Stock">
insert into seata_stock.stock(product_id,count)
values (#{productId},#{count})
insert>
<update id="updateStock" parameterType="Stock">
update seata_stock.stock set count = `count` - 1
where product_id = #{productId}
update>
<select id="getAllStocks" resultType="Stock">
select * from seata_stock.stock
select>
<delete id="deleteStock" parameterType="integer">
delete from seata_stock.stock where id = #{id}
delete>
mapper>
public interface StockService {
int addStock(Stock stock);
List<Stock> getAllStocks();
int deleteStock(Integer id);
int updateStock(Integer productId);
}
@SuppressWarnings("all")
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockMapper stockMapper;
@Override
public int addStock(Stock stock) {
return stockMapper.addStock(stock);
}
@Override
public List<Stock> getAllStocks() {
return stockMapper.getAllStocks();
}
@Override
public int deleteStock(Integer id) {
return stockMapper.deleteStock(id);
}
@Override
public int updateStock(Integer productId) {
return stockMapper.updateStock(productId);
}
}
@SuppressWarnings("all")
@RestController
@RequestMapping("/stock")
public class StockController {
@Autowired
private StockService stockService;
@RequestMapping("/reduct")
public String reductStock(@RequestParam(value = "productId") Integer productId){
stockService.updateStock(productId);
return "扣减库存成功!";
}
}
application.yml
配置文件# 端口号
server:
port: 8007
# mybatis配置
mybatis:
# 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名
cache-enabled: true # 开启二级缓存
type-aliases-package: com.vinjcent.springcloud.pojo
mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml
# spring配置
spring:
application:
name: springcloud-provider-stock-seata
datasource: # 数据源
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/seata_stock?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
测试
启动 springcloud-provider-order-seata-8006
启动 springcloud-provider-stock-seata-8007
访问http://localhost:8006/order/add
问题:@Transactional
本地事务无法解决分布式事务的场景
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
dependencies>
application.yml
添加 nacos 配置# 端口号
server:
port: 8006
# mybatis配置
mybatis:
# 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名
cache-enabled: true # 开启二级缓存
type-aliases-package: com.vinjcent.springcloud.pojo # 实体类别名
mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml # xxxMapper.xml 文件
# spring配置
spring:
application:
# 应用服务名
name: springcloud-provider-order-seata
datasource: # 数据源
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
cloud:
# Nacos 注册中心配置
nacos:
discovery:
# nacos服务地址
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
alibaba:
seata:
# 对应 service.vgroupMapping.guangzhou=default
tx-service-group: guangzhou # 配置事务分组,由于在config.txt中设置集群部署机房在"guangzhou",所以这里要修改
# Seata配置
seata:
enabled: true
application-id: ${spring.application.name} # 微服务应用名称
tx-service-group: guangzhou # 此处配置自定义的seata事务分组名称
enable-auto-data-source-proxy: true # 开启数据库代理
# 配置seata的注册中心,告诉seata client(参与者) 怎么去访问seata server(TC---事务协调者)
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
application: seata-server # seata服务名,默认是seata-server
username: nacos
password: nacos
group: SEATA_GROUP # seata服务分组,默认是SEATA_GROUP
# 对应registry.conf文件中的 cluster = "default"
cluster: default #默认集群名
# 配置seata的配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
# 配置微服务的事务分组
service:
vgroup-mapping:
gaungzhou: default
grouplist:
# 启动seata-server.bat 中的seata-server的端口号
default: 127.0.0.1:8091
enable-degrade: false
disable-global-transaction: false
# =========================================================================================
# 端口号
server:
port: 8007
# mybatis配置
mybatis:
# 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名
cache-enabled: true # 开启二级缓存
type-aliases-package: com.vinjcent.springcloud.pojo # 实体类别名
mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml # xxxMapper.xml 文件
# spring配置
spring:
application:
# 应用服务名
name: springcloud-provider-stock-seata
datasource: # 数据源
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/seata_stock?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
cloud:
# Nacos 注册中心配置
nacos:
discovery:
# nacos服务地址
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
alibaba:
# 配置所使用seata的事务分组
seata:
# 对应 service.vgroupMapping.guangzhou=default
tx-service-group: guangzhou # 配置事务分组,由于在config.txt中设置集群部署机房在"guangzhou",所以这里要修改
# Seata配置
seata:
enabled: true
application-id: ${spring.application.name} # 微服务应用名称
tx-service-group: guangzhou # 此处配置自定义的seata事务分组名称
enable-auto-data-source-proxy: true # 开启数据库代理
# 配置seata的注册中心,告诉seata client(参与者) 怎么去访问seata server(TC---事务协调者)
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
application: seata-server # seata服务名,默认是seata-server
username: nacos
password: nacos
group: SEATA_GROUP # seata服务分组,默认是SEATA_GROUP
# 对应registry.conf文件中的 cluster = "default"
cluster: default #默认集群名
# 配置seata的配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
# 配置微服务的事务分组
service:
vgroup-mapping:
gaungzhou: default
grouplist:
# 启动seata-server.bat 中的seata-server的端口号
default: 127.0.0.1:8091
enable-degrade: false
disable-global-transaction: false
参数配置信息:https://seata.io/zh-cn/docs/user/configurations.html
api
接口 StockService.class@SuppressWarnings("all")
@FeignClient(value = "springcloud-provider-stock-seata",path = "/stock")
public interface StockService {
@RequestMapping("/reduct")
public String reductStock(@RequestParam(value = "productId") Integer productId);
}
OrderServiceImpl.class
类@SuppressWarnings("all")
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private StockService stockService;
@Autowired
private OrderMapper orderMapper;
@GlobalTransactional
@Override
@GlobalLock
public int addOrder(Order order) {
int result = orderMapper.addOrder(order);
// 扣减库存
stockService.reductStock(order.getProductId());
// 模拟异常
int a = 1/0;
return result;
}
@Override
public List<Order> getAllOrders() {
return orderMapper.getAllOrders();
}
@Override
public int deleteOrder(Integer id) {
return orderMapper.deleteOrder(id);
}
@Override
public int updateOrder(Order order) {
return orderMapper.updateOrder(order);
}
}
@SpringBootApplication
@EnableTransactionManagement
@EnableFeignClients
public class Order_Seata {
public static void main(String[] args) {
SpringApplication.run(Order_Seata.class, args);
}
}
undo_log
表CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) DEFAULT NULL,
`xid` VARCHAR(100) DEFAULT NULL,
`context` VARCHAR(128) DEFAULT NULL,
`rollback_info` LONGBLOB,
`log_status` INT(11) DEFAULT NULL,
`log_created` DATETIME DEFAULT NULL,
`log_modified` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `xid` (`xid`),
UNIQUE KEY `branch_id` (`branch_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
测试
\seata\bin
目录下的seata-server.bat
服务数据库数据没有任何变化
Debug原理
如果报错:无法连接到 seata-server
can not register RM,err:can not connect to services-server
需要重新运行\seata\bin
目录下的seata-server.bat
服务
xid
order
表的回滚日志stock
表的回滚日志下一篇文章《微服务分布式组件—Gateway》