(五)Spring Cloud Alibaba Seata(含案例源码、案例解析及软件)

案例源码gitee地址:https://gitee.com/BanSheng/spring-cloud-alibaba-examples/tree/master/seata-examples

Spring Cloud Alibaba Seata

 

一、Seata 简介

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双 11,对各 BU 业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

二、Seata-Server 的安装

在使用 Seata 之前,我们首先要安装 Seata-Server 服务器。

2.1  下载 Seata

由于我们使用的 spring cloud alibaba 版本为 2.2.0.RELEASE,他里面控制了 seata 的版本为1.0.0,故我们在此下载 1.0.0 版本的 seata。访问:https://github.com/seata/seata/releases/tag/v1.0.0

由于我使用的是 windows 的电脑,故选择 seata-server-1.0.0.zip 该版本。

点击该文件下载。

当下载速度较慢时,大家可以从百度云里面去下载。

链接:https://pan.baidu.com/s/1w4aMASZ3AeREbJSUR5se0Q 提取码:o041 
 

2.2 Seata-Server  目录分析

将 seata-server 复制到软件的目录里面,使用解压工具解压该文件。

Bin:可执行文件目录

Conf:配置文件目录

lib:依赖的 jar

LICENSE:授权文件

2.3 Seata  启动

进入{seata}/bin 目录里面,双击:

代表 seata-server 已经启动成功。

三、框架的搭建

在本示例中,我们模拟了一个用户购买货物的场景:

  • StorageService 负责扣减库存数量;
  • OrderService 负责保存订单;
  •  AccountService 负责扣减用户账户余额;
  • Business 负责用户下单的整个流程处理;

3.1  搭建 seata-examples  项目

seata-examples 用来控制所有项目的依赖版本号,以及去除公共的依赖 seata。

3.1.1 使用 IDEA 创建 Maven 项目

选择 Maven 项目:

点击 Next,添加以下的信息:

Parent:选择 spring-cloud-alibaba-examples

Name:seata-examples

其他的信息保持默认即可。然后点击 Finish即可完成创建。

3.1.2 添加依赖

打开项目的 pom.xml 文件,添加以下的依赖。

<dependencies>
    <!-- 服务注册 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- seata-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!-- web 项目的基础依赖 -->
    <dependency>
            <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.1.3 完整的 pom.xml

<?xmlversion="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>spring-cloud-alibaba-examples</artifactId>
        <groupId>com.bjsxt</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>seata-examples</artifactId>
    <dependencies>
        <!-- 服务注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!-- web 项目的基础依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

最后,我们的项目的依赖关系如下:

可以看见,我们的 seata 版本为 1.0.0 。

3.2  搭建 account-service  项目

Account-service 项目将负责扣减用户账户余额

3.2.1 使用 IDEA 创建一个 Maven 项目

选择 Maven 项目:

 

点击 Next 后,填写以下的信息:

Parent:seata-examples

Name:account-service

其他的值保持默认即可。

3.2.2 添加依赖

我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。

打开 pom.xml ,在里面添加如下内容:

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!--MySQL 依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3.2.3 完整的 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>seata-examples</artifactId>
        <groupId>com.bjsxt</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>account-service</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3</version>
        </dependency>
    <!--MySQL 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

项目的依赖关系如下:

3.3  搭建 business-service  项目

在 business 将主要完成下单逻辑,包含库存的扣减,订单的创建。

3.3.1 使用 IDEA 创建一个 Maven 项目

选择 Maven 项目:

点击 Next 后,填写以下的信息:

Parent:seata-examples

Name:business-service

其他的值保持默认即可。

3.3.2 添加依赖

为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

 

3.2.3 完整的 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>seata-examples</artifactId>
        <groupId>com.bjsxt</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>account-service</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3</version>
        </dependency>
        <!--MySQL 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

项目的依赖关系如下:

3.4  搭建 order-service  项目

order-service 项目将负责保存用户订单。

3.4.1 使用 IDEA 创建一个 Maven 项目

选择 Maven 项目:

点击 Next 后,填写以下的信息:

Parent:seata-examples

Name:order-service

其他的值保持默认即可。

3.4.2 添加依赖

我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。

打开 pom.xml ,在里面添加如下内容:

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!--MySQL 依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        </dependency>
</dependencies>

为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3.4.3 完整的 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>seata-examples</artifactId>
        <groupId>com.bjsxt</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>order-service</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3</version>
        </dependency>
        <!--MySQL 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

项目的依赖关系如下:

3.5  搭建 storage-service  项目

storage-service 将负责扣除商品的库存。

3.5.1 使用 IDEA 创建一个 Maven 项目

选择 Maven 项目:

点击 Next 后,填写以下的信息:

Parent:seata-examples

Name:storage-service

其他的值保持默认即可。

3.5.2 添加依赖

我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。

打开 pom.xml ,在里面添加如下内容:

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!--MySQL 依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3.5.3 完整的 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>seata-examples</artifactId>
        <groupId>com.bjsxt</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>order-service</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3</version>
        </dependency>
        <!--MySQL 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

项目的依赖关系如下:

3.6  完整的项目的案例为

四、代码的完善

4.1  数据库表导入

 

       sql文件在gitee源代码里下载

       案例源码gitee地址:https://gitee.com/BanSheng/spring-cloud-alibaba-examples/tree/master/seata-examples

在测试分布式事务之前,我们需要先设计数据库,以及准备测试数据。

新建数据库,命名为:seata

导入 Sql:

Sql 文件在今天的软件文件夹里面:

导入该 sql:

点击开始,进行导入。

成功后,发现成功的条数为:

表有如下:

account:用户的账号表

Order:订单表;

Stoage:商品的库存表;

undo_log:回滚事务表,SEATA AT 模式需要 UNDO_LOG 表。

4.2  模型对象和 Mapper  对象生成

使用 IDEA 连接数据库:

成功后,如图所示:

执行代码的生成:

提示:若大家没有安装 mybatis 的代码生成插件,可以在今天的软件文件夹里面下载安装。

  • Account_tbl:

  • Order_tbl:

  • Storage_tbl:

代码生成完毕后:

4.3 storage-service  代码的完善

4.3.1 接口设计

在 storage-service 里面,主要完成对库存的扣减。

新建一个接口:

命名为:StorageService,代码如下:

代码如下:

public interface StorageService {
    /**
    * 扣减商品的库存
    *@param commodityCode
    * 商品的编码
    *@param count
    * 扣减商品的数量
    */
    void deduct(String commodityCode, int count);
}

4.3.2 实现该接口

名称为:impl.StorageService,代码的实现如下:

@Service
public class StorageServiceImpl implements StorageService {
    private static Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);
    @Autowired
    private StorageTblMapper storageTblMapper;
    @Override
    public void deduct(String commodityCode, int count) {
        logger.info("开始扣减库存,商品编码:{},数量:{}",commodityCode, count);
        StorageTbl storageTbl = storageTblMapper.selectOne(new LambdaQueryWrapper<StorageTbl>().eq(StorageTbl::getCommodityCode, commodityCode));
        int idleCount = storageTbl.getCount() - count;
        if (idleCount < 0) {
            throw new RuntimeException("库存不足");
        }
        storageTbl.setCount(idleCount);
        storageTblMapper.updateById(storageTbl);
        logger.info("库存扣减成功,商品编码:{},剩余数量:{}", commodityCode, idleCount);
    }
}

4.3.3 使用 Restful 暴露此接口

添加一个 Controller

代码如下:

@RestController
public class StorageController {
    private static Logger logger = LoggerFactory.getLogger(StorageController.class) ;
    @Autowired
    private StorageService storageService ;
    /**
    * 扣减商品的库存
    * @param commodityCode 商品的编码
    * @param count 商品的数量
    * @return
    */
    @GetMapping("/deduct/{commodityCode}/{count}")
    public ResponseEntity<Void> deduct(@PathVariable("commodityCode") String commodityCode,@PathVariable("count") Integer count){
        logger.info("Account Service ... xid: " + RootContext.getXID());
        // 开始扣减库存
        storageService.deduct(commodityCode , count);
        return ResponseEntity.ok().build() ;
    }
}

4.3.4 添加配置文件

在 resource 目录里面新建配置文件:

内容如下:

server:
  port: 18084
spring:
  application:
    name: storage-service
  cloud:
    alibaba:
      seata:
        tx-service-group: storage-service
    nacos:
      discovery:
        server-addr:localhost:8848
  datasource:
    name: storageDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
    druid:
      max-active: 20
      min-idle:2
      initial-size: 2
seata:
  service:
    vgroup-mapping:
      account-service: default
    grouplist:
      default: 127.0.0.1:8091
    disable-global-transaction: false
    enabled: true
mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml

4.3.5 添加启动类

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class StorageServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(StorageServiceApplication.class ,args) ;
    }
}

4.3.6 启动项目测试

启动项目后,打印该日志,说明连接 seata-server 成功。

4.4 account-service  代码的完善

4.4.1 接口设计

在 account-service 里面,主要完成对用户余扣减。

新建一个接口:

命名为:AccountService,代码如下:

代码如下:

public interface AccountService {
    /**
    * 从用户的账号扣减金额
    *@param userId
    * 用户的 Id
    *@param money
    * 金额
    */
    void debit(String userId, int money);
}

4.4.2 实现该接口

名称为:impl.StorageService,代码的实现如下:

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountTblMapper accountTblMapper;
    private static Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);
    @Override
    public void debit(String userId, intmoney) {
        logger.info("准备扣减用户:{} 余额,扣减的数目为:{}", userId, money);
        AccountTbl accountTbl = accountTblMapper.selectOne(new LambdaQueryWrapper<AccountTbl>().eq(AccountTbl::getUserId, userId));
        int idleMoney = accountTbl.getMoney() - money;
        if (idleMoney < 0) {
            throw new RuntimeException("用户余额不足");
        }
        accountTbl.setMoney(idleMoney);
        accountTblMapper.updateById(accountTbl);
        logger.info("扣减用户{}金额成功,剩余金额为{}",userId, money);
    }
}

4.4.3 使用 Restful 暴露此接口

添加一个 Controller

名称为:

代码如下:

@RestController
public class AccountController {
    @Autowired
    private AccountService accountService ;
    private static Loggerlogger =
    LoggerFactory.getLogger(AccountController.class) ;
    @GetMapping("/debit/{userId}/{money}")
    public ResponseEntity<Void> debit( @PathVariable("userId")String userId, @PathVariable("money") Integer money){
        logger.info("Account Service ... xid: " + RootContext.getXID());
        // 开始扣减余额
        accountService.debit(userId , money);
        return ResponseEntity.ok().build() ;
    }
}

4.4.4 添加配置文件

在 resource 目录里面新建配置文件:

内容如下:

server:
  port: 18085
spring:
  application:
    name: account-service
  cloud:
    alibaba:
      seata:
        tx-service-group: account-service
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username:root
    password:123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
    druid:
      max-active:20
      min-idle: 2
      initial-size: 2
seata:
  service:
    vgroup-mapping:
      account-service: default
    grouplist:
      default: 127.0.0.1:8091
    disable-global-transaction: false
    enabled: true
mybatis-plus:
  mapper-locations:classpath:/mapper/*.xml

4.4.5 添加启动类

命名为 AccountServiceApplication ,代码如下 :

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class AccoutServiceApplication {
    public static void main(String[] args) {
         SpringApplication.run(AccoutServiceApplication.class ,args) ;
    }
}

4.4.6 启动项目测试

启动项目后,打印该日志,说明连接 seata-server 成功。

4.5 order-service  代码的完善

4.5.1 接口设计

在 order-service 里面,主要完成保存用户订单的操作。

新建一个接口:

命名为:OrderService,代码如下:

代码如下:

public interface OrderService {
    /**
    * 创建一个订单
    *@param userId 用户 id
    *@param commodityCode 商品的编号
    *@param orderCount 商品的数量
    *@return OrderTbl
    */
    OrderTbl create(String userId, String commodityCode, int orderCount) ;
}

4.5.4 Ribbon 集成

创建一个配置类:

代码如下:

@Configuration
public class HttpUtilConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate() ;
    }
}

4.5.2 实现该接口

名称为:impl.OrderService,代码的实现如下:

@Service
public class OrderServiceImpl implements OrderService {
	@Autowired
	private OrderTblMapper orderTblMapper;
	@Autowired
	private AccountService accountService;
	private static Loggerlogger =
	LoggerFactory.getLogger(OrderServiceImpl.class);
	@Override
	public OrderTbl create(String userId, String commodityCode, int orderCount)
	{
		logger.info("准备为{}创建一个订单,商品编号为{},数量为{}", userId,commodityCode, orderCount);
		// 1 计算总金额
		int orderMoney = calculate(commodityCode, orderCount);
		accountService.debit(userId, orderMoney);
		OrderTbl order = new OrderTbl();
		order.setUserId(userId);
		order.setCommodityCode(commodityCode);
		order.setCount(orderCount);
		order.setMoney(orderMoney);
		orderTblMapper.insert(order);
		// INSERT INTO orders ...
		return order;
	}
	private int calculate(String commodityCode, int orderCount) {
		// 我们现在没有商品的表,在此我们把商品的价格定死
		int prodPrice= 0 ;
		if("HUAWEI_0001".equals(commodityCode)){ // 华为时 100
			prodPrice =100;
		}else if ("XIAOMI_002".equals(commodityCode)){ // 小米时 200
			prodPrice =200 ;
		}else {
			prodPrice =1000 ; // 其他为 1000
		}
		return orderCount *prodPrice ;
	}
}

4.5.3 远程调用 account-service 的实现

创建一个 AccountService 的类,该类里面主要完成对 accout-servic 的远程调用。

名称为:

/**
* 实现对账号服务的远程调用
*/
@Service
public class AccountService {
	private static Logger logger = LoggerFactory.getLogger(AccountService.class);
	/**
	* 1 ribbon 的方式
	*/
	@Autowired
	private RestTemplate restTemplate ;
	/**
	* 2 feign 的方式
	*/
	public void debit(String userId, intorderMoney) {
		ResponseEntity<Void> entity = restTemplate.
		getForEntity(
		"http://accout-service/debit/{userId}/{orderMoney}",
		Void.class,
		userId,
		orderMoney
		);
		if(entity.getStatusCode()== HttpStatus.OK){
			logger.info("扣减用户{}金额成功,本次扣减的数目为{}",userId,orderMoney);
			return ;
		}
		logger.info("扣减用户{}金额失败",userId);
		throw newRuntimeException("扣减金额失败") ;
	}
}

我们在此使用的时 Ribbon 做远程调用,下面的章节我们也会测试 Feign 。

4.5.5 使用 Restful 暴露此接口

添加一个 Controller

命名为:

代码如下:

@RestController
public class OrderController {
	private static Logger logger = LoggerFactory.getLogger(OrderController.class) ;
	@Autowired
	private OrderService orderService ;
	/**
	* 创建订单
	* @param userId
	* 用户 Id
	* @param commodityCode
	* 商品的编号
	* @param orderCount
	* 商品的数量
	* @return
	*/
	@GetMapping("/create/{userId}/{commodityCode}/{orderCount}")
	public ResponseEntity<Void> create(
	@PathVariable("userId") String userId,
	@PathVariable("commodityCode") String commodityCode,
	@PathVariable("orderCount") int orderCount){
		logger.info("Order Service ... xid: " + RootContext.getXID());
		orderService.create(userId, commodityCode, orderCount) ;
		return ResponseEntity.ok().build() ;
	}
}

4.5.6 添加配置文件

在 resource 目录里面新建配置文件:

命名为:application.yml

内容如下:

server:
  port: 18086
spring:
  application:
    name: order-service
  cloud:
    alibaba:
      seata:
        tx-service-group: order-service
    nacos:
      discovery:
        server-addr:localhost:8848
  datasource:
    name: orderDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
    druid:
      max-active: 20
      min-idle: 2
      initial-size: 2
seata:
  service:
    vgroup-mapping:
      order-service: default
    grouplist:
      default: 127.0.0.1:8091
    disable-global-transaction: false
    enabled: true
mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml

4.5.7 添加启动类

命名为:OrderServiceApplication

代码如下:

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class ,args) ;
    }
}

4.5.8 启动项目测试

启动项目后,打印该日志,说明连接 seata-server 成功。

4.6 business-service  代码的完善

4.6.1 接口设计

在 business-service 里面,主要完成下单的逻辑,包含 2 个主要的步骤,就是对库存服务和订单服务的远程调用。

新建一个接口:

命名为:com.bjsxt.service.BusinessService

代码如下:

public interface BusinessService {
    /**
     * 采购 / 下单的过程
     *@param userId
     * 用户的 Id
     *@param commodityCode
     * 商品的编码
     *@param orderCount
     * 商品的数量
     */
    void purchase(String userId, String commodityCode, int orderCount) ;
}

4.6.2 实现该接口

名称为:impl.BusinessServiceImpl,代码的实现如下:

@Service
public class BusinessServiceImpl implements BusinessService {
    private static Logger logger = LoggerFactory.getLogger(BusinessServiceImpl.class) ;
    @Autowired
    private StorageService storageService;
    @Autowired
    private OrderService orderService;
    @Override
    public void purchase(String userId, String commodityCode, int orderCount) {
        logger.info("准备下单,用户:{},商品:{},数量:{}",userId,commodityCode,orderCount);
        storageService.deduct(commodityCode,orderCount);
        orderService.create(userId, commodityCode, orderCount) ;
        logger.info("下单完成");
    }
}

4.6.3 远程调用 storage-service 的实现

创建一个 StorageService 的类,该类里面主要完成对 storage-servic 的远程调用。

名称为:


 

@Service
public class StorageService {
    private static Logger logger = LoggerFactory.getLogger(StorageService.class) ;
    /**
    * 1 采用 Ribbon 的形式
    */
    @Autowired
    private RestTemplate restTemplate ;
    /**
    * 2 采用 Feign 的形式
    */
    public void deduct(String commodityCode, int orderCount) {
        ResponseEntity<Void> entity = restTemplate.getForEntity(
        "http://storage-service/debut/{commodityCode}/{orderCount}",
        Void.class,
        commodityCode,
        orderCount
        );
        if (entity.getStatusCode()== HttpStatus.OK){
            logger.info("扣减库存成功,商品编号为{},本次扣减的数量为{}",commodityCode,orderCount);
        return;
        }
        throw new RuntimeException("扣减库存失败") ;
    }
}

我们在此使用的时 Ribbon 做远程调用,下面的章节我们也会测试 Feign 。

4.6.4 远程调用 order-service 的实现

新建一个类:

代码如下:

@Service
public class OrderService {
    private static Logger logger = LoggerFactory.getLogger(StorageService.class);
    /**
    * 1 采用 Ribbon 的形式
    */
    @Autowired
    private RestTemplate restTemplate ;
    /**
    * 2 采用 Feign 的形式
    */
    public void create(String userId, String commodityCode, int orderCount) {
        ResponseEntity<Void> entity = restTemplate.getForEntity(
        "http://order-service/create/{userId}/{commodityCode}/{orderCount}",
        Void.class,
        userId ,
        commodityCode,
        orderCount
        );
        if (entity.getStatusCode()== HttpStatus.OK){
            logger.info("订单创建成功,用户为{} ,商品编号为{},本次扣减的数量为{}",userId ,
            commodityCode,orderCount);
            return;
        }
        throw new RuntimeException("订单创建失败") ;
    }
}

4.6.5 集成 Ribbon

添加一个 HttpUtilConfig 的配置类:

代码如下:

@Configuration
public class HttpUtilConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate() ;
    }
}

4.6.6 添加配置文件

在 resource 目录里面新建配置文件:

命名为:application.yml

内容如下:

server:
  port: 18087
spring:
  application:
    name: business-service
  cloud:
    alibaba:
      seata:
        tx-service-group: business-service
    nacos:
      discovery:
        server-addr:localhost:8848
seata:
  service:
    vgroup-mapping:
      business-service: default
    grouplist:
      default: 127.0.0.1:8091
    disable-global-transaction: false
    enabled: true

4.6.7 添加启动类

命名为: BusinessServiceApplication

代码如下:

@SpringBootApplication
@EnableDiscoveryClient
public class BusinessServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(BusinessServiceApplication.class ,args) ;
    }
}

4.6.8 暴露下单接口

继续改造启动类:

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class BusinessServiceApplication {
    @Autowired
    private BusinessService businessService ;
    public static void main(String[] args) {
        SpringApplication.run(BusinessServiceApplication.class ,args) ;
    }
    /**
    * 开始下单
    * @param userId
    * 用户的 Id
    * @param commodityCode
    * 商品的编号
    * @param orderCount
    * 商品的数量
    * @return
    */
    @GetMapping("/purchase/{userId}/{commodityCode}/{orderCount}")
    public ResponseEntity<Void> purchase(
    @PathVariable("userId") String userId,
    @PathVariable("commodityCode")StringcommodityCode,
    @PathVariable("orderCount")Integer orderCount){
        businessService.purchase(userId,commodityCode,orderCount);
        return ResponseEntity.ok().build() ;
    }
}

4.6.9 启动项目测试

启动项目后,打印该日志,说明连接 seata-server 成功。

4.7  总体的调用流程如下

都启动完成后:

Nacos-Server:

4.8  正常下单测试

在浏览器里面访问:

http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1

代表 SXT_USER_1 购买 HUAWEI_0001 产品 1 件。

数据库里面:

  •  Accout_tbl 里面,SXT_USER_1 用户的金额减少100;

  • Storage_tbl 里面,HUAWEI_0001 的库存减少了 1;

  • Order_Tbl 里面,创建了一条订单记录;

说明,此时远程调用时正常的。

4.9  分布式事务的演示

我们演示如图的异常:


我们可以发现,远程调用共有 3 处。

4.9.1 在 accout-service 服务扣减余额触发异常

4.9.2 重启 accout-service

4.9.3 还原数据库里面的数据

Account_Tbl:

Storage_Tbl:

4.9.4 重新下单测试

http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1

数据库的数据:

Account_Tbl:

Storage_Tbl:

 

我们发现,分布式事务产生了,accout-service 内部的异常,导致 accout_tbl 表数据回滚了。

但是,在 storage_tbl :位于 stoage-service 的事务却没有回滚。

4.10  使用 Seata  解决分布式问题

4.10.1 改造 accout-service 里面的 AccountServiceImpl

当用户的 ID 为:SXT_USER_2 时,我们抛出异常,当为其他用户时,我们正常的下单。

4.10.2 改造 BusinessServiceImpl

添加一个注解,看他是否能解决分布式事务的问题

4.10.3 重启测试

重启 accout-service,business-service 测试

使用 SXT_USER_1 正常的下单测试:

Stoage_tbl:库存正常

Accout_Tbl:余额正常

使用 SXT_USER_2 下单测试:

发现发生异常后,

stoage_tbl 里面的没有发生改变,数据正常

Accout_tbl 里面的数据也没有发生改变,数据正常

分布式事务测试成功了


五、集成 Feign 测试 Seata

在上面的章节中,我们使用的时 Ribbon + RestTemplate 的形式做的远程调用。下面我们来演
示 Feign 的调用方式。

5.1  改造 business-service

5.1.1 添加依赖

修改 business-service 项目里面的 pom.xml 文件,在里面添加依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

5.1.2 添加 OrderServiceFeign

里面的代码如下:

@ FeignClient ("order-service")
public interface OrderServiceFeign {
    @GetMapping("/create/{userId}/{commodityCode}/{orderCount}")
    ResponseEntity<Void> create(
    @PathVariable("userId") String userId,
    @PathVariable("commodityCode") String commodityCode,
    @PathVariable("orderCount") Integer orderCount);
}

5.1.3 添加 StorageServiceFeign

@FeignClient ("storage-service")
public interface StorageServiceFeign {
    @GetMapping("/deduct/{commodityCode}/{orderCount}")
    ResponseEntity<Void> deduct(
    @PathVariable("commodityCode") String commodityCode,
    @PathVariable("orderCount") Integer orderCount
    ) ;
}

5.1.5 改造 OrderService

@Service
public class OrderService {
	private static Logger logger = LoggerFactory.getLogger(StorageService.class);
	/**
	* 1 采用 Ribbon 的形式
	*/
	@Autowired
	private RestTemplate restTemplate ;
	@Autowired
	private OrderServiceFeign orderServiceFeign ;
	/**
	* 2 采用 Feign 的形式
	*/
	public void create(String userId, String commodityCode, int orderCount){
		// Ribbon
		// ResponseEntity<Void> entity = restTemplate.
		// getForEntity("http://order-service/create/{userId}/{commodityCode}/{orderCount}",
		// Void.class,
		// userId ,
		// commodityCode,
		// orderCount
		// );
		//Feign
		ResponseEntity<Void> entity = orderServiceFeign.create(userId, commodityCode,orderCount);
		if (entity.getStatusCode()== HttpStatus.OK){
			logger.info("订单创建成功,用户为{} ,商品编号为{},本次扣减的数量为{}",userId ,
			commodityCode,orderCount);
			return;
		}
		throw new RuntimeException("订单创建失败") ;
	}
}

5.1.7 改造 StorageService

代码如下:

@Service
public class StorageService {
	private static Logger logger = LoggerFactory.getLogger(StorageService.class);
	/**
	* 1 采用 Ribbon 的形式
	*/
	@Autowired
	private RestTemplate restTemplate;
	@Autowired
	private StorageServiceFeign storageServiceFeign;
	/**
	* 2 采用 Feign 的形式
	*/
	public void deduct(String commodityCode, int orderCount){
		// Ribbon
		// ResponseEntity<Void> entity = restTemplate.
		// getForEntity(
		// "http://storage-service/deduct/{commodityCode}/{orderCount}",
		// Void.class,
		// commodityCode,
		// orderCount
		// );
		//Feign
		ResponseEntity<Void> entity = storageServiceFeign.deduct(commodityCode,orderCount);
		if (entity.getStatusCode() == HttpStatus.OK) {
			logger.info("扣减库存成功,商品编号为{},本次扣减的数量为{}",commodityCode,orderCount);
			return;
		}
		throw new RuntimeException("扣减库存失败");
	}
}

5.1.6 在启动类里面开启对 Feign 的支持

5.2  改造 order-service

5.2.1 添加依赖

在 dependencies 添加:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

5.2.2 添加接口

里面的代码如下:

@FeignClient("account-service")
public interface AccountServiceFeign{
    @GetMapping("/debit/{userId}/{orderMoney}")
    ResponseEntity<Void> debit(
    @PathVariable("userId") String userId,
    @PathVariable("orderMoney") Integer orderMoney
    ) ;
}

5.2.3 修改 AccoutService

/**
* 实现对账号服务的远程调用
*/
@Service
public class AccountService {
	private static Logger logger = LoggerFactory.getLogger(AccountService.class);
	/**
	* 1 ribbon 的方式
	*/
	@Autowired
	private RestTemplate restTemplate ;
	@Autowired
	private AccountServiceFeign accountServiceFeign ;
	/**
	* 2 feign 的方式
	*/
	public void debit(String userId, intorderMoney) {
		//Ribbon
		// ResponseEntity<Void> entity = restTemplate.
		// getForEntity(
		// "http://accout-service/debit/{userId}/{orderMoney}",
		// Void.class,
		// userId,
		// orderMoney
		// );
		ResponseEntity<Void> entity = accountServiceFeign.debit(userId, orderMoney);
		if(entity.getStatusCode()== HttpStatus.OK){
			logger.info("扣减用户{}金额成功,本次扣减的数目为{}",userId,orderMoney);
			return ;
		}
		logger.info("扣减用户{}金额失败",userId);
		throw newRuntimeException("扣减金额失败") ;
	}
}

5.2.4 在启动类里面添加对 Feign 的支持

5.3  重启测试

重启 order-service ,business-service

还原数据库数据,开始测试。

正常下单测试:

使用 SXT_USER_2 下单:

出错了,但是数据库的各个表都正常。

Seata 测试成功了。

案例源码gitee地址:https://gitee.com/BanSheng/spring-cloud-alibaba-examples/tree/master/seata-examples

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

plenilune-望月

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值