SpringCloud Alibaba Seata

news2025/1/12 6:14:14

SpringCloud Alibaba Seata

Seata 基础

先看一个问题,引出Seata

  1. 单机单库(多表)处理事务示意图

image.png

  1. 分布式微服务架构下的数据库事务示意图

image.png
3. 梳理上图

  • 用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持∶
  • 仓储服务∶对给定的商品扣除仓库/商品数量
  • 订单服务;根据采购需求创建订单
  • 帐户服务∶从用户帐户中扣除余额
  1. 问题分析

    单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源
    业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证
    但是全局的数据—致性问题没法保证
    简单的说: 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题=》seata

分布式事务问题&解决方案

  1. 分布式微服务架构下的全局数据一致性问题[即: 分布式事务问题]
  2. 解决方案: Seata

官网

  1. http://seata.io/zh-cn/

  2. 使用手册: https://seata.io/zh-cn/docs/overview/what-is-seata.html

Seata 是什么?

一句话: Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用
的分布式事务服务

Seata Server 安装

下载

https://github.com/seata/seata/releases/tag/v0.9.0

安装和配置

  1. 将seata-server-0.9.0.zip 解压到指定目录,比如d:\program

  2. 修改conf\file.conf 文件, 如图

service {
#vgroup->rgroup
#vgroup_mapping.my_test_tx_group = "default"
vgroup_mapping.my_test_tx_group = "wyx_order_tx_group"##---这个组自己写自己想定义的名字
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default
permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
## transaction log store
store {
## store mode: file、db
##mode = "file"
mode = "db" ##--这里选择db数据库
## database store
db {
## the implement of javax.sql.DataSource, such as
DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
##user = "mysql"
user = "root"##-自己的用户名
##password = "mysql"
password = "自己的密码"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
  1. 在mysql5.7 创建seata 数据库
CREATE DATABASE seata
USE seata
  1. 在seata 数据库创建表, 使用seata 提供的sql 脚本即可, 在seata 的\conf\db_store.sql

  2. 修改seata 的\conf\registry.conf , 配置注册中心nacos server

registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# type = "file"
type = "nacos"#这里选择nacos
nacos {
#serverAddr = "localhost"
serverAddr = "localhost:8848"#这里填自己的端口主机
namespace = ""
cluster = "default"
}

启动

  1. 启动Nacos Server 8848

  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server

    1. 启动seata-server.bat , 看到如下界面说明成功

image.png

  1. 登录Nacos Server , 查看Seata Server 是否注册成功
    1. 登录Nacos Server, 查看Seata Server 是否注册成功

image.png

Seata 分布式事务-应用实例

需求分析/图解

  1. 需求:完成下订单功能,由三个微服务模块协同完成, 涉及到多数据库, 多张表

image.png
分析 黑色线是执行顺序线 红色线是想Seata Server注册 最后紫色线是决定是否提交和回滚

项目目录

主题包结构都是一样的但是类名字每个项目是不一样的这里列举的10012端口微服务的

image.png

创建数据库和表

-- 订单微服务的数据库
CREATE DATABASE order_micro_service
USE order_micro_service

CREATE TABLE `order`(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT DEFAULT NULL ,
product_id BIGINT DEFAULT NULL ,
nums INT DEFAULT NULL ,
money INT DEFAULT NULL,
`status` INT DEFAULT NULL COMMENT '0:创建中; 1:已完结'
);
SELECT * FROM `order`

-- 库存微服务的数据库`storage``order`
CREATE DATABASE storage_micro_service
USE storage_micro_service

CREATE TABLE `storage`(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT DEFAULT NULL ,
amount INT DEFAULT NULL COMMENT '库存量'
);

-- 初始化库存表
INSERT INTO `storage` VALUES(NULL, 1, 10);
SELECT * FROM `storage`

-- 账号微服务的数据库
CREATE DATABASE account_micro_service
USE account_micro_service

CREATE TABLE `account`(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT DEFAULT NULL ,
money INT DEFAULT NULL COMMENT '账户金额'
);

-- 初始化账户表
INSERT INTO `account` VALUES(NULL, 666, 10000);

分别为3 库创建对应的回滚日志表, 说明回滚日志表在seata 的\conf\db_undo_log.sql

use order_micro_service
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

use storage_micro_service
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

use account_micro_service
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

开发seata_storage_micro_service-10010 微服务

参考以前的方式,创建seata_storage_micro_service-10010 微服务模块

修改pom.xml

添加相关的jar 依赖

    <!--引入相关的依赖-->
    <dependencies>
        <!--引入opefeign starter 即场景启动器 因为要和其他微服务通信-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--引入 seata starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <!--排除自带的seata-all, 引入自己的版本, 否则会出现冲突-->
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--引入指定版本的seata-all-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--引入nacos-starter nacos的场景启动器starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--引入web-starter 说明我们使用版本仲裁(从父项目继承了版本)
        -->
        <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>

        <!--引入mybatis-starter 整合到springboot-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!--引入druid-starter-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <!--这里需要我们指定版本, 因为父项目没有-->
            <version>1.1.17</version>
        </dependency>

        <!--引入mysql依赖,使用版本仲裁-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--spring-boot-start-jdbc引入-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--引入test-starter-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!--引入公共模块-->
        <dependency>
            <groupId>com.wyxedu.springcloud</groupId>
            <artifactId>e_commerce_center-common-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

创建application.yml

进行相关的配置

server:
  port: 10010
spring:
  application:
    name: seata-storage-micro-service
  cloud:
    alibaba:
      seata:
        #指定事务组名,需要和seata-server中的对应 /conf/file.conf
        tx-service-group: wyx_order_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848 #指定nacos server地址
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
         #注意数据库要改对应的数据库
    url: jdbc:mysql://localhost:3306/storage_micro_service
    username: root
    password: 自己的密码
#配置seata日志输出
logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

创建conf文件

resources目录下

  • 创建file.conf, 进行相关的配置, 说明:该文件从seata 的\conf\file.conf 拷贝,进行修改即可 注意因为我们前面已经修改了所以这里可以不需要修改之际拷贝即可

  • 创建registry.conf, 进行相关的配置, 说明:该文件从seata 的\conf\registry.conf 拷贝,进行修改即可 注意因为我们前面已经修改了所以这里可以不需要修改之际拷贝即

创建/entity/Storage

/entity/Storage.java

/**
 * Storage 实体类对应 storage表
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
    private Long id;
    private Long productId;
    private Integer amount;
}

创建StorageDao

/dao/StorageDao.java接口

@Mapper
public interface StorageDao {
    //扣减库存信息
    void reduce(@Param("productId") Long productId, @Param("nums") Integer nums);
}

注意这里使用; @Param注解指定一下为好 防止不识别

创建StorageMapper

resources/mapper/StorageMapper.xml

<?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.wyxedu.springcloud.dao.StorageDao">

    <resultMap id="BaseResultMap" type="com.springcloud.entity.Storage">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="amount" property="amount" jdbcType="INTEGER"/>
    </resultMap>
    <!-- 减少库存 -->
    <update id="reduce">
        UPDATE
            storage
        SET
            amount = amount - #{nums}
        WHERE
            product_id = #{productId}
    </update>

</mapper>

创建StorageService

service/StorageService.java接口

public interface StorageService {
    // 扣减库存
    void reduce(Long productId, Integer nums);
}

创建StorageServiceImpl

service/impl/StorageServiceImpl.java

@Slf4j
@Service
public class StorageServiceImpl implements StorageService {

    @Resource
    private StorageDao storageDao;

    @Override
    public void reduce(Long productId, Integer nums) {
        log.info("==========seata_storage_micro_service-10010 扣减库存 start==========");
        storageDao.reduce(productId, nums);
        log.info("==========seata_storage_micro_service-10010 扣减库存 end==========");

    }
}

创建StorageController

controller/StorageController.java

@RestController
public class StorageController {

    @Resource
    private StorageService storageService;

    //扣减库存
    @PostMapping("/storage/reduce")
    public Result reduce(@RequestParam("productId") Long productId,@RequestParam("nums") Integer nums) {
        storageService.reduce(productId, nums);
        return Result.success("扣减库存成功ok", null);
    }

}

创建MyBatisConfig

config/MyBatisConfig

/**
 * 常规配置 Mybatis 和 dao关联
 */
@Configuration
@MapperScan({"com.springcloud.dao"})
public class MyBatisConfig {
}

创建DataSourceProxyConfig

config/DataSourceProxyConfig常规配置(拿来使用即可)

注意DataSourceProxy 是引入的 io.seata.rm.datasource不要引入错了


/**
 * 说明
 * 1. 这里很重要: 配置数据源的代理是seata 也就是使用seata代理数据源
 * 2. DataSourceProxy 是引入的 io.seata.rm.datasource
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    //配置druidDataSource
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    //配置DataSourceProxy- 使用seata代理数据源
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    //配置SqlSessionFactory-常规写法
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean =
                new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations
                (new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory
                (new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

创建主启动类

SeataStorageMicroServiceApplication10010.java

//注意: 需要取消数据源的自动配置
//而是使用seata 代理数据源, DataSourceProxy
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageMicroServiceApplication10010 {
    public static void main(String[] args) {
        SpringApplication.run
                (SeataStorageMicroServiceApplication10010.class,args);
    }
}

测试seata_storage_micro_service-10010 微服务

  1. 启动Nacos Server 8848

  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server

  3. 启动seata_storage_micro_service-10010

  4. 登录Nacos Server , 查看10010 微服务是否注册成功

    4. 1. 登录Nacos Server, 查看10010 是否注册成功

image.png

开发seata_account_micro_service-10012 微服务

  1. 参考以前的方式,创建seata_account_micro_service-10012 微服务模块

  2. 修改pom.xml, 添加相关的jar 依赖 和1010端口微服务一模一样

创建application.yml

进行相关的配置

server:
  port: 10012
spring:
  application:
    name: seata-account-micro-service
  cloud:
    alibaba:
      seata:
        #指定事务组名,需要和seata-server中的对应 /conf/file.conf
        tx-service-group: wyxedu_order_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848 #指定nacos server地址
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
         #注意数据库要改对应的数据库
    url: jdbc:mysql://localhost:3306/account_micro_service
    username: root
    password: 自己的密码
#配置seata日志输出
logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

创建conf文件

resources目录下

  • 创建file.conf, 进行相关的配置, 说明:该文件从seata 的\conf\file.conf 拷贝,进行修改即可 注意因为我们前面已经修改了所以这里可以不需要修改之际拷贝即可

  • 创建registry.conf, 进行相关的配置, 说明:该文件从seata 的\conf\registry.conf 拷贝,进行修改即可 注意因为我们前面已经修改了所以这里可以不需要修改之际拷贝即可

创建Account

com/springcloud/entity/Account.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private Long id;
    private Long userId;
    private Integer money;

}

创建AccountDao

com/springcloud/dao/AccountDao.java接口

@Mapper
public interface AccountDao {
    void reduce(@Param("userId") Long userId, @Param("money") Integer money);
}

创建AccountMapper

resources/mapper/AccountMapper.xml

<?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.wyx.springcloud.dao.AccountDao">

    <resultMap id="BaseResultMap" type="com.wyxedu.springcloud.entity.Account">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="money" property="money" jdbcType="INTEGER"/>

    </resultMap>

    <!-- 扣减金额用户account表的money -->
    <update id="reduce">
        UPDATE account
        SET
          money = money - #{money}
        WHERE
          user_id = #{userId};
    </update>

</mapper>

创建AccountService

com/springcloud/service/AccountService.java接口

public interface AccountService {
    //扣减用户的money
    void reduce(Long userId, Integer money);
}

AccountServiceImpl

com/springcloud/service/impl/AccountServiceImpl.java

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {

    @Resource
    AccountDao accountDao;

    @Override
    public void reduce(Long userId, Integer money) {
        log.info("========seata_account_micro_service-10012 扣减账户余额 start ======");
        accountDao.reduce(userId, money);
        log.info("========seata_account_micro_service-10012 扣减账户余额 end ======");
    }
}

创建AccountController

com/springcloud/controller/AccountController.java

@RestController
public class AccountController {
    @Resource
    AccountService accountService;

    /**
     * 扣减账户余额
     */
    @PostMapping("/account/reduce")
    public Result reduce(@RequestParam("userId") Long userId, @RequestParam("money") Integer money){
    
        accountService.reduce(userId,money);
        return Result.success("200", "扣减账户余额OK");
    }
}

创建MyBatisConfig

//常规配置 Mybatis 和 dao关联
@Configuration
@MapperScan({"com.wyxedu.springcloud.dao"})
public class MyBatisConfig {
}

创建DataSourceProxyConfig

config/DataSourceProxyConfig常规配置(拿来使用即可)

注意DataSourceProxy 是引入的 io.seata.rm.datasource不要引入错了

/**
 * 说明
 * 1. 这里很重要: 配置数据源的代理是seata 也就是使用seata代理数据源
 * 2. DataSourceProxy 是引入的 io.seata.rm.datasource
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    //配置druidDataSource
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    //配置DataSourceProxy- 使用seata代理数据源
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    //配置SqlSessionFactory-常规写法
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean =
                new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations
                (new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory
                (new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

创建主启动类

springcloud/SeataAccountMicroServiceApplication10012

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class SeataAccountMicroServiceApplication10012 {
    public static void main(String[] args) {
        SpringApplication.run
                (SeataAccountMicroServiceApplication10012.class,args);
    }
}

测试seata_storage_micro_service-10012 微服务

  1. 启动Nacos Server 8848

  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server

  3. 启动seata_storage_micro_service-10012

  4. 登录Nacos Server , 查看10010 微服务是否注册成功

    4. 1. 登录Nacos Server, 查看10012 是否注册成功

image.png

开发seata-order-micro-service-10008 微服务

  1. 参考以前的方式,创建seata-order-micro-service-10008 微服务模块

修改pom.xml

添加相关的jar 依赖

和10012 微服务一模一样

创建application.yml

进行相关的配置

server:
  port: 10008
spring:
  application:
    name: seata-order-micro-service
  cloud:
    alibaba:
      seata:
        #指定事务组名,需要和seata-server中的对应 /conf/file.conf
        tx-service-group: wyxedu_order_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848 #指定nacos server地址
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    #注意数据库要改对应的数据库
    url: jdbc:mysql://localhost:3306/order_micro_service
    username: root
    password: 自己的密码
#配置seata日志输出
logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

创建conf文件

resources目录下

  • 创建file.conf, 进行相关的配置, 说明:该文件从seata 的\conf\file.conf 拷贝,进行修改即可 注意因为我们前面已经修改了所以这里可以不需要修改之际拷贝即可

  • 创建registry.conf, 进行相关的配置, 说明:该文件从seata 的\conf\registry.conf 拷贝,进行修改即可 注意因为我们前面已经修改了所以这里可以不需要修改之际拷贝即可

创建Order

com/springcloud/entity/Order.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer nums;
    private Integer money;
    private Integer status;

}

创建OrderDao

com/springcloud/dao/OrderDao.java接口

@Mapper
public interface OrderDao {
    //新建订单
    void save(Order order);
    //修改订单状态
    void update(@Param("userId") Long userId, @Param("status") Integer status);

}

创建OrderMapper

resources/mapper/OrderMapper.xml

<?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.wyx.springcloud.dao.OrderDao">

    <resultMap id="BaseResultMap" type="com.wyxedu.springcloud.entity.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="nums" property="nums" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="INTEGER"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>
    <!--配置实现save方法 添加订单 -->
    <insert id="save">
        insert into `order` (id,user_id,product_id,nums,money,status)
        values (null,#{userId},#{productId},#{nums},#{money},0);
    </insert>

    <!--配置实现update- 修改订单状态 这里写的比较简单,实际工作中根据业务需求编写即可-->
    <update id="update">
        update `order` set status = 1
        where user_id=#{userId} and status = #{status};
    </update>
</mapper>

创建OrderService

com/springcloud/service/OrderService.java接口

public interface OrderService {
    void save(Order order);
}

创建AccountService

创建com/springcloud/service/AccountService.java接口

@FeignClient(value = "seata-account-micro-service")
public interface AccountService {

    /**
     * 解读
     * 1. 远程调用方式是 post
     * 2. 远程调用的url 为 http://seata_account_micro_service/account/reduce
     * 3. seata_account_micro_service是nacos注册中心服务名
     * 4. openfeign是通过接口方式调用服务
     */
    /**
     * 扣减账户余额
     */
    @PostMapping("/account/reduce")
    public Result reduce(@RequestParam("userId") Long userId,@RequestParam("money") Integer money);
}

创建StorageService

创建com/springcloud/service/StorageService.java接口

@FeignClient(value = "seata-storage-micro-service")
public interface StorageService {

    /**
     * 解读
     * 1. 远程调用方式是 post
     * 2. 远程调用的url 为 http://seata_storage_micro_service/storage/reduce
     * 3. seata_storage_micro_service是nacos注册中心服务名
     * 4. openfeign是通过接口方式调用服务
     */
    //扣减库存
    @PostMapping("/storage/reduce")
    public Result reduce(@RequestParam("productId") Long productId,@RequestParam("nums") Integer nums);
}

创建OrderServiceImpl

com/springcloud/service/impl/OrderServiceImpl.java


@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;
    /**
     * 创建订单->调用库存服务扣减库存->
     * 调用账户服务扣减账户余额->修改订单状态
     */
    @Override
    public void save(Order order) {
        log.info("=========开始新建订单start ==========");
        //新建订单
        orderDao.save(order);
        System.out.println("order=" + order);
        log.info("=========减库存start ==========");
        storageService.reduce(order.getProductId(), order.getNums());
        log.info("=========减库存end ==========");
        log.info("=========减账户金额start ==========");
        accountService.reduce(order.getUserId(), order.getMoney());
        log.info("=========减账户金额end ==========");
        log.info("=========修改订单状态start ==========");
        orderDao.update(order.getUserId(), 0);
        log.info("=========修改订单状态end ==========");
        log.info("=========下订单end==========");
    }
}

创建OrderController

com/springcloud/controller/OrderController.java

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    /**
     * 提醒 
     * 1. 我们的前端如果是以json格式来发送添加信息Order, 那么我们需要使用@RequestBody
     *     才能将数据封装到对应的bean, 同时保证http的请求头的 content-type是对应
     * 2. 如果前端是以表单形式提交了,则不需要使用@RequestBody, 才会进行对象参数封装, 同时保证
     *      http的请求头的 content-type是对应
     */
    @GetMapping("/order/save")
    public Result save(Order order) {
        orderService.save(order);
        return Result.success("订单创建成功", null);
    }
}

创建MyBatisConfig

//常规配置 Mybatis 和 dao关联
@Configuration
@MapperScan({"com.wyxedu.springcloud.dao"})
public class MyBatisConfig {
}

创建DataSourceProxyConfig

config/DataSourceProxyConfig常规配置(拿来使用即可)

注意DataSourceProxy 是引入的 io.seata.rm.datasource不要引入错了

/**
 * 说明
 * 1. 这里很重要: 配置数据源的代理是seata 也就是使用seata代理数据源
 * 2. DataSourceProxy 是引入的 io.seata.rm.datasource
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    //配置druidDataSource
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    //配置DataSourceProxy- 使用seata代理数据源
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    //配置SqlSessionFactory-常规写法
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean =
                new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations
                (new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory
                (new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

创建主启动类

springcloud/SeataAccountMicroServiceApplication10008

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMicroServiceApplication10008 {
    public static void main(String[] args) {
        SpringApplication.run
                (SeataOrderMicroServiceApplication10008.class,args);
    }
}

测试seata_storage_micro_service-10008 微服务

  1. 启动Nacos Server 8848
  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server
  3. 启动seata_storage_micro_service-10008
  4. 登录Nacos Server , 查看10010 微服务是否注册成功

4. 1. 登录Nacos Server, 查看10008 是否注册成功

image.png

集成测试(1) 三个微服务协同完成-正常下单

  1. 启动Nacos Server 8848

  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server

  3. 启动seata-order-micro-service-10010 /10012/10008 三个微服务

  4. 浏览器: http://localhost:10008/order/save?userId=666&productId=1&nums=1&money=100
    1.

  5. 查看数据库/表的情况是否正常, 结论:如果没有异常出现,正常下单,数据库三张表数据一致性是OK 的

image.png

注意事项和细节

  1. MySQL 出现too many connections(1040)错误
解决方法在my.ini 设置
max\_connections=1000
  1. 如果出现: service id not legal hostname报错Service id not legal hostname 的原因是服务名称不能带有下划线,可以使用中划线,springcloud 无法识别下划线,把下划线改成中划线就好

集成测试(2) 三个微服务协同完成-模拟异常

  1. 启动Nacos Server 8848
  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server
  3. 启动seata-order-micro-service-10010 /10012/10008 三个微服务
  4. 浏览器: http://localhost:10008/order/save?userId=666&productId=1&nums=1&money=100

修改seata_account_micro_service-10012 的com/springcloud/controller/AccountController.java, 模拟异常出现

@RestController
public class AccountController {
    @Resource
    AccountService accountService;

    /**
     * 扣减账户余额
     */
    @PostMapping("/account/reduce")
    public Result reduce(@RequestParam("userId") Long userId, @RequestParam("money") Integer money){

       // 模拟异常,超时
        //openfeign 接口调用默认超时时间为1s
        try {
           TimeUnit.SECONDS.sleep(12);
        } catch (InterruptedException e) {
           e.printStackTrace();
        }
        accountService.reduce(userId,money);
        return Result.success("200", "扣减账户余额OK");
    }
}
  1. 浏览器:http://localhost:10008/order/save?userId=666&productId=1&nums=1&money=100

image.png

  1. 查看数据库/表的情况是否正常, 结论:这时数据库/表,出现数据不一致现象, 订单是未支付,但是库存减少了,账号钱也扣了(提示: 等休眠时间完成后,再查看account 表,会看到数据不一致.)

集成测试(3)使用@GlobalTransactional

三个微服务协同完成-使用@GlobalTransactional完成分布式事务控制(出现异常,也能保证数据一致性)

  1. 启动Nacos Server 8848

  2. 双击Seata 的\bin\seata-server.bat , 启动Seata Server

  3. 启动seata-order-micro-service-10010 /10012/10008 三个微服务

  4. 浏览器: http://localhost:10008/order/save?userId=666&productId=1&nums=1&money=100

  5. 修改seata_account_micro_service-10012 的com/springcloud/controller/AccountController.java, 模拟异常出现

@RestController
public class AccountController {
    @Resource
    AccountService accountService;
    /**
     * 扣减账户余额
     */
    @PostMapping("/account/reduce")
    public Result result(@RequestParam("userId") Long userId, @RequestParam("money")
    Integer money){
    //模拟异常, 超时,或者int i = 9 / 0;
    //openfeign 接口调用默认超时时间为1s
    //说明1. 也可以使用其它方式模拟异常, 但在Debug 看Seata 分布式事务机制不方便, 不好看效果, 所以这里我们使用超时异常
    //说明2. 因为是超时异常, 所以在Debug 分析Seata 机制时, 可能会发现某张表被锁几条记录, 因为seata 会做最终一致性操作(即尝试再提交上次超时的事务).
        try {
            TimeUnit.SECONDS.sleep(12);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        accountService.reduce(userId,money);
        return Result.success("200", "扣减账户余额OK");
    }
}
  1. 修改seata-order-micro-service-10008 的com/springcloud/service/impl/OrderServiceImpl.java
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;

    @Resource
    private StorageService storageService;

    @Resource
    private AccountService accountService;

    @Override
    /**
     * 解读
     * 1. @GlobalTransactional : 分布式全局事务控制  io.seata.spring.annotation包
     * 2. name = "wyxedu-save-order" 名称,程序员自己指定,保证唯一即可
     * 3. rollbackFor = Exception.class 指定发送什么异常就回滚, 这里我们指定的是Exception.class
     *    即 只要发生了异常就回滚
     */
    @GlobalTransactional(name = "wyxedu-save-order", rollbackFor = Exception.class)
    public void save(Order order) {

        //后面我们如果需要打印日志
        log.info("====创建订单 start=====");

        log.info("====本地生成订单 start===");
        orderDao.save(order);//调用本地方法生成订单order
        log.info("====本地生成订单 end===");

        log.info("====扣减库存 start===");
        //远程调用storage微服务扣减库存
        storageService.reduce(order.getProductId(), order.getNums());
        log.info("====扣减库存 end===");

        log.info("====扣减用户余额 start===");
        //远程调用account微服务扣减用户money
        accountService.reduce(order.getUserId(), order.getMoney());
        log.info("====扣减用户余额 end===");

        log.info("====本地修改订单状态 start===");
        //调用本地方法修改订单状态0->1
        orderDao.update(order.getUserId(), 0);
        log.info("====本地修改订单状态 end===");

        log.info("====创建订单 end=====");
    }
}
  1. 重启seata-order-micro-service-10008

浏览器:http://localhost:10008/order/save?userId=666&productId=1&nums=1&money=100

image.png
在数据库就可以看到数据回滚了为什么可以呢我们下面详细说

注意事项和细节

如果数据库/表使用到关键字,需要使用反引号

举例说明:

比如mapper/OrderMapper.xml , 这里的order 就要使用``, 否则会报错

<insert id="save">
    insert into `order` (id,user_id,product_id,nums,money,status)
    values (null,#{userId},#{productId},#{nums},#{money},0);
</insert>
<update id="update">
    update `order` set status = 1
    where user_id=#{userId} and status = #{status};
</update>

openfeign 在远程调用api 接口时, 默认超时时间为1s

Seata 工作机制

说明

之所以放在后面说工作机制是因为如果一开始就说的话理解困难

所以我们有了前面的列子和说明我们在结合本节内容会收获的多理解相对容易点

分布式事务过程分析

  1. Seata 分布式事务处理过程-ID+三组件模型

image.png

image.png

image.png

debug

image.png

梳理: 术语

先说出现了几个术语XID, TC, TM, RM上图展示了一个分布式事务在Seata的处理过程

  1. Transaction ID XID: 全局唯一的事务ID
  2. Transaction Coordinator(TC) : 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
  3. Transaction Manager™ : 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
  4. Resource Manager(RM) : 控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

梳理: 执行过程

  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID XID在微服务调用链路的上下文中传播;
  2. RM 向TC注册分支事务,将其纳入XID 对应全局事务的管辖
  3. TM 向TC 发起针对XID 的全局提交或回滚决议
  4. TC 调度XID下管辖的全部分支事务完成提交或回滚请求。

Seata 事务模式

  1. 地址: https://seata.io/zh-cn/

image.png

  1. AT(默认模式)

  2. TCC

  3. SAGA

  4. XA

AT 无侵入模式

文档: https://seata.io/zh-cn/docs/overview/what-is-seata.html

一阶段加载

在一阶段,Seata 会拦截 业务SQL

image.png

image.png

  1. 解析SQL 语义,找到"业务SQL"要更新的业务数据,在业务数据被更新前,将其保存成"before image" (前置镜像)
  2. 执行"业务SQL"更新业务数据,在业务数据更新之后, 其保存成"after image"/后置镜像
  3. 最后生成行锁
  4. 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性

二阶段提交

image.png

  1. 二阶段如果是顺利提交
  2. 因为"业务SQL"在一阶段已经提交至数据库,所以Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可

二阶段回滚

image.png

  1. 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的"业务SQL",还原业务数据。
  2. 回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比"数据库当前业务数据"和"after image 如果两份数据完全一致就说明没有脏写,可以还原业务数据
  3. 如果不一致就说明有脏写,出现脏写就需要转人工处理。

AT 事务模式Debug 验证

SEATA 的分布式交易解决方案

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/580171.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第10届蓝桥杯Scratch国赛真题集锦

编程题 第 1 题 问答题 捉迷藏之 题目说明 编程实现:小猫随机躲在6个按固定位置排列的前景角色任一个的后面,只露出一点点痕迹。具体要求: 1).添加任意1个背景,保留小猫角色,从角色库中挑选6个角色作为前景角色(小猫将躲在它们的后面) 2).6个前景角色按照两行三列的方式以固…

从数据工程师到提示工程师:使用AI解决数据准备任务

数据工程占据了数据科学过程的很大一部分。在CRISP-DM中&#xff0c;这个过程阶段被称为“数据准备”。它包括数据摄取、数据转换和数据质量保证等任务。在本文中&#xff0c;我们使用ChatGPT和Python解决了典型的数据工程任务。通过这样做&#xff0c;我们探索了数据工程与新的…

Redis集群安装之分片集群

1.为什么使用分片集群 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决&#xff1a; 海量数据存储问题高并发写的问题 哨兵模式本质是依旧是主从模式&#xff0c;在主从模式下我们可以增加slave节点来拓展读并发能力&#xff0c;但是没办法扩展写能力…

项目笔记-瑞吉外卖(全)

文章目录 1.业务开发day011.软件开发整体介绍2.项目整体介绍:star:3.开发环境搭建4.登录功能:star:4.1代码实现 5.退出功能6.页面效果出现 day021.完善登录功能2.新增员工功能3.启用禁用员工信息:star:(自定义消息转换器使用)4.编辑员工信息 day031.公共字段自动填充2.新增分类…

第11届蓝桥杯Scratch国赛真题集锦

编程题 第 1题 问答题 3D打印小猫 题目说明 背景信息:3D打印技术,它与普通打印工作原理基本相同,打印机内装有液体或粉未等“打印材料”,与电脑连接后,通过电脑控制把“打印材料”一层层叠加起来,最终把计算机上的蓝图变成实物。 编程实现:通过滑杆控制小猫造型变化,按下…

聚类算法以及聚类算法模型评估的介绍

一、聚类算法的介绍 1.什么是聚类算法 聚类算法是一类无监督学习算法&#xff0c;用于将数据集中的对象分组&#xff08;或聚类&#xff09;成具有相似性的集合。聚类算法不依赖于预定义的类别标签&#xff0c;而是根据数据的内在特点将相似的数据点聚集在一起。聚类算法的目…

分布式网络通信框架(三)——protobuf使用案例

例子1 test.proto文件如下&#xff1a; syntax "proto3"; // 声明了protobuf版本package fixbug; // 声明了代码所在的包&#xff08;生成C代码后就是namespace 名字&#xff09;// 定义登录消息类型 message LoginRequest {string name 1; // 1 代表name是这个m…

详解uni-app项目运行在安卓真机调试

详解uni-app项目运行在安卓真机调试 uni-app项目运行在安卓真机调试 文章目录 详解uni-app项目运行在安卓真机调试前言为什么要用真机调试&#xff1f;真机调试操作步骤总结 前言 UNI-APP学习系列之详解uni-app项目运行在安卓真机调试 为什么要用真机调试&#xff1f; 因为安…

mybatis-plus实现非法sql拦截(防止全表更新与删除)

文章目录 什么是非法sql&#xff1a;拦截的意义是&#xff1a;使用&#xff1a;1、在pom文件中添加依赖2、注入MybatisPlusInterceptor类&#xff0c;并配置BlockAttackInnerInterceptor拦截器 测试&#xff1a; 什么是非法sql&#xff1a; 不带where子句的delete和update&…

C++反向迭代器

C反向迭代器 &#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;C &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容介绍反向迭代器的概念和模拟实现一个通用的反向…

HTB-Escape

HTB-Escape 信息收集立足sql_svc -> RyanRyan -> Administrator扩展NTLM简单介绍 信息收集 可以从中获取两个域名。 看看 rpc服务是否有可以收集的信息。 看看samba。 rpc的Public共享文件有一个关于SQL Server的pdf。 阅读它&#xff01; 从中能得到几个用户名…

unix环境高级编程 第一章 UNIX基础知识 Go实现代码

ls命令的Go语言实现 package mainimport ("fmt""os" )func main() {if len(os.Args) ! 2 {panic("参数数量不足")}targetPath : os.Args[1]if dirList, err : os.ReadDir(targetPath); err nil {for _, dirInfo : range dirList {fmt.Println(…

SpringBoot集成ElasticSearch

文章目录 前言一、ElasticSearch本地环境搭建二、SpringBoot整合ElasticSearch1.pom中引入ES依赖2.application.yaml配置elasticsearch3.ElasticSearchClientConnect连接ES客户端工具类4.ElasticSearchResult封装响应结果5.Person实体类6.Person实体类7.ElasticsearchControlle…

OS之磁盘调度算法

目录 一、先来先服务(FCFS) 基本思想 案例 二、最短寻道时间优先(SSTF) 基本思想 案例 饥饿现象 三、扫描算法(SCAN) 基本思想 案例 四、循环扫描算法(CSCAN) 基本思想 案例 一、先来先服务(FCFS) 基本思想 根据进程请求访问磁盘的先后次序来进行调度 案例 二、…

数据结构与算法04:队列

目录 什么是队列&#xff1f; 循环队列 双端队列 阻塞队列 队列的应用场景 每日一练 什么是队列&#xff1f; 在 上一篇文章 中讲述了栈&#xff1a;先进后出就是栈&#xff0c;队列刚好相反&#xff0c;先进先出的数据结构就是队列&#xff0c;还是拿纸箱子来举例&…

《数据库应用系统实践》------ 校友会信息系统

系列文章 《数据库应用系统实践》------ 校友会信息系统 文章目录 系列文章一、需求分析1、系统背景2、 系统功能结构&#xff08;需包含功能结构框图和模块说明&#xff09;3&#xff0e;系统功能简介 二、概念模型设计1&#xff0e;基本要素&#xff08;符号介绍说明&#x…

DJ6-6/7 文件共享和访问控制、文件保护

目录 6.6 文件共享和访问控制 1、同时存取 2、存取权限 3、文件共享的实现 6.6.1 基于索引结点的共享方式 1、基本思想 2、具体操作 6.6.2 利用符号链接实现文件共享 6.6.3 利用 URL 实现文件共享 6.7 文件保护 6.6 文件共享和访问控制 文件共享的有效控制涉及…

腾讯云服务器可用区是什么?怎么选择随机吗?

腾讯云服务器可用区什么意思&#xff1f;可用区&#xff08;Zone&#xff09;是指腾讯云在同一地域内电力和网络互相独立的物理数据中心&#xff0c;一个可用区故障不会影响另一个可用区的正常运行&#xff0c;所以可用区用于构建高容灾、高可靠性应用。腾讯云服务器网来详细说…

如何在华为OD机试中获得满分?Java实现【截取字符串】一文详解!

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: Java华为OD机试真题&#xff08;2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述…

PCA主成分分析 | 机器学习

1、概述(Principal componet analysis,PCA) 是一种无监督学习方法&#xff0c;是为了降低特征的维度。将原始高维数据转化为低维度的数据&#xff0c;高维数据指的是数据的特征维度较多&#xff0c;找到一个坐标系&#xff0c;使得这些数据特征映射到一个二维或三维的坐标系中…