基于Eureka搭建Springcloud微服务-12.使用Seata进行分布式事务控制

lingwh原创2020年6月14日大约 8 分钟约 2351 字

12.使用Seata进行分布式事务控制

12.1.章节内容概述

本章节涉及主要内容有:
 12.1.章节内容概述
 12.2.章节内容大纲
 12.3.Seata简介
 12.4.搭建Seata Server
 12.5.准备数据库环境
 12.6.搭建服务提供者Account服务(Seata)
 12.6.搭建服务提供者Storage服务(Seata)
 12.8.搭建服务消费者
 12.8.测试使用Seata进行分布式事务控制
 12.9.注意事项
具体每个小节中包含的内容可使通过下面的章节内容大纲进行查看。

12.2.章节内容大纲

12.3.Seata简介

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

官方网址

https://seata.io/zh-cn/

12.4.搭建Seata Server

在localhost上搭建Seata Server

详细参考-> 搭建Seate-Server(Windows版)

12.5.准备数据库环境

导入数据库脚本(application.yml中数据库配置和mysql部署机器信息保持一致)
DROP DATABASE IF EXISTS `payment`;
CREATE DATABASE `payment`;
USE `payment`;

DROP TABLE IF EXISTS `payment`;
CREATE TABLE `payment` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `serial` varchar(200) DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

LOCK TABLES `payment` WRITE;
INSERT INTO `payment` VALUES (1,'15646546546');
UNLOCK TABLES;

12.6.搭建服务提供者Account服务(Seata)

12.6.1.模块简介

具有分布式事务控制功能的服务提供者Account服务,启动端口: 8007

12.6.2.模块目录结构

@import "./projects/springcloud-provider-seata-account8007/tree.md"

12.6.3.创建模块

在父工程(springcloud-eureka)中创建一个名为springcloud-provider-seata-account8007的maven模块,注意:当前模块创建成功后,在父工程pom.xml中<modules></modules>中会自动生成有关当前模块的信息

12.6.4.编写模块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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-eureka</artifactId>
        <groupId>org.openatom</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-provider-seata-account8007</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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--引入公共的工程-->
        <dependency>
            <groupId>org.openatom</groupId>
            <artifactId>springcloud-api-commons</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--Apollo客户端-->
        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <!--是否依赖传递:true,依赖不传递,false:依赖传递,这是maven的特性-->
            <optional>true</optional>
        </dependency>
        <!--Apollo客户端-->
        <!-- seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>
        <!-- seata -->
    </dependencies>
    <!--热部署需要加这个-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
        <!--打包多环境-->
        <resources>
            <resource>
                <directory>src/main/resources/</directory>
                <includes>
                    <!--不区分环境:直接加载application.yml配置文件-->
                    <include>application.yml</include>
                    <!--不区分环境:直接加载mapper下*.xml配置文件-->
                    <include>mapper/*.xml</include>
                    <!--不区分环境:直接加载*.properties配置文件-->
                    <include>*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

12.6.5.编写模块application.yml

server:
  port: 8007
  tomcat:
    mbeanregistry:
      enabled: true
spring:
  application:
    name: SPRINGCLOUD-PROVIDER-SEATA-ACCOUNT8007 #注意:服务名不要出现_
  devtools:
    restart:
      enabled: true
  logging: #Spring运行日志配置
    level: info
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver             # mysql驱动包
    url: jdbc:mysql://192.168.0.5:3306/seata_account
    username: root
    password: Mysql123456_

eureka:
  client:
    register-with-eureka: true  #表示是否将自己注册进EurekaServer默认为true。
    fetchRegistry: true  #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #单机版
      defaultZone: http://localhost:7001/eureka
      #集群版
      #defaultZone: http://eureka7002:7002/eureka,http://eureka7003:7003/eureka,http://eureka7004:7004/eureka
  instance:
    instance-id: SPRINGCLOUD-PROVIDER-SEATA-ACCOUNT #Eureka仪表盘中Instances currently registered with Eureka.Status显示的内容
    prefer-ip-address: true  #访问路径可以显示IP地址,点击Eureka仪表盘中Instances currently registered with Eureka.Status显示的内容地址栏是否显示IP地址
    lease-renewal-interval-in-seconds: 30 #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    lease-expiration-duration-in-seconds: 90 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    restart:
      enabled: true

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: org.openatom.springcloud.entities    # 所有Entity别名类所在包

app:
  id: springcloud-eureka-seata
apollo:
  bootstrap:
    enabled: true
    namespaces: seata-account #多个namespaces之间使用,隔开

#所有服务信息:这是自定义的节点,和seata和项目无关
service:
  seata-server:
    name: seata-server
#所有服务信息:这是自定义的节点,和seata和项目无关
seata:
  enabled: true
  application-id: seata-account
  # 客户端和服务端在同一个事务组
  tx-service-group: my_test_tx_group
  # 事务群组,配置项值为TC集群名,需要与服务端在Eureka中注册时使用的应用名称保持一致
  service:
    vgroup-mapping.my_test_tx_group: ${service.seata-server.name}
  config:
    type: apollo
    apollo:
      seata: default
      cluster: default
      appId: ${app.id}
      apolloMeta: http://localhost:8080
      apolloConfigService: http://localhost:8080
      namespace: ${service.seata-server.name}
  registry:
    type: eureka
    eureka:
      serviceUrl: http://localhost:7001/eureka
      application: ${service.seata-server.name}
      weight: 1


12.6.6.编写Apollo配置文件

dev.meta=http://localhost:8080
pro.meta=http://localhost:8081

12.6.7.编写模块Mybatis配置文件

<?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="org.openatom.springcloud.dao.AccountDao">

    <resultMap id="BaseResultMap" type="org.openatom.springcloud.entities.Account">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="DECIMAL"/>
        <result column="used" property="used" jdbcType="DECIMAL"/>
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
    </resultMap>

    <update id="decrease">
        UPDATE t_account
        SET
          residue = residue - #{money},used = used + #{money}
        WHERE
          user_id = #{userId};
    </update>

</mapper>




12.6.8.编写模块dao

package org.openatom.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface AccountDao {

    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

12.6.9.编写模块service

package org.openatom.springcloud.service;

import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;


public interface AccountService {

    /**
     * 扣减账户余额
     * @param userId 用户id
     * @param money 金额
     */
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

12.6.10.编写模块service实现类

package org.openatom.springcloud.service.impl;


import lombok.extern.slf4j.Slf4j;
import org.openatom.springcloud.dao.AccountDao;
import org.openatom.springcloud.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

/**
 * 账户业务实现类
 */
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {

    @Resource
    AccountDao accountDao;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("------->account-service中扣减账户余额开始");
        //模拟超时异常,全局事务回滚
        int i = 10/0;
        //暂停几秒钟线程
//        try {
//            TimeUnit.SECONDS.sleep(20);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        accountDao.decrease(userId,money);
        log.info("------->account-service中扣减账户余额结束");
    }
}

12.6.11.编写模块listener

package org.openatom.springcloud.listener;

import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.restart.RestartEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@Slf4j
public class ApolloPropertiesChangedListener implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private RestartEndpoint restartEndpoint;

    /**
     * 注意,要监听非application命名空间的 配置文件变化时,要@ApolloConfigChangeListener说明时具体时是哪个命名空间
     * @param changeEvent
     */
    @ApolloConfigChangeListener("seata-account")
    private void someChangeHandler(ConfigChangeEvent changeEvent) {
        for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
//            log.info("Found change - {}", change.toString());
            //如果key符合特定情况,则重启应用程序
            isRestartApplication(change.getPropertyName());
        }
        // 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    }

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

    /**
     * 重启SpringBoot项目
     */
    /**
     * 重启SpringBoot项目
     */
    public void isRestartApplication(String propertyName){
        List<String> propertyNames = new ArrayList<>();
        /**
         * 重启逻辑1:修改了指定的key的值
         */
        propertyNames.add("spring.application.name");
        if(propertyNames.contains(propertyName)){
            restartEndpoint.restart();
        }
        /**
         * 重启逻辑2:key包含seata
         */
        if(propertyName.contains("seata")){
            restartEndpoint.restart();
        }
    }
}

12.6.12.编写模块config

package org.openatom.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;


/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceProxyConfig {

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

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

    @Bean
    public DataSource dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

12.6.13.编写模块controller

package org.openatom.springcloud.controller;

import org.openatom.springcloud.entities.CommonResult;
import org.openatom.springcloud.service.AccountService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.math.BigDecimal;

@RestController
public class AccountController {

    @Resource
    AccountService accountService;

    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        accountService.decrease(userId,money);
        return new CommonResult(200,"扣减账户余额成功!");
    }
}

12.6.14.编写模块主启动类

package org.openatom.springcloud;

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableApolloConfig
@EnableEurekaClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class AccountServiceProviderSeatal8007 {
    public static void main(String[] args) {
        /**
         * 注意:
         *  1.下面的启动参数要以seata-server中的registry.conf中config.apollo{}的配置为准
         *  2.这里的配置其实和yml中以及seata-server中的registry.conf中config.apollo{}的配置是一致的
         */
        System.setProperty("env","dev");
        System.setProperty("seata","default");
        System.setProperty("apollo.cluster","default");
        System.setProperty("seata.config.apollo.namespace","seata-server");
        System.setProperty("apolloConfigService","dafult");
        SpringApplication.run(AccountServiceProviderSeatal8007.class, args);
    }
}

12.6.搭建服务提供者Storage服务(Seata)

12.7.1.模块简介

具有分布式事务控制功能的服务提供者Storage服务,启动端口: 8008

12.7.2.模块目录结构

@import "./projects/springcloud-provider-seata-storage8008/tree.md"

12.7.3.创建模块

在父工程(springcloud-eureka)中创建一个名为springcloud-provider-seata-storage8008的maven模块,注意:当前模块创建成功后,在父工程pom.xml中<modules></modules>中会自动生成有关当前模块的信息

12.7.4.编写模块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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-eureka</artifactId>
        <groupId>org.openatom</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-provider-seata-storage8008</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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--引入公共的工程-->
        <dependency>
            <groupId>org.openatom</groupId>
            <artifactId>springcloud-api-commons</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--Apollo客户端-->
        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <!--是否依赖传递:true,依赖不传递,false:依赖传递,这是maven的特性-->
            <optional>true</optional>
        </dependency>
        <!-- seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <!--热部署需要加这个-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
        <!--打包多环境-->
        <resources>
            <resource>
                <directory>src/main/resources/</directory>
                <includes>
                    <!--不区分环境:直接加载application.yml配置文件-->
                    <include>application.yml</include>
                    <!--不区分环境:直接加载mapper下*.xml配置文件-->
                    <include>mapper/*.xml</include>
                    <!--不区分环境:直接加载*.properties配置文件-->
                    <include>*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

12.7.5.编写模块application.yml

server:
  port: 8008
  tomcat:
    mbeanregistry:
      enabled: true
spring:
  application:
    name: SPRINGCLOUD-PROVIDER-SEATA-STORAGE8008 #注意:服务名不要出现_
  devtools:
    restart:
      enabled: true
  logging: #Spring运行日志配置
    level: info
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver             # mysql驱动包
    url: jdbc:mysql://192.168.0.5:3306/seata_storage
    username: root
    password: Mysql123456_

eureka:
  client:
    register-with-eureka: true  #表示是否将自己注册进EurekaServer默认为true。
    fetchRegistry: true  #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #单机版
      defaultZone: http://localhost:7001/eureka
      #集群版
      #defaultZone: http://eureka7002:7002/eureka,http://eureka7003:7003/eureka,http://eureka7004:7004/eureka
  instance:
    instance-id: SPRINGCLOUD-PROVIDER-SEATA-STORAGE #Eureka仪表盘中Instances currently registered with Eureka.Status显示的内容
    prefer-ip-address: true  #访问路径可以显示IP地址,点击Eureka仪表盘中Instances currently registered with Eureka.Status显示的内容地址栏是否显示IP地址
    lease-renewal-interval-in-seconds: 30 #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    lease-expiration-duration-in-seconds: 90 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    restart:
      enabled: true

logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: org.openatom.springcloud.entities    # 所有Entity别名类所在包

app:
  id: springcloud-eureka-seata
apollo:
  bootstrap:
    enabled: true
    namespaces: seata-storage #多个namespaces之间使用,隔开

#所有服务信息:这是自定义的节点,和seata和项目无关
service:
  seata-server:
    name: seata-server
#所有服务信息:这是自定义的节点,和seata和项目无关
seata:
  enabled: true
  application-id: seata-storge
  # 客户端和服务端在同一个事务组
  tx-service-group: my_test_tx_group
  # 事务群组,配置项值为TC集群名,需要与服务端在Eureka中注册时使用的应用名称保持一致
  service:
    vgroup-mapping.my_test_tx_group: ${service.seata-server.name}
  config:
    type: apollo
    apollo:
      seata: default
      cluster: default
      appId: ${app.id}
      apolloMeta: http://localhost:8080
      apolloConfigService: http://localhost:8080
      namespace: ${service.seata-server.name}
  registry:
    type: eureka
    eureka:
      serviceUrl: http://localhost:7001/eureka
      application: ${service.seata-server.name}
      weight: 1

12.7.6.编写Apollo配置文件

dev.meta=http://localhost:8080
pro.meta=http://localhost:8081

12.7.7.编写模块Mybatis配置文件

<?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="org.openatom.springcloud.dao.StorageDao">

    <resultMap id="BaseResultMap" type="org.openatom.springcloud.entities.Storage">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="INTEGER"/>
        <result column="used" property="used" jdbcType="INTEGER"/>
        <result column="residue" property="residue" jdbcType="INTEGER"/>
    </resultMap>

    <update id="decrease">
        UPDATE
            t_storage
        SET
            used = used + #{count},residue = residue - #{count}
        WHERE
            product_id = #{productId}
    </update>

</mapper>




12.7.8.编写模块dao

package org.openatom.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface StorageDao {

    /**
     * 扣减库存
     * @param productId
     * @param count
     */
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

12.7.9.编写模块service

package org.openatom.springcloud.service;


public interface StorageService {
    /**
     * 扣减库存
     */
    void decrease(Long productId, Integer count);
}

12.7.10.编写模块service实现类

package org.openatom.springcloud.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.openatom.springcloud.dao.StorageDao;
import org.openatom.springcloud.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;


@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    @Resource
    private StorageDao storageDao;

    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣减库存开始");
        storageDao.decrease(productId,count);
        log.info("------->storage-service中扣减库存结束");
    }
}

12.7.11.编写模块listener

package org.openatom.springcloud.listener;

import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.restart.RestartEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@Slf4j
public class ApolloPropertiesChangedListener implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private RestartEndpoint restartEndpoint;


    /**
     * 注意,要监听非application命名空间的 配置文件变化时,要@ApolloConfigChangeListener说明时具体时是哪个命名空间
     * @param changeEvent
     */
    @ApolloConfigChangeListener("seata-storage")
    private void someChangeHandler(ConfigChangeEvent changeEvent) {
        for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
//            log.info("Found change - {}", change.toString());
            //如果key符合特定情况,则重启应用程序
            isRestartApplication(change.getPropertyName());
        }
        // 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    }

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

    /**
     * 重启SpringBoot项目
     */
    public void isRestartApplication(String propertyName){
        List<String> propertyNames = new ArrayList<>();
        /**
         * 重启逻辑1:修改了指定的key的值
         */
        propertyNames.add("spring.application.name");
        if(propertyNames.contains(propertyName)){
            restartEndpoint.restart();
        }
        /**
         * 重启逻辑2:key包含seata
         */
        if(propertyName.contains("seata")){
            restartEndpoint.restart();
        }
    }
}

12.7.12.编写模块config

package org.openatom.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;


/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceProxyConfig {

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

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

    @Bean
    public DataSource dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

12.7.13.编写模块controller

package org.openatom.springcloud.controller;


import org.openatom.springcloud.entities.CommonResult;
import org.openatom.springcloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StorageController {

    @Autowired
    private StorageService storageService;

    /**
     * 扣减库存
     */
    @RequestMapping("/storage/decrease")
    public CommonResult decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return new CommonResult(200,"扣减库存成功!");
    }
}

12.7.14.编写模块主启动类

package org.openatom.springcloud;

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;


@EnableApolloConfig
@EnableEurekaClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class StorageServiceProviderSeatal8008 {

    public static void main(String[] args) {
        /**
         * 注意:
         *  1.下面的启动参数要以seata-server中的registry.conf中config.apollo{}的配置为准
         *  2.这里的配置其实和yml中以及seata-server中的registry.conf中config.apollo{}的配置是一致的
          */
        System.setProperty("env","dev");
        System.setProperty("seata","default");
        System.setProperty("apollo.cluster","default");
        System.setProperty("seata.config.apollo.namespace","seata-server");
        System.setProperty("apolloConfigService","dafult");
        SpringApplication.run(StorageServiceProviderSeatal8008.class, args);
    }
}

12.8.搭建服务消费者

12.8.1.模块简介

具有分布式事务控制功能的服务消费者Order服务,启动端口: 80

12.8.2.模块目录结构

@import "./projects/springcloud-consumer-seata-loadbalance-openfeign-configuration-order80/tree.md"

12.8.3.创建模块

在父工程(springcloud-eureka)中创建一个名为springcloud-consumer-seata-loadbalance-openfeign-configuration-order80的maven模块,注意:当前模块创建成功后,在父工程pom.xml中<modules></modules>中会自动生成有关当前模块的信息

12.8.4.编写模块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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-eureka</artifactId>
        <groupId>org.openatom</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-consumer-seata-loadbalance-openfeign-configuration-order80</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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--引入公共的工程-->
        <dependency>
            <groupId>org.openatom</groupId>
            <artifactId>springcloud-api-commons</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--Apollo客户端-->
        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <!--是否依赖传递:true,依赖不传递,false:依赖传递,这是maven的特性-->
            <optional>true</optional>
        </dependency>
        <!-- seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <!--热部署需要加这个-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
        <!--打包多环境-->
        <resources>
            <resource>
                <directory>src/main/resources/</directory>
                <includes>
                    <!--不区分环境:直接加载application.yml配置文件-->
                    <include>application.yml</include>
                    <!--不区分环境:直接加载mapper下*.xml配置文件-->
                    <include>mapper/*.xml</include>
                    <!--不区分环境:直接加载*.properties配置文件-->
                    <include>*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

12.8.5.编写模块application.yml

server:
  port: 80
  tomcat:
    mbeanregistry:
      enabled: true

spring:
  application:
    name: SPRINGCLOUD-CONSUMER-SEATA-LOADBALANCE-OPENFEIGN-CONFIGURATION-ORDER80 #注意:服务名不要出现_
  devtools:
    restart:
      enabled: true
  logging: #Spring运行日志配置
    level: info
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver             # mysql驱动包
    url: jdbc:mysql://192.168.0.5:3306/seata_order
    username: root
    password: Mysql123456_

eureka:
  client:
    register-with-eureka: true  #表示是否将自己注册进EurekaServer默认为true。
    fetchRegistry: true  #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #单机版
      defaultZone: http://localhost:7001/eureka
      #集群版
      #defaultZone: http://eureka7002:7002/eureka,http://eureka7003:7003/eureka,http://eureka7004:7004/eureka
  instance:
    instance-id: SPRINGCLOUD-CONSUMER-SEATA-LOADBALANCE-OPENFEIGN-CONFIGURATION-ORDER #Eureka仪表盘中Instances currently registered with Eureka.Status显示的内容
    prefer-ip-address: true  #访问路径可以显示IP地址,点击Eureka仪表盘中Instances currently registered with Eureka.Status显示的内容地址栏是否显示IP地址
    lease-renewal-interval-in-seconds: 30 #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    lease-expiration-duration-in-seconds: 90 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    restart:
      enabled: true
#对OpenFeign进行单独配置
feign:
  client:
    config:
      default:
        #connectTimeout和readTimeout这两个得一起配置才会生效
        connectTimeout: 5000  #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
        readTimeout: 5000   #指的是建立连接后从服务器读取到可用资源所用的时间

ribbon:
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule  #Ribbon负载均衡规则类所在的路径,自带七种规则,也可以是自定位规则的类所在的路径

logging: #OpenFeign增强日志配置
  level:
    org.openatom.springcloud.services.AccountService: debug  #OpenFeign日志以什么级别监控哪个接口
    org.openatom.springcloud.services.StorageService: debug  #OpenFeign日志以什么级别监控哪个接口
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: org.openatom.springcloud.entities    # 所有Entity别名类所在包

app:
  id: springcloud-eureka-seata
apollo:
  bootstrap:
    enabled: true
    namespaces: seata-order #多个namespaces之间使用,隔开

#所有服务信息:这是自定义的节点,和seata和项目无关
service:
  seata-server:
    name: seata-server
#所有服务信息:这是自定义的节点,和seata和项目无关
seata:
  enabled: true
  application-id: seata-order
  # 客户端和服务端在同一个事务组
  tx-service-group: my_test_tx_group
  # 事务群组,配置项值为TC集群名,需要与服务端在Eureka中注册时使用的应用名称保持一致
  service:
    vgroup-mapping.my_test_tx_group: ${service.seata-server.name}
  config:
    type: apollo
    apollo:
      seata: default
      cluster: default
      appId: ${app.id}
      apolloMeta: http://localhost:8080
      apolloConfigService: http://localhost:8080
      namespace: ${service.seata-server.name}
  registry:
    type: eureka
    eureka:
      serviceUrl: http://localhost:7001/eureka
      application: ${service.seata-server.name}
      weight: 1

12.8.6.编写Apollo配置文件

dev.meta=http://localhost:8080
pro.meta=http://localhost:8081

12.8.7.编写模块Mybatis配置文件

<?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="org.openatom.springcloud.dao.OrderDao">

    <resultMap id="BaseResultMap" type="org.openatom.springcloud.entities.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="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>

    <insert id="create">
        insert into t_order (id,user_id,product_id,count,money,status)
        values (null,#{userId},#{productId},#{count},#{money},0);
    </insert>


    <update id="update">
        update t_order set status = 1
        where user_id=#{userId} and status = #{status};
    </update>

</mapper>



12.8.8.编写模块dao

package org.openatom.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.openatom.springcloud.entities.Order;

@Mapper
public interface OrderDao {

    /**
     * 新建订单
     * @param order
     */
    void create(Order order);

    /**
     * 修改订单状态,从0改为1
     * @param userId
     * @param status
     */
    void update(@Param("userId") Long userId,@Param("status") Integer status);
}

12.8.9.编写模块service

AccountService.java
package org.openatom.springcloud.service;

import org.openatom.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "SPRINGCLOUD-PROVIDER-SEATA-ACCOUNT8007")
public interface AccountService {

    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

OrderService.java
package org.openatom.springcloud.service;


import org.openatom.springcloud.entities.Order;

public interface OrderService {

    void create(Order order);
}

StorageService.java
package org.openatom.springcloud.service;

import org.openatom.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;


@FeignClient(value = "SPRINGCLOUD-PROVIDER-SEATA-STORAGE8008")
public interface StorageService {

    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

12.8.10.编写模块service实现类

package org.openatom.springcloud.service.impl;

import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.openatom.springcloud.dao.OrderDao;
import org.openatom.springcloud.entities.Order;
import org.openatom.springcloud.service.AccountService;
import org.openatom.springcloud.service.OrderService;
import org.openatom.springcloud.service.StorageService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改状态
     */
    @Override
    @GlobalTransactional
    public void create(Order order) {

        log.info("----->开始下订单");
        //1 新建订单
        orderDao.create(order);

        //2 扣减库存
        log.info("----->订单微服务开始调用库存,做扣减Count");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("----->订单微服务开始调用库存,做扣减end");

        //3 扣减账户
        log.info("----->订单微服务开始调用账户,做扣减Money");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("----->订单微服务开始调用账户,做扣减end");

        //4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        log.info("----->修改订单状态结束");

        log.info("----->完成下订单");

    }
}

12.8.11.编写模块listener

package org.openatom.springcloud.listener;

import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.restart.RestartEndpoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@Slf4j
public class ApolloPropertiesChangedListener implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private RestartEndpoint restartEndpoint;

    /**
     * 注意,要监听非application命名空间的 配置文件变化时,要@ApolloConfigChangeListener说明时具体时是哪个命名空间
     * @param changeEvent
     */
    @ApolloConfigChangeListener("seata-order")
    private void someChangeHandler(ConfigChangeEvent changeEvent) {
        for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
//            log.info("Found change - {}", change.toString());
            //如果key符合特定情况,则重启应用程序
            isRestartApplication(change.getPropertyName());
        }
        // 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    }

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

    /**
     * 重启SpringBoot项目
     */
    /**
     * 重启SpringBoot项目
     */
    public void isRestartApplication(String propertyName){
        List<String> propertyNames = new ArrayList<>();
        /**
         * 重启逻辑1:修改了指定的key的值
         */
        propertyNames.add("spring.application.name");
        if(propertyNames.contains(propertyName)){
            restartEndpoint.restart();
        }
        /**
         * 重启逻辑2:key包含seata
         */
        if(propertyName.contains("seata")){
            restartEndpoint.restart();
        }
    }
}

12.8.12.编写模块config

DataSourceProxyConfig.java
package org.openatom.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;


/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceProxyConfig {

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

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

    @Bean
    public DataSource dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

FeignConfig.java
package org.openatom.springcloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class FeignConfig {
    /**
     * NONE:默认的,不显示任何日志;
     * BASIC:仅记录请求方法、URL、响应状态码及执行时间;
     * HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
     * FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

12.8.13.编写模块controller

package org.openatom.springcloud.controller;

import org.openatom.springcloud.entities.CommonResult;
import org.openatom.springcloud.entities.Order;
import org.openatom.springcloud.service.OrderService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;
    /**
     * 测试地址:
     *      http://localhost/order/create?userId=1&productId=1&count=10&money=100
     * @param order
     * @return
     */
    @GetMapping("/order/create")
    public CommonResult create(Order order) {
        orderService.create(order);
        return new CommonResult(200,"订单创建成功");
    }
}

12.8.14.编写模块主启动类

package org.openatom.springcloud;

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 1.使用OpenFeign完成远程调用,如果要配置负载均衡策略,和Ribbon配置负载均衡策略方式相同
 *      本微服务主要测试OpenFeign的功能,所以采用YML文件配置Ribbon的负载均衡策略
 * 2.OpenFeign是对Ribbon和RestTemplate的封装,所以配置负载均衡方式同Ribbon配置负载均衡方式,而且不需要在容器中手动注入ResTemplate对象
 * 3.OpenFeign YML文件配置实现远程调用,但不是完全将服务信息配置在YML中,只是在YML中写一些增强的配置,相关的服务中仍然要写服务名,@FeignClient(name="SPRING-CLOUD-PROVIDER-CONSUL-PAYMENT-SERVICE")
 * 4.对每个微服务单独进行配置,如连接超时时间配置、读取超时时间配置,YML没有把OpenFegin的配置和对Ribbon的配置写在一起
 * 5.开启OpenFeign增强日志后可以看到Http调用的详细信息
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] <--- HTTP/1.1 200 (59ms)
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] connection: keep-alive
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] content-type: application/json
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] date: Tue, 31 May 2022 19:51:37 GMT
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] keep-alive: timeout=60
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] transfer-encoding: chunked
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById]
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] {"code":200,"message":"查询成功,serverPort:  8006","data":{"id":1,"serial":"15646546546"}}
 *      2022-06-01 03:51:37.176 DEBUG 16792 --- [p-nio-80-exec-1] o.o.s.services.PaymentServiceOpenFeign   : [PaymentServiceOpenFeign#getPaymentById] <--- END HTTP (94-byte body)
 */
@EnableApolloConfig
@EnableEurekaClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
@EnableFeignClients
public class OrderServiceConsumerSeatalLoadBalanceOpenFeignConfiguration80 {
    public static void main(String[] args) {
        /**
         * 注意:
         *  1.下面的启动参数要以seata-server中的registry.conf中config.apollo{}的配置为准
         *  2.这里的配置其实和yml中以及seata-server中的registry.conf中config.apollo{}的配置是一致的
         */
        System.setProperty("env","dev");
        System.setProperty("seata","default");
        System.setProperty("apollo.cluster","default");
        System.setProperty("seata.config.apollo.namespace","seata-server");
        System.setProperty("apolloConfigService","dafult");
        SpringApplication.run(OrderServiceConsumerSeatalLoadBalanceOpenFeignConfiguration80.class, args);
    }
}

12.8.测试使用Seata进行分布式事务控制

启动相关服务
在seate-server控制台查看,三个服务已经被成功注册
测试使用Seata控制实现分布式事务回滚
调用接口前查看数据库中数据
a.t_account表
mysql> SELECT * FROM seata_account.t_account;
+----+---------+-------+------+---------+
| id | user_id | total | used | residue |
+----+---------+-------+------+---------+
|  1 |       1 |  1000 |    0 |    1000 |
+----+---------+-------+------+---------+
1 row in set (0.00 sec)

b.t_storage表
mysql> SELECT * FROM seata_storage.t_storage;
+----+------------+-------+------+---------+
| id | product_id | total | used | residue |
+----+------------+-------+------+---------+
|  1 |          1 |   100 |    0 |     100 |
+----+------------+-------+------+---------+
1 row in set (0.00 sec)

c.t_order表
mysql> SELECT * FROM seata_order.t_order;
Empty set (0.00 sec)

在浏览器访问引发异常的接口
http://localhost/order/create?userId=1&productId=1&count=10&money=100
由于在调用Account服务时会报异常,浏览器页面会直接报错,seata会自动进行回滚

调用接口前查看数据库中数据
a.t_account表
mysql> SELECT * FROM seata_account.t_account;
+----+---------+-------+------+---------+
| id | user_id | total | used | residue |
+----+---------+-------+------+---------+
|  1 |       1 |  1000 |    0 |    1000 |
+----+---------+-------+------+---------+
1 row in set (0.00 sec)

b.t_storage表
mysql> SELECT * FROM seata_storage.t_storage;
+----+------------+-------+------+---------+
| id | product_id | total | used | residue |
+----+------------+-------+------+---------+
|  1 |          1 |   100 |    0 |     100 |
+----+------------+-------+------+---------+
1 row in set (0.00 sec)

c.t_order表
mysql> SELECT * FROM seata_order.t_order;
Empty set (0.00 sec)

如果想要更明显的查看Seata在项目中起的作用,可使用如下方式
a.关闭seata-server,在浏览器中访问服务,再去数据库中查看,可以发现表中的数据发生了改变
b.在调用的时候打端点,可以观察到表中的数据会先发生变化,放开断点后,又会因为发生异常触发回滚导致表中的数据恢复到初始状态

12.9.注意事项

在这个案例中,三个服务和seata-server在Apollo注册中接入在同一个项目中,依靠namespace的值区分三个不同服务和seata-server,这样就可以让三个不同的服务和seata-server同时使用apollo,因为application.yml中app.id这个配置项只能配置一个值,如果不这样处理,三个服务只能使用seata进行分布式事务控制,并不能使用apollo管理配置
上次编辑于: 2022/9/9 06:18:47
贡献者: lingwh
评论