分布式事务 Seata 详解 + 链路追踪 SkyWalking 实战

一、事务基础概念

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 分布式事务产生场景

  1. 微服务远程调用 (跨 JVM):订单服务 → 商品服务

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

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

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 一阶段(执行 + 准备回滚)

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

4.2 二阶段 - 回滚

  1. 接收 TC 回滚指令
  2. 根据 XID + BranchID 查询undo_log
  3. 数据校验
  4. 使用前镜像恢复数据
  5. 本地提交,删除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 安装

  1. 下载:https://archive.apache.org/dist/skywalking/
  2. 解压(路径不能有中文
  3. 修改 UI 端口:webapp/application.yml

yaml

复制代码
serverPort: ${SW_SERVER_PORT:-18080} #端口号
  • 启动:bin/startup.bat

启动成功后会启动两个服务,skywalking-aop-serverskywalking-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:

  1. 修改 config/application.yml
  2. 放入 MySQL 驱动到 oap-libs
  3. 创建数据库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,然后对接口进行请求触发告警,控制台即可打印出告警信息

十、总结

  1. 本地事务:单库单服务,@Transactional 即可
  2. 分布式事务:跨服务 / 跨库,必须用 Seata
  3. Seata AT 模式:无侵入、最终一致,生产首选
  4. SkyWalking:无侵入链路追踪 + 性能监控 + 告警,微服务必备

本文从理论到实战,完整覆盖分布式事务与链路追踪两大微服务核心痛点,建议收藏反复练习!

相关推荐
曹牧1 小时前
Spring:@RequestMapping 注解匹配顺序
java·后端·spring
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【44】多智能体 - 混合模式、监督者(SupervisorAgent)、自定义模式
java·人工智能·spring
_日拱一卒1 小时前
LeetCode:23合并K个升序链表
java·数据结构·算法·leetcode·链表·职场和发展
cany10001 小时前
C++ -- 泛型编程
java·开发语言·c++
lee_curry1 小时前
第三章 jvm中的对象和执行引擎
java·jvm·执行引擎
wang09071 小时前
Linux性能优化之文件系统基础介绍
java·linux·性能优化
AI攻城狮1 小时前
DeepSeek 的 Vision 能力要来了吗?
人工智能·后端·openai
迷藏4941 小时前
# 发散创新:用Locust实现高并发场景下的精准压力测试与性能调优实战在现代微服务架构中,**接口稳定性与响应速度**已成为衡量
java·python·微服务·架构·压力测试
用户622475758461 小时前
面试官问我:"如何实现你项目中的这块代码."我说:"看好了."
后端