基于Eureka搭建Springcloud微服务-12.使用Seata进行分布式事务控制
原创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管理配置
评论