【Seata】SpringBoot集成Seata1.6-AT模式

目录

开始语

😎项目目录结构如下

🖋️数据库执行脚本

🖋️pom.xml文件

🖋️项目所需配置文件application.yml如下

🖋️nacos中配置

🖋️common模块 

🖋️服务调用Orders 请求 Inventory代码片段

😎源码地址 

结束语


开始语

一位普通的程序员,慢慢在努力变强!

没有安装seata的同学点击此处前往👈

没有安装nacos的同学点击此处前往👈

😎项目目录结构如下

🖋️数据库执行脚本

-- 数据库
CREATE DATABASE spring-boot-seata DEFAULT CHARACTER SET utf8mb4;

-- `spring-boot-seata`.inventory definition
CREATE TABLE `inventory` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`buss_name` varchar(100) DEFAULT NULL COMMENT '库存名称',
`number` bigint(20) DEFAULT NULL COMMENT '库存量',
`create_user` varchar(50) DEFAULT ' ' COMMENT '创建人',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` varchar(50) DEFAULT ' ' COMMENT '修改人',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效(0.不可用,1.可用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';

-- `spring-boot-seata`.orders definition

CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_name` varchar(100) DEFAULT NULL COMMENT '订单名称',
`amount` decimal(12,4) DEFAULT NULL COMMENT '订单金额',
`number` bigint(20) DEFAULT NULL COMMENT '订单数',
`create_user` varchar(50) DEFAULT ' ' COMMENT '创建人',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` varchar(50) DEFAULT ' ' COMMENT '修改人',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效(0.不可用,1.可用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log,如果你是多个数据库,那么在对应的库中需要添加这张表
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
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

🖋️pom.xml文件

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <packaging>pom</packaging>

    <groupId>com.net</groupId>
    <artifactId>springboot-seata</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-seata</name>
    <description>分布式事务的学习</description>

    <modules>
        <module>orders</module>
        <module>inventory</module>
        <module>common</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
        <springboot.version>2.3.12.RELEASE</springboot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>nacos-config-spring-boot-starter</artifactId>
            <version>0.2.7</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${springboot.version}</version>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>

            <!--maven编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

            <!-- maven 打包时跳过测试 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>

        <resources>
            <!--编译||打包排除和加载设置 install-->
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <!-- 先排除所有环境相关的配置文件 -->
                <excludes>
                    <!--<exclude>application*.yml</exclude>-->
                </excludes>
            </resource>
            <resource>
                <!--打包编译项目默认编译加载的文件,例如一些txt,xml...等文件-->
                <directory>src/main/resources</directory>
                <!--提供@配置@在yml文件中支持读取xml文件配置-->
                <filtering>true</filtering>
                <includes>
                    <include>banner.txt</include>
                    <include>bootstrap.yml</include>
                    <include>application.properties</include>
                    <include>application.yml</include>
                </includes>
            </resource>
        </resources>
    </build>



    <!--环境切换-->
    <profiles>
        <profile>
            <!--开发环境-->
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <project.active>dev</project.active>
                <nacos.addr>localhost:8848</nacos.addr>
                <nacos.service>localhost:8091</nacos.service>
            </properties>
        </profile>
        <profile>
            <!--测试环境-->
            <id>test</id>
            <properties>
                <project.active>test</project.active>
            </properties>
        </profile>
        <profile>
            <!--验收环境,仅次于prod生产环境-->
            <id>beta</id>
            <properties>
                <project.active>beta</project.active>
            </properties>
        </profile>
    </profiles>

</project>

🖋️项目所需配置文件application.yml如下

温馨提示:inventory举例(orders同理),后面我会发出demo的git地址

server:
  port: 8091

spring:
  application:
    name: @project.name@
  profiles:
    # 如果不生效请刷新maven依赖
    active: @project.active@

# start------------------------------这块配置应该放入nacos中,这边方便演示,所以就没有放入nacos中
seata:
  config:
    type: nacos
    nacos:
      server-addr: @nacos.addr@
      namespace: seata-server
      group: SEATA_GROUP
      username: nacos
      password: Getring0817#.
  registry:
    type: nacos
    nacos:
      server-addr: @nacos.addr@
      namespace: seata-server
      application: seata-server
      group: SEATA_GROUP
      username: nacos
      password: Getring0817#.
  service:
    vgroup-mapping:
      my_test_tx_group: default
    disable-global-transaction: false
    grouplist:
      default: @nacos.service@
# end------------------------------这块配置应该放入nacos中,这边方便演示,所以就没有放入nacos中

# nacos服务配置
nacos:
  config:
    server-addr: @nacos.addr@
    namespace: ${spring.profiles.active}
    username: nacos
    password: Getring0817#.
    type: yaml
    bootstrap:
      enable: true
      log-enable: true
    auto-refresh: true
    enable-remote-sync-config: true
    data-ids: ${spring.application.name}-server.yaml,${spring.application.name}-db.yaml

logging:
  level:
    # 解决注册中心跳机制的日志问题
    com.alibaba.nacos.client.config.impl: warn

🖋️nacos中配置

 

# 放置inventory-db.yaml文件配置中
spring:
  #----------------------------mysql服务配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-boot-seata?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
    username: root
    password: root_123
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 10
      min-idle: 10
      maxActive: 200
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      connectionErrorRetryAttempts: 3
      breakAfterAcquireFailure: true
      timeBetweenConnectErrorMillis: 300000
      asyncInit: true
      remove-abandoned: false
      remove-abandoned-timeout: 1800
      transaction-query-timeout: 6000
      filters: stat,wall,log4j2
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      stat-view-servlet:
        url-pattern: "/druid/*"
        allow:
        deny:
        reset-enable: false
        login-username: admin
        login-password: admin

mybatis-plus:
  global-config:
    db-config:
      # 逻辑已删除值(默认为 1)
      logic-delete-value: 1
      # 逻辑未删除值(默认为 0)
      logic-not-delete-value: 0
  mapper-locations: classpath*:**/mapper/xml/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 驼峰,_映射 app_name = appName
    map-underscore-to-camel-case: true

🖋️common模块 

package com.net.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }

}
package com.net.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * spring上下文帮助类
 *
 * @author tianyu.Ge
 * @date 2023/3/3 9:19
 */
@Component
public class SpringApplicationContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringApplicationContextUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static <T> T getBean(String beanName) {
        return (T) applicationContext.getBean(beanName);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static boolean containsBean(String beanName) {
        return applicationContext.containsBean(beanName);
    }

}

package com.net.utils;

import io.seata.core.context.RootContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

/**
 * Htttp工具类
 *
 * @author tianyu.Ge
 * @date 2023/3/3 9:17
 */
public class HTTPUtil {

    public static final RestTemplate HTTP_UTIL() {
        RestTemplate restTemplate = SpringApplicationContextUtil.getBean(RestTemplate.class);
        return restTemplate;
    }

    /**
     * seata需要分布式事务专属方法
     *
     * @param url    请求路径
     * @param resultClass 返回类型
     * @return T
     * @author tianyu.Ge
     * @date 2023/3/3 9:29
     */
    public static <T> T get(String url, Class<T> resultClass) {
        HttpHeaders headers = new HttpHeaders();
        // 添加一些head设置,比如token等
        HttpEntity httpEntity = new HttpEntity(headers);
        ResponseEntity<T> exchange = HTTP_UTIL().exchange(url, HttpMethod.GET, httpEntity, resultClass);
        return exchange.getBody();
    }

    /**
     * seata需要分布式事务专属方法
     *
     * @param url    请求路径
     * @param params 请求参数
     * @param resultClass 返回类型
     * @return T
     * @author tianyu.Ge
     * @date 2023/3/3 9:29
     */
    public static <T> T seataGetFrom(String url, Map<String, Object> params, Class<T> resultClass) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.add(RootContext.KEY_XID, RootContext.getXID());
        StringBuffer stringBuffer = new StringBuffer(url + "?");
        if (params != null && params.size() > 0) {
            params.forEach((k, v) -> {
                stringBuffer.append(String.format("&%s=%s", k, v));
            });
        }
        HttpEntity<String> httpEntity = new HttpEntity(headers);
        ResponseEntity<T> exchange = HTTP_UTIL().exchange(stringBuffer.toString(), HttpMethod.GET, httpEntity, resultClass);
        return exchange.getBody();
    }

}

🖋️服务调用Orders 请求 Inventory代码片段

-------------------Orders服务service模块代码-------------------

package com.net.module.service.impl;

import com.net.module.entity.Orders;
import com.net.module.mapper.OrdersMapper;
import com.net.module.service.IOrdersService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.net.utils.HTTPUtil;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;

import java.util.HashMap;

/**
 * <p>
 * 订单表 服务实现类
 * </p>
 *
 * @author 猿仁
 * @since 2023-03-02
 */
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersService {

    /**
     * 下单
     * 模拟事务场景,实际业务和表设计并不是如此
     *
     * @param orders
     * @param inventoryId 库存id
     * @return
     * @author tianyu.Ge
     * @date 2023/3/2 16:50
     */
    @GlobalTransactional
    @Override
    public boolean placeAnOrder(Long inventoryId, Orders orders) {
        System.out.println("account XID " + RootContext.getXID());
        if (orders.getNumber() == null || orders.getNumber() != 1) {
            throw new RuntimeException("订单数量只能选1个!");
        }
        // 新增订单
        boolean save = save(orders);
        // 调用库存-1接口(将XID传递给下一个接口,或者其他服务,让对应的服务读取Header请求头XID被全局事务进行管理)
        Boolean boyd = HTTPUtil.seataGetFrom("http://127.0.0.1:8091/api/inventory/inventorySubOne/" + inventoryId, new HashMap<>() {{
            put("message", "请检查是否异常!");
        }}, Boolean.class);
        // 场景1,库存失败
        if (!boyd) {
            throw new RuntimeException("库存扣除失败,请查看详情!");
        } else {
            if (boyd) {
                // 场景2,库存成功,业务处理异常...
                throw new RuntimeException("订单业务处理繁忙,请稍后再试!");
            }
        }
        return save;
    }
}
-------------------Inventory服务service模块代码-------------------

package com.net.module.service.impl;

import com.net.module.entity.Inventory;
import com.net.module.mapper.InventoryMapper;
import com.net.module.service.IInventoryService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 库存表 服务实现类
 * </p>
 *
 * @author 猿仁
 * @since 2023-03-02
 */
@Slf4j
@Service
public class InventoryServiceImpl extends ServiceImpl<InventoryMapper, Inventory> implements IInventoryService {

    @Autowired
    private InventoryMapper inventoryMapper;

    /**
     * 库存减一
     *
     * @param id 库存id
     * @return boolean
     * @author tianyu.Ge
     * @date 2023/3/2 17:00
     */
    @Transactional
    @Override
    public boolean inventorySubOne(Long id) {
        Inventory one = lambdaQuery().eq(Inventory::getId, id).one();
        if (null == one) {
            log.error("没有库存id为:" + id + "的数据!");
            return false;
        }
        Long number = one.getNumber();
        if (number == 0) {
            log.error("当前库存为0,请检查订单对应的商品正确性,期望下架,支付金额返回支付账户!");
            return false;
        }
        boolean update = lambdaUpdate().set(Inventory::getNumber, number - 1).eq(Inventory::getId, id).update();
        return update;
    }

    /**
     * 根据库存名称查询库存信息
     *
     * @param bussName
     * @return com.net.module.entity.Inventory
     * @author tianyu.Ge
     * @date 2023/3/3 8:33
     */
    @Override
    public Inventory getByBussName(String bussName) {
        return inventoryMapper.getByBussName(bussName);
    }
}

😎源码地址 

>>>>>>>>>>>>>>>代码仓库地址👈https://gitee.com/gtring/springboot-seata.git

结束语

本章节完成了,各位正在努力的程序员们,如果你们觉得本文章对您有用的话,你学到了一些东西,希望猿友们点个赞+关注,支持一下猿仁!
持续更新中…

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿仁

多一分支持,多一分动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值