一、事务基础概念
1.1 什么是本地事务
当需要一次执行多条 SQL 时,要么全部成功提交,要么全部失败回滚 ,这就是事务。经典总结:去不了终点,回到原点。
1.2 事务四大特性(ACID)
- 原子性(Atomicity):事务是最小执行单位,不可分割
- 一致性(Consistency):事务执行前后数据保持正确
- 隔离性(Isolation):并发事务相互隔离,互不干扰
- 持久性(Durability):事务提交后不可回滚
1.3 本地事务控制方式
1)数据库原生控制
mysql
START TRANSACTION;
-- 保存订单
-- 扣减库存
COMMIT;
-- 异常执行
ROLLBACK;
2)JDBC 控制事务
java
// 关闭自动提交
conn.setAutoCommit(false);
try {
// 执行多条SQL
conn.commit();
} catch (Exception e) {
conn.rollback();
}
3)Spring AOP 事务
java
@Transactional
public void insertOrder(TbOrder tbOrder) {
// 保存订单
tbOrderMapper.insertSelective(tbOrder);
// 扣减库存
itemServiceFeign.updateItem(tbOrder.getItemId(), tbOrder.getNum());
}
二、分布式事务详解
2.1 什么是分布式事务

单体应用拆分为微服务后,跨服务、跨数据库、跨 JVM 完成的事务操作,就是分布式事务。典型场景:下单 + 减库存、注册送积分、跨库转账。
2.2 分布式事务产生场景
-
微服务远程调用 (跨 JVM):订单服务 → 商品服务

-
单服务访问多数据库 :操作不同 MySQL 实例

-
多服务访问同数据库 :不同连接导致事务失效

2.3 本地事务失效问题
java
@Transactional
public void insertOrder(TbOrder tbOrder) {
// 保存订单
tbOrderMapper.insertSelective(tbOrder);
// 远程扣减库存
itemServiceFeign.updateItem(...);
// 模拟异常
int a = 6/0;
}
- 两个服务中订单回滚了
- 库存扣减无法回滚,出现数据不一致
三、分布式事务解决方案 ------ Seata
3.1 Seata 简介
Seata(原 Fescar)是阿里巴巴开源的一站式分布式事务解决方案 ,目标:让分布式事务用起来像本地事务一样简单。官网:https://seata.io/zh-cn/
https://seata.io/zh-cn/
3.2 Seata 四种事务模式
- XA 模式:强一致性,无业务侵入
- AT 模式 :最终一致,无侵入,默认使用
- TCC 模式:最终一致,有业务侵入
- SAGA 模式:长事务,有业务侵入
3.3 Seata 三大核心组件

- TC(Transaction Coordinator):事务协调器,维护全局事务状态,协调提交 / 回滚
- TM(Transaction Manager):事务管理器,定义全局事务,开启 / 提交 / 回滚全局事务
- RM(Resource Manager):资源管理器,管理本地事务,注册分支、上报状态
四、Seata AT 模式工作流程
4.1 一阶段(执行 + 准备回滚)

- 解析 SQL,获取表、条件等信息
- 查询前镜像(修改前数据)
- 执行业务 SQL
- 查询后镜像(修改后数据)
- 写入
undo_log回滚日志 - 注册分支事务,申请全局锁
- 本地事务提交
- 上报状态给 TC
4.2 二阶段 - 回滚

- 接收 TC 回滚指令
- 根据 XID + BranchID 查询
undo_log - 数据校验
- 使用前镜像恢复数据
- 本地提交,删除
undo_log
4.3 二阶段 - 提交

全局事务成功,异步删除 undo_log,速度极快。
五、Seata 安装与启动(Nacos 注册配置)
5.1 下载
https://github.com/seata/seata/releases

5.2 上传并解压
sh
cd /usr/upload
tar -zxvf seata-server-1.4.2.tar.gz -C /usr/local
5.3 修改 registry.conf
修改seata/seata-server-1.4.2/conf/目录下的registry.conf文件:
properties
registry {
type = "nacos"
nacos {
serverAddr = "192.168.19.132:8848" # nacos的地址
group = "SEATA_GROUP" # seata服务所在分组
application = "seata-server" # seata服务所在的名称空间,这里不填就是使用默认的"public"
cluster = "default" # TC集群名,默认是"default"
}
}
config {
type = "nacos"
nacos {
serverAddr = "192.168.19.132:8848"
group = "SEATA_GROUP"
dataId = "seataServer.properties"
}
}
5.4 Nacos 配置中心添加配置
配置文件地址:https://gitee.com/seata-io/seata/raw/develop/script/config-center/config.txt主要配置:存储模式为 db,连接 MySQL。
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.19.130:3306/seata?useUnicode=true&rewriteBatchedStatements=true&useSSL=false
store.db.user=root
store.db.password=1111
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
# 事务、日志等配置
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
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
效果:

5.5 创建 Seata 服务表
mysql
-- global_table、branch_table、lock_table
建表语句:https://gitee.com/seata-io/seata/raw/develop/script/server/db/mysql.sql
sql
-- -------------- 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;
5.6 启动 Seata
sh
sql
cd seata/bin
./seata-server.sh -h 192.168.19.133 -p 8091
注册成功如下:

六、微服务整合 Seata 实战
6.1 准备数据库
- item 库:tb_item + undo_log
- order 库:tb_order + undo_log
tb_item 建表语句
mysql
sql
-- 商品表 ----------------------------
DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`num` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- Records of tb_item ----------------------------
INSERT INTO `tb_item` VALUES ('1', '手机', '100');
INSERT INTO `tb_item` VALUES ('3', '电脑', '100');
-- 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 NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) 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';
tb_order 建表语句
mysql
sql
-- Table structure for tb_order ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`item_id` int(11) DEFAULT NULL,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 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 NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) 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';
6.2 项目结构
- springcloud_parent(父工程)
pom.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hg</groupId>
<artifactId>springcloud_parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>springcloud_common</module>
<module>nacos_provider</module>
<module>nacos_consumer</module>
<module>nacos_config</module>
<module>ribbon_provider_01</module>
<module>ribbon_provider_02</module>
<module>ribbon_consumer</module>
<module>feign_provider_01</module>
<module>feign_provider_02</module>
<module>feign_interface</module>
<module>feign_consumer</module>
<module>sentinel_provider</module>
<module>sentinel_interface</module>
<module>sentinel_consumer</module>
<module>api_gateway</module>
<module>seata_item_service</module>
<module>seata_common</module>
<module>seata_order_service</module>
<module>seata_Item_service_feign</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Netflix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud 阿里巴巴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
- common_pojo(实体)
Item 实体
java
package com.hg.pojo;
public class Item {
private Integer id;
private String name;
private Integer num;
public Item() {
}
@Override
public String toString() {
return "Item{" +
"id=" + id +
", name='" + name + '\'' +
", num=" + num +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
}
Order 实体
java
package com.hg.pojo;
public class Order {
private Integer id;
private Integer itemId;
private Integer num;
@Override
public String toString() {
return "Order{" +
"id=" + id +
", itemId=" + itemId +
", num=" + num +
'}';
}
public Order() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getItemId() {
return itemId;
}
public void setItemId(Integer itemId) {
this.itemId = itemId;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
}
- seata_item_service(商品服务)
pom.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<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>seata_demo</artifactId>
<groupId>com.hg</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata_item_service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<!-- MySql Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Alibaba DataBase Connection Pool-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!--MyBatis And Spring Integration Starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.hg</groupId>
<artifactId>common_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml
java
server:
port: 81
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.19.130:3306/item?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 1111
type: com.alibaba.druid.pool.DruidDataSource
cloud:
nacos:
discovery:
server-addr: 192.168.19.132:8848
application:
name: seata-item-service
mybatis:
mapper-locations: classpath:mapper/*.xml
seata:
registry:
type: nacos #查找TC服务,参考registry.conf
nacos:
server-addr: 192.168.19.132:8848
namespace: ""
group: SEATA_GROUP
application: seata-server #TC服务名
tx-service-group: springcloud-parent #事务组,根据tx-service-group名称获得TC服务cluster名称
service:
vgroup-mapping: #tx-service-group与TC cluster的映射关系
springcloud-parent: default
service
java
@Service
@Transactional
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemMapper itemMapper;
@Override
public void updateItem(Integer itemId, Integer num) {
Item tbItem = itemMapper.selectByPrimaryKey(itemId);
tbItem.setNum(tbItem.getNum()-num);
itemMapper.updateByPrimaryKeySelective(tbItem);
}
}
controller
java
@RestController
@RequestMapping("/item")
public class ItemController {
@Autowired
private ItemService itemService;
@RequestMapping("/updateItem")
public void updateItem(Integer itemId, Integer num){
itemService.updateItem(itemId,num);
}
}
mapper
java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hg.mapper.ItemMapper">
<!-- 商品表结果映射 -->
<resultMap id="BaseResultMap" type="com.hg.pojo.Item">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="num" property="num" jdbcType="INTEGER"/>
</resultMap>
<!-- 根据主键查询商品信息 -->
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
SELECT id, name, num
FROM tb_item
WHERE id = #{id,jdbcType=INTEGER}
</select>
<!-- 根据主键选择性更新商品信息 -->
<update id="updateByPrimaryKeySelective" parameterType="com.hg.pojo.Item">
UPDATE tb_item
<set>
<if test="name != null">
name = #{name,jdbcType=VARCHAR},
</if>
<if test="num != null">
num = #{num,jdbcType=INTEGER},
</if>
</set>
WHERE id = #{id,jdbcType=INTEGER}
</update>
</mapper>
app启动类
java
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.hg.mapper")
public class seataItemServiceApp {
public static void main(String[] args) {
SpringApplication.run(seataItemServiceApp.class);
}
}
- seata_item_feign(feign 接口)
pom.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<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>springcloud_parent</artifactId>
<groupId>com.hg</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata_Item_service_feign</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.hg</groupId>
<artifactId>seata_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
Feign接口
java
@FeignClient("seata-item-service")
@RequestMapping("/item")
public interface SeataItemFeign {
@RequestMapping("/updateItem")
public void updateItem(@RequestParam("itemId") Integer itemId, @RequestParam("num") Integer num);
}
- seata_order_service(订单服务)
pom.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<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>springcloud_parent</artifactId>
<groupId>com.hg</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata_order_service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.hg</groupId>
<artifactId>seata_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.hg</groupId>
<artifactId>seata_Item_service_feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
</project>
application.yml
java
server:
port: 80
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.19.130:3306/order?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 1111
type: com.alibaba.druid.pool.DruidDataSource
cloud:
nacos:
discovery:
server-addr: 192.168.19.132:8848
application:
name: seata-order-service
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.hg.pojo
seata:
registry:
type: nacos #查找TC服务,参考registry.conf
nacos:
server-addr: 192.168.19.132:8848
namespace: ""
group: SEATA_GROUP
application: seata-server #TC服务名
tx-service-group: springcloud-parent #事务组,根据tx-service-group名称获得TC服务cluster名称
service:
vgroup-mapping: #tx-service-group与TC cluster的映射关系
springcloud-parent: default
controller
java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/addOrder")
public Map addOrder(Order order){
Map<Integer, Object> result = new HashMap<>();
try {
orderService.addOrder(order);
result.put(200,"下单成功");
return result;
} catch (Exception e) {
e.printStackTrace();
result.put(500,"下单失败");
return result;
}
}
}
service
java
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private SeataItemFeign seataItemFeign;
@Override
public void addOrder(Order order) {
//1、保存订单
orderMapper.addOrder(order);
//2、扣减库存
seataItemFeign.updateItem(order.getItemId(), order.getNum());
//模拟扣款失败,此时扣减库存会回滚吗?
int a = 6/0;
}
}
mapper
java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hg.mapper.OrderMapper">
<!-- 订单表结果映射 -->
<resultMap id="BaseResultMap" type="com.hg.pojo.Order">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="item_id" property="itemId" jdbcType="INTEGER"/>
<result column="num" property="num" jdbcType="INTEGER"/>
</resultMap>
<!-- 添加订单 -->
<insert id="addOrder" parameterType="com.hg.pojo.Order" useGeneratedKeys="true" keyProperty="id">
INSERT INTO tb_order (item_id, num)
VALUES (#{itemId}, #{num})
</insert>
</mapper>
app启动类
java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.hg.mapper")
public class seataOrderServiceApp {
public static void main(String[] args) {
SpringApplication.run(seataOrderServiceApp.class);
}
}
6.3 核心依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
6.4 微服务配置(application.yml)
查找TC服务,需要四个信息:namespace、group、application name、cluster name

yaml
item服务和order服务都需要配置
java
seata:
registry:
type: nacos
nacos:
server-addr: 192.168.19.132:8848
group: SEATA_GROUP
application: seata-server
tx-service-group: springcloud-parent
service:
vgroup-mapping:
springcloud-parent: default
测试:
启动seata_item_service服务如果发现RM注册失败
查看注册到Nacos中的seata-server服务ip不是公网

在seata启动时,后面加上-h:ip
java
./seata-server.sh -h 192.168.204.135
6.5 开启分布式事务
在订单服务 方法上添加 @GlobalTransactional
java
@Service
public class OrderServiceImpl implements OrderService {
@GlobalTransactional
public void insertOrder(TbOrder tbOrder) {
// 保存订单
tbOrderMapper.insertSelective(tbOrder);
// 扣减库存
itemServiceFeign.updateItem(...);
// 模拟异常
int a = 6/0;
}
}
6.6 测试效果
- 异常时:订单回滚 + 库存回滚

- 成功时:订单创建 + 库存扣减完美解决分布式事务一致性问题!

七、分布式链路追踪 ------ SkyWalking
7.1 为什么需要 SkyWalking

微服务调用链复杂,一个请求跨多个服务,传统日志无法追踪整条链路,SkyWalking 可以:
- 分布式链路追踪
- 服务性能监控
- 异常告警
- 日志采集
7.2 SkyWalking 架构

- Agent:探针,无侵入采集数据
- OAP:接收、分析、存储数据
- UI:可视化展示
7.3 SkyWalking 安装
- 下载:https://archive.apache.org/dist/skywalking/
- 解压(路径不能有中文)
- 修改 UI 端口:webapp/application.yml

yaml
serverPort: ${SW_SERVER_PORT:-18080} #端口号
- 启动:bin/startup.bat

启动成功后会启动两个服务,skywalking-aop-server和skywalking-web-ui
skywalking-aop-server服务启动后会暴露11800和12800两个端口,分别为收集监控数据的端口11800和接受前端请求的端口12800

八、SkyWalking 接入微服务
8.1 JVM 探针配置
yaml
# 你的 skywalking-agent.jar包地址
-javaagent:D:\skywalking-agent\skywalking-agent.jar
# 在skywalking上显示的服务名
-DSW_AGENT_NAME=skywalking-agent
#你的skywalking-aop-server地址及端口号
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800
注意:配置时去掉注释


启动成功后,随便对一个接口进行访问,然后刷新我们的skywalking UI界面

服务界面介绍

8.2 数据持久化(MySQL)
默认使用 H2 内存库,重启数据丢失,改为 MySQL:
- 修改 config/application.yml


- 放入 MySQL 驱动到 oap-libs

- 创建数据库
swtest,重启自动建表
九、SkyWalking 高级功能
9.1 自定义链路追踪(@Trace)
xml
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.5.0</version>
</dependency>
java
@Trace
@Tags({
@Tag(key = "参数", value = "arg[0]"),
@Tag(key = "返回值", value = "returnedObj")
})
public String test(String str) {
return "hello " + str;
}
为追踪链路增加其额外的信息,实现方式:在方法上增加@Tags和@Tag
测试
重启后,对一个接口访问,然后刷新skywalking UI界面,自定义链路追踪效果:

可以看到我们的业务方法也进入到了链路当中
链路追踪添加的额外信息,效果:

9.2 日志整合
引入日志依赖,配置 logback.xml,日志自动带上 TraceID。
- 在需要添加日志的服务里引入依赖
java
<!--apm‐toolkit‐logback‐1.x 日志 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.5.0</version>
</dependency>
- 添加文件
logback.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志输出格式 -->
<property name="log.pattern"
value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %boldMagenta([%tid]) %highlight(%-5level) %boldMagenta(%logger{36}) - %gray(%msg%n)"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>${log.pattern}</Pattern>
</layout>
</encoder>
</appender>
<!-- 使用gRpc将日志发送到skywalking服务端 -->
<appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>${log.pattern}</Pattern>
</layout>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="grpc-log"/>
</root>
</configuration>
- 重启项目,查看日志

9.3 性能分析
针对接口做慢查询采样,定位性能瓶颈。
- 模拟慢查询
java
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("测试慢查询");
return userFeign.getUserById(id);
}
- 点击新建任务

- 设置监控端点

- 请求5次以上然后进行刷新

作用:可以准确定位资源的问题所在
9.4 告警配置
修改 config/alarm-settings.yml,配置阈值与 Webhook,实现服务异常自动通知。
- 实体类用来接受数据
java
@Data
public class SkAlarmDTO {
private Integer scopeId;
private String scope;
private String name;
private String id0;
private String id1;
private String ruleName;
private String alarmMessage;
private Long startTime;
}
- 实现告警通知
java
@RestController
@RequestMapping("/skAlarm")
public class SkAlarmController {
@RequestMapping("/getMsg")
public void getMsg(@RequestBody List<SkAlarmDTO> skAlarmDTOList){
System.out.println("接收消息,然后进行处理-发送短信,发送邮件");
StringBuilder sb = new StringBuilder();
for (SkAlarmDTO dto : skAlarmDTOList) {
sb.append("\nscopeId: ").append(dto.getScopeId())
.append("\nscope: ").append(dto.getScope())
.append("\n目标 Scope 的实体名称: ").append(dto.getName())
.append("\nScope 实体的 ID: ").append(dto.getId0())
.append("\nid1: ").append(dto.getId1())
.append("\n告警规则名称: ").append(dto.getRuleName())
.append("\n告警消息内容: ").append(dto.getAlarmMessage())
.append("\n告警时间: ").append(dto.getStartTime())
.append("\n‐‐‐‐‐‐‐‐‐‐‐‐‐‐--------------------------‐\n");
}
System.out.println(sb);
}
}
- 添加告警后需要请求的接口
修改config/alarm-settings.yml文件
java
webhooks:
- http://127.0.0.1:80/skAlarm/getMsg
- 定义报错的接口
java
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
int a = 6/0;
return userFeign.getUserById(id);
}
- 重新启动skywalking,然后对接口进行请求触发告警,控制台即可打印出告警信息


十、总结
- 本地事务:单库单服务,@Transactional 即可
- 分布式事务:跨服务 / 跨库,必须用 Seata
- Seata AT 模式:无侵入、最终一致,生产首选
- SkyWalking:无侵入链路追踪 + 性能监控 + 告警,微服务必备
本文从理论到实战,完整覆盖分布式事务与链路追踪两大微服务核心痛点,建议收藏反复练习!