SpringCloud Alibaba Seata 处理分布式事务

一、前言

        接下来是开展一系列的 SpringCloud 的学习之旅,从传统的模块之间调用,一步步的升级为 SpringCloud 模块之间的调用,此篇文章为第十八篇,即使用 Seata 处理分布式事务。

二、分布式事务问题

        当单体应用被拆分成微服务应用时,假设原来由三个模块构成的一个单体应用,就会被拆分成三个独立的应用,分别使用三个独立的数据源,以下图为例:

        用户购买商品的业务逻辑,整个业务逻辑由三个微服务提供支持。仓储服务对给定的商品扣除仓储数量,订单服务根据采购需求创建订单,账户服务从用户账户中扣除余额。

        总结起来就是:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。

三、Seata 简介

3.1 Seata 是什么

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

3.2 Seata 作用

        一个典型的分布式事务过程是由:分布式事务处理过程的一ID + 三组件模型构成。

        Transaction ID XID:全局唯一的事务 ID

        Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

        Transaction Manager (TM):事务管理器,定义全局事务的范围:开始全局事务、提交或回滚全局事务。

        Resource Manager (RM):资源管理器,管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

3.3 处理过程

        1、TM TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID

        2、XID 在微服务调用链路的上下文中传播

        3、RM TC 注册分支事务,将其纳入 XID 对应全局事务的管辖

        4、TM TC 发起针对 XID 的全局提交或回滚决议。

        5、TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

四、Seata-Server 安装

4.1 下载

        在官网选择适合自己的版本,我这边下载的是 seata-server-0.9.0.zip 版本,最好跟我一样

4.2 安装

        首先将下载好的安装包解压到指定目录并修改 conf 目录下的 file.conf 配置文件,一共修改两个地方,第一个地方是修改 service 模块自定义事务组的名称,叫什么都可以,如下图:

        第二个地方是修改 store 模块事务日志存储模式为 db 和数据库连接信息,如下图:

        接下来在 mysql5.7 数据库新建库 seata,然后运行 conf 目录下的 db_store.sql 脚本,如下图:

        执行完 sql 脚本之后会创建三张表,如下图: 

        接下来修改 conf 目录下的 registry.conf 配置文件,指明注册中心为 nacos,并修改 nacos 的连接信息,如下图:

        最后先启动 Nacos,然后在 bin 目录下点击 seata-server.bat 命令来启动 seata,启动成功的界面如下所示:

五、数据库准备

5.1 业务说明

        接下来我们模拟一个 seata 的分布式交易解决方案的场景,如下图

        这里我们会创建三个服务,一个订单服务,一个库存服务和一个账户服务。

        当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

        该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

        总结起来就是:下订单 ---- 扣库存 ---- 减账户

5.2 创建数据库

        我们需要创建三个数据库,分别为:seata_order 存储订单的数据库,seata_storage 存储库存的数据库,seata_account 存储账户信息的数据库。建表语句如下:

create database seata_order;
create database seata_storage;
create database seata_account;

5.3 创建表

        在 seata_order 库下创建 t_order 表,建表语句如下:

drop TABLE if EXISTS `t_order`;
CREATE TABLE `t_order` (`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',`count` INT(11) DEFAULT NULL COMMENT '数量',`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0: 创建中;1: 已完结'
)ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

        在 seata_storage 库下创建 t_storage 表,建表语句如下:

drop TABLE if EXISTS `t_storage`;
CREATE TABLE `t_storage` (`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',`total` INT(11) DEFAULT NULL COMMENT '总库存',`used` INT(11) DEFAULT NULL COMMENT '已用库存',`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `t_storage`(id, product_id,total,used,residue)VALUES ('1','1','100','0','100');

        在 seata_account 库下创建 t_account 表,建表语句如下:

drop TABLE if EXISTS `t_account`;
CREATE TABLE `t_account` (`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',`used` DECIMAL(10.0) DEFAULT NULL COMMENT '已用余',`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `t_account`( `id` , `user_id` , `total`,`used`,`residue`) VALUES ('1','1','1000','0','1000');

        最后在上面创建的三个数据库下,分别创建各自的回滚日志表,执行的语句在 conf 目录下的 db_undo_log.sql,如下图:

        最终的效果如下图:

六、订单微服务准备

        创建一个 seata-order-service2001 的订单微服务模块,接下来详细的介绍下模块的相关内容。

        父 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"><modelVersion>4.0.0</modelVersion><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>cloud-provider-payment8001</module><module>cloud-consumer-order80</module><module>cloud-api-commons</module><module>cloud-eureka-server7001</module><module>cloud-eureka-server7002</module><module>cloud-provider-payment8002</module><module>cloud-provider-payment8004</module><module>cloud-consumerzk-order80</module><module>cloud-provider-payment8006</module><module>cloud-consumerconsul-order80</module><module>cloud-consumer-feign-order80</module><module>cloud-provider-hystrix-payment8001</module><module>cloud-consumer-feign-hystrix-order80</module><module>cloud-consumer-hystrix-dashboard9001</module><module>cloud-gateway-gateway9527</module><module>cloud-config-center-3344</module><module>cloud-config-client-3355</module><module>cloud-config-center-3366</module><module>cloud-stream-rabbitmq-provider8801</module><module>cloud-stream-rabbitmq-consumer8802</module><module>cloud-stream-rabbitmq-consumer8803</module><module>cloudalibaba-provider-payment9001</module><module>cloudalibaba-provider-payment9002</module><module>cloudalibaba-consumer-nacos-order83</module><module>cloudalibaba-config-nacos-client3377</module><module>cloudalibaba-sentinel-service8401</module><module>cloudalibaba-provider-payment9003</module><module>cloudalibaba-provider-payment9004</module><module>cloudalibaba-consumer-nacos-order84</module><module>seata-order-service2001</module><module>seata-storage-service2002</module><module>seata-account-service2003</module></modules><!-- 统一管理jar包版本 --><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><junit.version>4.12</junit.version><log4j.version>1.2.17</log4j.version><lombok.version>1.16.18</lombok.version><mysql.version>5.1.47</mysql.version><druid.version>1.1.16</druid.version><mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version></properties><!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version  --><dependencyManagement><dependencies><!--spring boot 2.2.2--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.2.2.RELEASE</version><type>pom</type><scope>import</scope></dependency><!--spring cloud Hoxton.SR1--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Hoxton.SR1</version><type>pom</type><scope>import</scope></dependency><!--spring cloud alibaba 2.1.0.RELEASE--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.1.0.RELEASE</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>${druid.version}</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.spring.boot.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>${log4j.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><optional>true</optional></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><addResources>true</addResources></configuration></plugin></plugins></build>
</project>

6.1 添加 maven 依赖

        pom.xml 的内容如下所示,唯一需要注意的是需要移除 spring-cloud-starter-alibaba-seata 依赖里面包含的 seata 依赖,单独引入 seata 依赖(版本不兼容)。模块之间的调用使用 openfeign

<?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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>seata-order-service2001</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--web-actuator--><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><!--mysql-druid--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.37</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>
</project>

6.2 创建 application.yml

        在 resources 目录下创建 application.yml 文件内容如下所示,唯一需要注意的是自定义事务组名称需要与 seata-server 中的对应,即本次的事务组要和我们的 seata 服务器的组要匹配。

server:port: 2001spring:application:name: seata-order-servicecloud:alibaba:seata:# 自定义事务组名称需要与 seata-server 中的对应tx-service-group: xhf_tx_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_orderusername: rootpassword: 123456feign:hystrix:enabled: falselogging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml

6.3 创建 file.conf

        在 resources 目录下创建 file.conf 文件内容如下所示,注意修改 servicedb 模块的相关信息。这个文件是控制该微服务的,我们还有 seata-server 里面还有一个 file.conf 是负责总控的。

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}service {vgroup_mapping.xhf_tx_group = "default" #修改自定义事务组名称default.grouplist = "127.0.0.1:8091"enableDegrade = falsedisable = falsemax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"disableGlobalTransaction = false
}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5tm.commit.retry.count = 1tm.rollback.retry.count = 1
}## transaction log store
store {## store mode: file、dbmode = "db"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"driver-class-name = "com.mysql.jdbc.Driver"url = "jdbc:mysql://127.0.0.1:3306/seata"user = "root"password = "123456"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}
}
recovery {#schedule committing retry period in millisecondscommitting-retry-period = 1000#schedule asyn committing retry period in millisecondsasyn-committing-retry-period = 1000#schedule rollbacking retry period in millisecondsrollbacking-retry-period = 1000#schedule timeout retry period in millisecondstimeout-retry-period = 1000
}transaction {undo.data.validation = trueundo.log.serialization = "jackson"undo.log.save.days = 7#schedule delete expired undo_log in millisecondsundo.log.delete.period = 86400000undo.log.table = "undo_log"
}## metrics settings
metrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898
}support {## springspring {# auto proxy the DataSource beandatasource.autoproxy = false}
}

6.4 创建 registry.conf

        在 resources 目录下创建 registry.conf 文件内容如下所示,这个配置文件用于标注出我们的这个微服务是向哪个注册中心进行注册的。这个文件是控制该微服务的,我们还有 seata-server 里面还有一个 registry.conf 是负责总控的。

registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {serverAddr = "localhost:8848"namespace = ""cluster = "default"}eureka {serviceUrl = "http://localhost:8761/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}etcd3 {cluster = "default"serverAddr = "http://localhost:2379"}sofa {serverAddr = "127.0.0.1:9603"application = "default"region = "DEFAULT_ZONE"datacenter = "DefaultDataCenter"cluster = "default"group = "SEATA_GROUP"addressWaitTime = "3000"}file {name = "file.conf"}
}config {# file、nacos 、apollo、zk、consul、etcd3type = "file"nacos {serverAddr = "localhost"namespace = ""}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}
}

6.5 创建实体类

        首先创建一个用于返回前端的调用结果的实体类 CommonResult,代码如下:

package com.springcloud.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;// 用于返回前端的调用结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult <T>{private Integer code;private String message;private T data;public CommonResult(Integer code,String message){this(code,message,null);}
}

        然后创建订单的实体类 Order,代码如下:

package com.springcloud.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.math.BigDecimal;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {private Long id;private Long userId;private Long productId;private Integer count;private BigDecimal money;// 订单状态:0 创建中,1 已完结private Integer status;
}

6.6 创建 Dao 层

        创建 dao 层接口 OrderDao,里面提供创建订单和更新订单状态的方法,代码如下:

package com.springcloud.dao;import com.springcloud.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface OrderDao {// 创建订单void create(Order order);// 更新订单状态void update(@Param("userId") Long userId,@Param("status") Integer status);
}

        在 resources 目录下新建 mapper 文件夹,并创建 OrderMapper.xml 文件,内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.springcloud.dao.OrderDao"><resultMap id="BaseResultMap" type="com.springcloud.entity.Order"><id column="id" property="id" jdbcType="BIGINT"/><result column="user_id" property="userId" jdbcType="BIGINT"/><result column="product_id" property="productId" jdbcType="BIGINT"/><result column="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 = 1where user_id=#{userId} and status = #{status};</update></mapper>

6.7 创建 service 层

        首先创建库存 service 接口,用于 openFeign 远程调用,代码如下:

package com.springcloud.service;import com.springcloud.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value="seata-storage-service")
public interface StorageService {// 扣减库存@PostMapping(value="/storage/decrease")CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

        其次创建账户 service 接口,用于 openFeign 远程调用,代码如下:

package com.springcloud.service;import com.springcloud.entity.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="seata-account-service")
public interface AccountService {// 扣减余额@PostMapping(value="/account/decrease")CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

        最后创建订单的 service 接口和实现类,代码如下:

package com.springcloud.service;import com.springcloud.entity.Order;public interface OrderService {// 创建订单void create(Order order);
}
package com.springcloud.service.impl;import com.springcloud.dao.OrderDao;
import com.springcloud.entity.Order;
import com.springcloud.service.AccountService;
import com.springcloud.service.OrderService;
import com.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import javax.annotation.Resource;// 下订单->减库存->减余额->改状态
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@ResourceOrderDao orderDao;@ResourceStorageService storageService;@ResourceAccountService accountService;@Overridepublic void create(Order order) {log.info("------->下单开始");//1 新建订单orderDao.create(order);//2 扣减库存log.info("------->order-service中扣减库存开始");storageService.decrease(order.getProductId(),order.getCount());log.info("------->order-service中扣减库存结束");//3 扣减账户log.info("------->order-service中扣减余额开始");accountService.decrease(order.getUserId(),order.getMoney());log.info("------->order-service中扣减余额结束");//4 修改订单状态,从零到1,1代表已经完成log.info("------->order-service中修改订单状态开始");orderDao.update(order.getUserId(), 0);log.info("------->order-service中修改订单状态结束");log.info("------->下单结束");}
}

6.8 创建 controller

        创建对外提供的 OrderController 类,代码如下:

package com.springcloud.controller;import com.springcloud.entity.CommonResult;
import com.springcloud.entity.Order;
import com.springcloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
public class OrderController {@ResourceOrderService orderService;@GetMapping ("/order/create")public CommonResult create (Order order){orderService.create(order);return new CommonResult(200,"创建成功");}}

6.9 创建配置类

        接下来创建一个 Mybatis 的配置类,用于扫描 mapper 文件,代码如下

package com.springcloud.config;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;@Configuration
@MapperScan({"com.springcloud.dao"})
public class MyBatisConfig {
}

        然后创建一个使用 Seata 对数据源进行代理的配置类,代码如下:

package com.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;@Configuration
public class DataSourceProxyConfig {@Value("${mybatis.mapperLocations}")private String mapperLocations;@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource druidDataSource() {return new DruidDataSource();}@Beanpublic DataSourceProxy dataSourceProxy(DataSource dataSource) {return new DataSourceProxy(dataSource);}@Beanpublic SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSourceProxy);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());return sqlSessionFactoryBean.getObject();}}

6.10 主启动类

        主启动类的代码如下所示:

package com.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class SeataOrderMainApp2001
{public static void main(String[] args){SpringApplication.run(SeataOrderMainApp2001.class, args);}
}

6.11 测试

        启动模块进行测试,可以看到,启动没有任何问题,如下图:

七、库存微服务准备

         创建一个 seata-storage-service2002 的库存微服务模块,接下来详细的介绍下模块的相关内容。

7.1 添加 maven 依赖

        pom.xml 的内容如下所示,唯一需要注意的是需要移除 spring-cloud-starter-alibaba-seata 依赖里面包含的 seata 依赖,单独引入 seata 依赖(版本不兼容)。模块之间的调用使用 openfeign

<?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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>seata-storage-service2002</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.37</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>
</project>

7.2 创建 application.yml

        在 resources 目录下创建 application.yml 文件内容如下所示,唯一需要注意的是自定义事务组名称需要与 seata-server 中的对应,即本次的事务组要和我们的 seata 服务器的组要匹配。

server:port: 2002spring:application:name: seata-storage-servicecloud:alibaba:seata:tx-service-group: xhf_tx_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storageusername: rootpassword: 123456logging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml

7.3 创建 file.conf

        在 resources 目录下创建 file.conf 文件内容如下所示,注意修改 servicedb 模块的相关信息。这个文件是控制该微服务的,我们还有 seata-server 里面还有一个 file.conf 是负责总控的。

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}service {vgroup_mapping.xhf_tx_group = "default" #修改自定义事务组名称default.grouplist = "127.0.0.1:8091"enableDegrade = falsedisable = falsemax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"disableGlobalTransaction = false
}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5tm.commit.retry.count = 1tm.rollback.retry.count = 1
}## transaction log store
store {## store mode: file、dbmode = "db"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"driver-class-name = "com.mysql.jdbc.Driver"url = "jdbc:mysql://127.0.0.1:3306/seata"user = "root"password = "123456"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}
}
recovery {#schedule committing retry period in millisecondscommitting-retry-period = 1000#schedule asyn committing retry period in millisecondsasyn-committing-retry-period = 1000#schedule rollbacking retry period in millisecondsrollbacking-retry-period = 1000#schedule timeout retry period in millisecondstimeout-retry-period = 1000
}transaction {undo.data.validation = trueundo.log.serialization = "jackson"undo.log.save.days = 7#schedule delete expired undo_log in millisecondsundo.log.delete.period = 86400000undo.log.table = "undo_log"
}## metrics settings
metrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898
}support {## springspring {# auto proxy the DataSource beandatasource.autoproxy = false}
}

7.4 创建 registry.conf

        在 resources 目录下创建 registry.conf 文件内容如下所示,这个配置文件用于标注出我们的这个微服务是向哪个注册中心进行注册的。这个文件是控制该微服务的,我们还有 seata-server 里面还有一个 registry.conf 是负责总控的。

registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {serverAddr = "localhost:8848"namespace = ""cluster = "default"}eureka {serviceUrl = "http://localhost:8761/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}etcd3 {cluster = "default"serverAddr = "http://localhost:2379"}sofa {serverAddr = "127.0.0.1:9603"application = "default"region = "DEFAULT_ZONE"datacenter = "DefaultDataCenter"cluster = "default"group = "SEATA_GROUP"addressWaitTime = "3000"}file {name = "file.conf"}
}config {# file、nacos 、apollo、zk、consul、etcd3type = "file"nacos {serverAddr = "localhost"namespace = ""}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}
}

7.5 创建实体类

        首先创建一个用于返回前端的调用结果的实体类 CommonResult,代码如下:

package com.springcloud.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;// 用于返回前端的调用结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult <T>{private Integer code;private String message;private T data;public CommonResult(Integer code,String message){this(code,message,null);}
}

        然后创建订单的实体类 Storage,代码如下:

package com.springcloud.entity;import lombok.Data;@Data
public class Storage {private Long id;/*** 产品id*/private Long productId;/*** 总库存*/private Integer total;/*** 已用库存*/private Integer used;/*** 剩余库存*/private Integer residue;
}

7.6 创建 Dao 层

        创建 dao 层接口 StorageDao,里面提供扣减库存的方法,代码如下:

package com.springcloud.dao;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface StorageDao {/*** 扣减库存*/void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

        在 resources 目录下新建 mapper 文件夹,并创建 StorageMapper.xml 文件,内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.springcloud.dao.StorageDao"><resultMap id="BaseResultMap" type="com.springcloud.entity.Storage"><id column="id" property="id" jdbcType="BIGINT"/><result column="product_id" property="productId" jdbcType="BIGINT"/><result column="total" property="total" jdbcType="INTEGER"/><result column="used" property="used" jdbcType="INTEGER"/><result column="residue" property="residue" jdbcType="INTEGER"/></resultMap><update id="decrease">UPDATEt_storageSETused = used + #{count},residue = residue - #{count}WHEREproduct_id = #{productId}</update></mapper>

7.7 创建 service 层

        创建库存的 service 接口和实现类,代码如下:

package com.springcloud.service;public interface StorageService {/*** 扣减库存*/void decrease(Long productId, Integer count);
}
package com.springcloud.service.impl;import com.springcloud.dao.StorageDao;
import com.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
@Slf4j
public class StorageServiceImpl implements StorageService {@ResourceStorageDao storageDao;@Overridepublic void decrease(Long productId, Integer count) {log.info("------->storage-service中扣减库存开始");storageDao.decrease(productId, count);log.info("------->storage-service中扣减库存结束");}
}

7.8 创建 controller

        创建对外提供的 StorageController 类,代码如下:

package com.springcloud.controller;import com.springcloud.entity.CommonResult;
import com.springcloud.service.StorageService;
import org.apache.ibatis.annotations.Param;
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;@RestController
public class StorageController {@ResourceStorageService storageService;/*** 扣减库存*/@RequestMapping("/storage/decrease")public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){storageService.decrease(productId,count);return new CommonResult(200,"扣减库存成功!");}
}

7.9 创建配置类

        接下来创建一个 Mybatis 的配置类,用于扫描 mapper 文件,代码如下

package com.springcloud.config;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;@Configuration
@MapperScan({"com.springcloud.dao"})
public class MyBatisConfig {
}

        然后创建一个使用 Seata 对数据源进行代理的配置类,代码如下:

package com.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;@Configuration
public class DataSourceProxyConfig {@Value("${mybatis.mapperLocations}")private String mapperLocations;@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource druidDataSource() {return new DruidDataSource();}@Beanpublic DataSourceProxy dataSourceProxy(DataSource dataSource) {return new DataSourceProxy(dataSource);}@Beanpublic SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSourceProxy);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());return sqlSessionFactoryBean.getObject();}}

7.10 主启动类

        主启动类的代码如下所示:

package com.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002 {public static void main(String[] args) {SpringApplication.run(SeataStorageServiceApplication2002.class, args);}}

7.11 测试

        启动模块进行测试,可以看到,启动没有任何问题,如下图:

八、账户微服务准备

         创建一个 seata-account-service2003 的账户微服务模块,接下来详细的介绍下模块的相关内容。

8.1 添加 maven 依赖

        pom.xml 的内容如下所示,唯一需要注意的是需要移除 spring-cloud-starter-alibaba-seata 依赖里面包含的 seata 依赖,单独引入 seata 依赖(版本不兼容)。模块之间的调用使用 openfeign

<?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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>seata-account-service2003</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.37</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>
</project>

8.2 创建 application.yml

        在 resources 目录下创建 application.yml 文件内容如下所示,唯一需要注意的是自定义事务组名称需要与 seata-server 中的对应,即本次的事务组要和我们的 seata 服务器的组要匹配。

server:port: 2003spring:application:name: seata-account-servicecloud:alibaba:seata:tx-service-group: xhf_tx_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_accountusername: rootpassword: 123456feign:hystrix:enabled: falselogging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml

8.3 创建 file.conf

        在 resources 目录下创建 file.conf 文件内容如下所示,注意修改 servicedb 模块的相关信息。这个文件是控制该微服务的,我们还有 seata-server 里面还有一个 file.conf 是负责总控的。

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}service {vgroup_mapping.xhf_tx_group = "default" #修改自定义事务组名称default.grouplist = "127.0.0.1:8091"enableDegrade = falsedisable = falsemax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"disableGlobalTransaction = false
}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5tm.commit.retry.count = 1tm.rollback.retry.count = 1
}## transaction log store
store {## store mode: file、dbmode = "db"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"driver-class-name = "com.mysql.jdbc.Driver"url = "jdbc:mysql://127.0.0.1:3306/seata"user = "root"password = "123456"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}
}
recovery {#schedule committing retry period in millisecondscommitting-retry-period = 1000#schedule asyn committing retry period in millisecondsasyn-committing-retry-period = 1000#schedule rollbacking retry period in millisecondsrollbacking-retry-period = 1000#schedule timeout retry period in millisecondstimeout-retry-period = 1000
}transaction {undo.data.validation = trueundo.log.serialization = "jackson"undo.log.save.days = 7#schedule delete expired undo_log in millisecondsundo.log.delete.period = 86400000undo.log.table = "undo_log"
}## metrics settings
metrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898
}support {## springspring {# auto proxy the DataSource beandatasource.autoproxy = false}
}

8.4 创建 registry.conf

        在 resources 目录下创建 registry.conf 文件内容如下所示,这个配置文件用于标注出我们的这个微服务是向哪个注册中心进行注册的。这个文件是控制该微服务的,我们还有 seata-server 里面还有一个 registry.conf 是负责总控的。

registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {serverAddr = "localhost:8848"namespace = ""cluster = "default"}eureka {serviceUrl = "http://localhost:8761/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}etcd3 {cluster = "default"serverAddr = "http://localhost:2379"}sofa {serverAddr = "127.0.0.1:9603"application = "default"region = "DEFAULT_ZONE"datacenter = "DefaultDataCenter"cluster = "default"group = "SEATA_GROUP"addressWaitTime = "3000"}file {name = "file.conf"}
}config {# file、nacos 、apollo、zk、consul、etcd3type = "file"nacos {serverAddr = "localhost"namespace = ""}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}
}

8.5 创建实体类

        首先创建一个用于返回前端的调用结果的实体类 CommonResult,代码如下:

package com.springcloud.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;// 用于返回前端的调用结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult <T>{private Integer code;private String message;private T data;public CommonResult(Integer code,String message){this(code,message,null);}
}

        然后创建账户的实体类 Account,代码如下:

package com.springcloud.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.math.BigDecimal;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {/*** 用户id*/private Long userId;/*** 总额度*/private BigDecimal total;/*** 已用额度*/private BigDecimal used;/*** 剩余额度*/private BigDecimal residue;
}

8.6 创建 Dao 层

        创建 dao 层接口 AccountDao,里面提供扣减账户余额的方法,代码如下:

package com.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);
}

        在 resources 目录下新建 mapper 文件夹,并创建 AccountMapper.xml 文件,内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.springcloud.dao.AccountDao"><resultMap id="BaseResultMap" type="com.springcloud.entity.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_accountSETresidue = residue - #{money},used = used + #{money}WHEREuser_id = #{userId};</update></mapper>

8.7 创建 service 层

        创建账户的 service 接口和实现类,代码如下:

package com.springcloud.service;import java.math.BigDecimal;public interface AccountService {void decrease(Long userId,BigDecimal money);
}
package com.springcloud.service.impl;import com.springcloud.dao.AccountDao;
import com.springcloud.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.math.BigDecimal;@Service
@Slf4j
public class AccountServiceImpl implements AccountService {@ResourceAccountDao accountDao;@Overridepublic void decrease(Long userId, BigDecimal money) {log.info("------->account-service中扣减账户余额开始");accountDao.decrease(userId, money);log.info("------->account-service中扣减账户余额结束");}
}

8.8 创建 controller

        创建对外提供的 AccountController 类,代码如下:

package com.springcloud.controller;import com.springcloud.entity.CommonResult;
import com.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 {@ResourceAccountService accountService;/*** 扣减账户余额*/@RequestMapping("/account/decrease")public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){accountService.decrease(userId,money);return new CommonResult(200,"扣减账户余额!");}
}

8.9 创建配置类

        接下来创建一个 Mybatis 的配置类,用于扫描 mapper 文件,代码如下

package com.springcloud.config;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;@Configuration
@MapperScan({"com.springcloud.dao"})
public class MyBatisConfig {
}

        然后创建一个使用 Seata 对数据源进行代理的配置类,代码如下:

package com.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;@Configuration
public class DataSourceProxyConfig {@Value("${mybatis.mapperLocations}")private String mapperLocations;@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource druidDataSource() {return new DruidDataSource();}@Beanpublic DataSourceProxy dataSourceProxy(DataSource dataSource) {return new DataSourceProxy(dataSource);}@Beanpublic SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSourceProxy);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());return sqlSessionFactoryBean.getObject();}}

8.10 主启动类

        主启动类的代码如下所示:

package com.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003
{public static void main(String[] args){SpringApplication.run(SeataAccountMainApp2003.class, args);}
}

8.11 测试

        启动模块进行测试,可以看到,启动没有任何问题,如下图:

九、测试

9.1 正常下单

        此时数据库的初始数据如下图,由于无人下单,订单表没有数据;库存表一共有 100 个,卖了 0 个,还剩 100 个;账户表一共 1000 元,用了 0 元,还剩 1000 元。

        分别启动三个微服务,然后通过浏览器创建订单 http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100,如下图,订单创建成功。

        控制台的打印日志输出如下,也是没有任何问题的。

        此时数据库的数据如下图,用户下单成功,订单表里面新增了一条数据;库存表里面一共 100 个,卖出去 10 个,还剩 90 个;账户表里面一共 1000 元,花了 100 元,还剩 900 元。

9.2 超时异常

        为了演示超时异常的情况,我们在 seata-account-service2003 模块的 AccountServiceImpl 类中模拟一个超时的异常,因为 openFeign 默认的超时时间为 1s,必定会发生异常,代码如下:

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {@ResourceAccountDao accountDao;@Overridepublic void decrease(Long userId, BigDecimal money) {log.info("------->account-service中扣减账户余额开始");// 模拟超时场景try {Thread.sleep(2000L);} catch (InterruptedException e) {throw new RuntimeException(e);}accountDao.decrease(userId, money);log.info("------->account-service中扣减账户余额结束");}
}

        此时重启 seata-account-service2003 模块,然后再次访问 http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100,如下图,可以看到直接就返回了错误的信息

        此时数据库的数据如下图,订单表里面新增了一条数据,但此条数据处于下单未成功的状态;但是库存和金额都扣除了,订单状态并没有设置为已经完成,没有从 0 改为 1,而且由于 feign 的重试机制,账户余额还有可能被多次扣减,故出现了事务的问题。

9.3 添加全局事务控制

        为了解决上面出现的事务问题,我们可以在业务类 OrderServiceImpl 上添加一个 @GlobalTransactional 注解来解决这个问题,代码如下:

// 下订单->减库存->减余额->改状态
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@ResourceOrderDao orderDao;@ResourceStorageService storageService;@ResourceAccountService accountService;@Override// name:叫什么都行,rollbackFor:发生什么异常时进行回滚@GlobalTransactional(name = "xhf-create-order",rollbackFor = Exception.class)public void create(Order order) {log.info("------->下单开始");//1 新建订单orderDao.create(order);//2 扣减库存log.info("------->order-service中扣减库存开始");storageService.decrease(order.getProductId(),order.getCount());log.info("------->order-service中扣减库存结束");//3 扣减账户log.info("------->order-service中扣减余额开始");accountService.decrease(order.getUserId(),order.getMoney());log.info("------->order-service中扣减余额结束");//4 修改订单状态,从零到1,1代表已经完成log.info("------->order-service中修改订单状态开始");orderDao.update(order.getUserId(), 0);log.info("------->order-service中修改订单状态结束");log.info("------->下单结束");}
}

        再次重启工程,再次访问上述的 url 地址,返回结果如下

        但是数据库里面三个表的数据没有发生任何的变化,这里不再粘图了。

十、一部分补充

10.1 seata 背景

        seata 的全称为 Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架,是 2019 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。

        从 2020 开始,参加工作后用 1.0 以后的版本,因为它支持集群了。

10.2 再看三大组件

        结合我们开发的案例来讲,TC 就是我们单独部署的 seata-serverTM 就是有 @GlobalTransactional 注解标注的那个方法,而 RM 就是我们编写的订单模块、库存模块和账户模块,因为它们都操作了数据库资源。

10.3 分布式事务的执行流程

        1、TM 开启分布式事务,即 TM TC 注册全局事务记录(代码扫描到了 @GlobalTransactional 注解)

        2、按业务场景编排数据库和服务等事务内部资源,即 RM TC 汇报资源状态(需要让 TC 知道此次事务都涉及到哪些数据库表和资源)

        3、TM 结束分布式事务,事务一阶段结束,即 TM 通知 TC 提交/回滚分布式事务。

        4、TC 汇总事务信息,决定分布式事务是提交还剩回滚。

        5、TC 通知所有 RM 提交/回滚资源,事务二阶段结束。

10.4 seata 模式

        seata 有三种模式供大家选择,分别为 AT 模式、TCC 模式和 Saga 模式,我们默认使用的都是 AT 模式,接下来详细的介绍下这种模式。

10.4.1 seata 模式概述

        AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxySeata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

10.4.2 seata 模式整体机制

10.4.3 一阶段加载

        在一阶段,Seata 会拦截业务 SQL,如下图:

        1 、解析 SQL 语义,找到业务 SQL 要更新的业务数据,在业务数据被更新前,将其保存成 before image。

        2、执行业务 SQL 更新业务数据,

        3、在业务数据更新之后,其保存成 after image,最后生成行锁。

        以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

10.4.4 二阶段提交

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

10.4.5 二阶段回滚

        二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的业务 SQL,还原业务数据。

        回滚方式便是用 before image 还原业务数据;但在还原前要首先要校验脏写,对比数据库当前业务数据和 after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。如下图:

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

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

相关文章

【超简单】基于PaddleSpeech搭建个人语音听写服务

一、【超简单】之基于PaddleSpeech搭建个人语音听写服务 1.需求分析 亲们,你们要写会议纪要嘛?亲们,你们要写会议纪要嘛?亲们,你们要写会议纪要嘛?当您面对成吨的会议录音,着急写会议纪要而不得不愚公移山、人海战术?听的头晕眼花,听的漏洞百出,听的怀疑人生,那么你…

代码随想录算法训练营Day48|LC198 打家劫舍LC213 打家劫舍IILC337 打家劫舍III

一句话总结&#xff1a;前两题白给&#xff0c;第三题树形DP有点难。 原题链接&#xff1a;198 打家劫舍 滚动数组直接秒了。 class Solution {public int rob(int[] nums) {int n nums.length;int first 0, second nums[0];for (int i 2; i < n; i) {int tmp Math.m…

如何开始用 C++ 写一个光栅化渲染器?

光栅化渲染器是计算机图形学中最基础且广泛应用的一种渲染技术&#xff0c;它将三维模型转化为二维图像。下面我们将逐步介绍如何使用C语言从零开始构建一个简单的光栅化渲染器。 一、理解光栅化渲染原理 光栅化是一种将几何数据&#xff08;如点、线、三角形&#xff09;转换…

电商选品难?那是因为你不会用大数据选品工具…

电商选品之所以难&#xff0c;主要有以下几个方面的原因。电商市场更新换代非常快&#xff0c;新的产品不断涌现&#xff0c;旧的产品可能很快就被淘汰。电商选品紧跟市场趋势&#xff0c;不断调整和更新&#xff0c;这对电商运营市场敏感度和反应速度提出了很高的要求。 电商…

110V降9V1A非隔离降压恒压WT5112

110V降9V1A非隔离降压恒压WT5112 嘿&#xff0c;让我来给你说说这个WT5112控制芯片。这可是个厉害的东西&#xff0c;特别适合用在充电器啊、适配器啊还有LED灯这些地方。它最牛的地方就是能稳稳地控制电压和电流&#xff0c;而且还有个什么原边反馈技术让控制得更准。更酷的是…

MySQL_00001_00000

数据准备 员工表&#xff1a;emp Oracle: create table emp ( empno number(4) not null, ename varchar2(10), job varchar2(9), mgr number(4), hiredate date, sal number(7, 2), comm number(7, 2), deptno number(2) ); insert into em…

数据库讲解---(SQL语句--表的使用)【MySQL版本】

零.前言 数据库讲解&#xff08;MySQL版&#xff09;&#xff08;超详细&#xff09;【第一章】-CSDN博客 数据库-ER图教程_e-r图数据库-CSDN博客 数据库讲解&#xff08;MySQL版&#xff09;&#xff08;超详细&#xff09;【第二章】【上】-CSDN博客 一.SQL概述 1.1SQL简…

组合逻辑电路中的竞争与冒险

竞争与冒险 进行理想的组合逻辑电路分析与设计时&#xff0c;没有考虑逻辑门的延迟时间&#xff08;原因&#xff09;对电路产生的影响&#xff0c;且认为电路的输入和输出均处于稳定的逻辑电平。 实际上&#xff0c;信号经过逻辑门需要一定的时间。不同路径上门电路数目不同…

【Qt】文件与音视频

目录 一、输入输出设备类 二、文件读写类 三、文件和目录信息类 四、音视频 4.1 音频 4.2 视频 文件操作是应用程序必不可少的部分。Qt作为一个通用开发库&#xff0c;提供了跨平台的文件操作能力。Qt提供了很多关于文件的类&#xff0c;通过这些类能够对文件系统进行操作…

LeetCode刷题之94.二叉树中序遍历

文章目录 1. 描述2. 分析2.1 递归方法2.2 迭代 3. 解答3.1 递归3.2 迭代 1. 描述 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2] 示例 2&#xff1a; 输入&#xff1a;ro…

浏览器工作原理与实践--页面性能:如何系统地优化页面

在前面几篇文章中&#xff0c;我们分析了页面加载和DOM生成&#xff0c;讨论了JavaScript和CSS是如何影响到DOM生成的&#xff0c;还结合渲染流水线来讲解了分层和合成机制&#xff0c;同时在这些文章里面&#xff0c;我们还穿插说明了很多优化页面性能的最佳实践策略。通过这些…

【C语言】扫雷小游戏

文章目录 前言一、游戏玩法二、创建文件test.c文件menu()——打印菜单game()——调用功能函数&#xff0c;游戏的实现main()主函数 game.c文件初始化棋盘打印棋盘随机布置雷的位置统计周围雷的个数展开周围一片没有雷的区域计算已排查位置的个数排查雷(包括检测输赢): game.h文…

【剪映专业版】04全局设置

视频课程&#xff1a;B站有知公开课【剪映电脑版教程】 设置-全局设置 草稿 草稿位置&#xff1a;非系统盘&#xff08;C盘&#xff09; 素材下载位置与 缓存管理&#xff1a;如果下载素材较多&#xff0c;需要定期删除缓存 预设保存位置&#xff1a;非系统盘&#xff08;C盘&a…

基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务多租户系统架构

简介 基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务多租户系统架构。并引入组件化的思想实现高内聚低耦合并且高度可配置化&#xff0c;适合学习和企业中使用。 真正实现了基于RBAC、jwt和oauth2的无状态统一权限认证的解决方案&#x…

C语言操作符详解(二)

一、位操作符 & 按位与 | 按位或 ^ 按位异或 ~ 按位取反 注意&#xff1a;它们的操作数必须是整数。 下面的码我都只取了后八位 1.1、按位与 使用补码进行按位与 规则:对应二进制位有0就是0,两个同时为1才为1. 1.2、按位或 使用补码进行按位或 规则:对应二进…

【攻防世界】Web_python_template_injection

{{}}是变量包裹标识符&#xff0c;里面存放的是一个变量&#xff0c;当你输入 http://61.147.171.105:55121/{{8*8}} 执行成功&#xff0c;说明存在模版注入。接下来&#xff0c;开始想办法编代码拿到服务器的控制台权限 。 首先&#xff0c;题目告诉我们这是一个 python 注入…

SysTick滴答定时器 - 延时函数

SysTick定时器 Systick定时器&#xff0c;是一个简单的定时器&#xff0c;对于CM3,CM4内核芯片&#xff0c;都有Systick定时器。Systick定时器常用来做延时&#xff0c;或者实时系统的心跳时钟。这样可以节省MCU资源&#xff0c;不用浪费一个定时器。比如UCOS中&#xff0c;分…

每日一题:矩阵置零

给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]]使用两个标记变量。 class Sol…

全球IP数据库:多维度的数据收集与应用

随着互联网的普及和信息技术的飞速发展&#xff0c;全球IP数据库作为一种重要的数据资源&#xff0c;正在被广泛应用于各个领域。全球IP数据库不仅包含了庞大的IP地址信息&#xff0c;还涵盖了丰富的多维度数据&#xff0c;这些数据可以帮助企业、政府和研究机构更好地了解用户…

并发学习26--多线程 异步模式之工作线程

定义&#xff1a;让有限的工作线程&#xff08;Worker Thread&#xff09;来轮流异步处理无限多的任务。线程池便是这种模式。 饥饿&#xff1a; 固定大小线程池也会有饥饿现象。 若一个线程池有两个线程&#xff0c;能够处理两种任务。但是两种任务间有先后顺序。若来一个任…