目录

一、分布式事务由来

二、Seata简介

三、Seata-Server下载及安装

四、Seata实战案例

五、Seata之Account账户微服务搭建

六、Seata之Storage库存微服务搭建

七、Seata之Order订单微服务搭建

八、Seata之@GlobalTransactional验证

九、总结


一、分布式事务由来

在构建微服务的过程中,不管是使用什么框架、组件来构建,都绕不开一个问题,跨服务的业务操作如何保持数据一致性。

  • 什么是分布式事务?

在传统的单体应用中,无论多少内部调用,最后终归是在同一个数据库上进行操作来完成一项业务操作,单体应用架构图类似下图:

随着业务量的发展,业务需求和架构发生了巨大的变化,整体架构由原来的单体应用逐渐拆分成为了微服务,原来的3个服务被从一个单体架构上拆分为3个独立的微服务,分别使用独立的数据源,具体的业务将由三个服务的调用来完成,如图:

如上图: 订单服务创建订单的同时,需要调用库存服务扣减库存,同时还要调用账户服务扣减用户余额,跨了三个数据库进行一个下单业务逻辑。 此时,每一个服务的内部数据一致性仍然有本地事务来保证,但是全局事务怎么保证数据一致性,这就需要控制分布式事务来保证数据一致性。

通俗地讲,一句话:一次业务操作需要跨多个数据源或者需要跨多个系统进行远程调用,就会产生分布式事务问题。

在分布式系统中,实现分布式事务的方案有很多种,比如两阶段提交方案/XA方案、 TCC 方案(Try、Confirm、Cancel)等,同时Spring Cloud Alibaba组合套件也提供了Seata组件来实现分布式事务。本篇文章主要围绕Spring Cloud Alibaba Seata如何实现分布式事务进行讲解。

二、Seata简介

  • Seata是什么?

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

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

  • Seata官网地址?

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

  • Seata术语
  • TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器

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

  • Seata处理过程?

Seata处理过程图如下所示:

Seata处理过程分为如下几步:

  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
  2. XID在微服务调用链路的上下文传播;
  3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
  4. TM向TC发起针对XID的全局提交或回滚决议;
  5. TC调度XID下管辖的全部分支事务完成提交或者回滚请求;

三、Seata-Server下载及安装

【a】下载Seata服务器安装包

下载链接,这里我们没有使用最新版本的,选择的是0.9.0版本进行测试。

【b】修改conf目录下的file.conf配置文件

seata-server-0.9.0.zip解压到seata-server-0.9.0目录,然后打开conf配置相关的目录,找到file.conf文件,主要修改如下:

  • 自定义事务组名称;

  • 事务日志存储模式为db;
  • 数据库连接信息;

【c】mysql数据库新建一个数据库:名字为seata

【d】初始化表结构

在下载好的seata-server安装包的conf目录中已经帮我们准备好了建表的SQL,我们直接拿过来执行即可。

执行sql脚本后,会自动生成三张表:global_table、branch_table、lock_table。

【e】修改conf目录下的registry.conf配置文件

主要修改seata注册的地址,seata支持注册到file 、nacos 、eureka、redis、zk、consul、etcd3、sofa中,这里我们选择nacos作为注册的地址。

目的是指明注册中心为nacos,以及修改nacos的连接信息。

【f】启动nacos服务端

注意,必须先启动nacos注册服务端,因为seata需要注册进来。

我们先启动Nacos服务注册中心:

【g】 启动seata-server服务端

直接双击seata=server.bat批处理文件,直接启动seata服务端。

如果看到如上图所示的注册了NacosRegistryProvider组件等信息,说明我们的seata-server启动成功。

四、Seata实战案例

在本节,我们将通过一个实战案例来具体介绍Seata的使用方式,我们将模拟用户购买商品的业务逻辑,整个业务由3个微服务提供支持:

  • 库存服务:对给定的商品扣除库存数量;
  • 订单服务:根据采购需求创建订单;
  • 账户服务:从用户账户中扣除余额;

具体流程图如图:

业务说明:

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

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程服务调用,很明显会有分布式事务问题。

下面我们先准备好业务数据库,这里需要创建三个数据库:

  • seata_order:存储订单的数据库;
  • seata_storage:存储库存的数据库;
  • seata_account:存储账户信息的数据库;

【a】建立数据库

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

【b】在各个数据库中建立对应业务表

  • seata_order:建立t_order表;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (`id` BIGINT(11) NOT NULL AUTO_INCREMENT,`user_id` BIGINT(11) NULL DEFAULT NULL COMMENT '用户id',`product_id` BIGINT(11) NULL DEFAULT NULL COMMENT '产品id',`count` INT(11) NULL DEFAULT NULL COMMENT '数量',`money` DECIMAL(11, 0) NULL DEFAULT NULL COMMENT '金额',`status` INT(1) NULL DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 7 CHARACTER SET = utf8;SELECT * FROM t_order;

  • seata_storage:建立t_storage表;
CREATE TABLE `t_storage`  (`id` bigint(11) NOT NULL AUTO_INCREMENT,`product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id',`total` int(11) NULL DEFAULT NULL COMMENT '库存',`used` int(11) NULL DEFAULT NULL COMMENT '已用库存',`residue` int(11) NULL DEFAULT NULL COMMENT '剩余库存',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 ;INSERT INTO `seata_storage`.`t_storage`(`id`,`product_id`,`total`,`used`,`residue`)
VALUES ('1','1','100','0','100');SELECT * FROM t_storage;

  • seata_account:建立t_account表;
CREATE TABLE `t_account`  (`id` BIGINT(11) NOT NULL AUTO_INCREMENT,`user_id` BIGINT(11) NULL DEFAULT NULL COMMENT '用户id',`total` DECIMAL(10, 0) NULL DEFAULT NULL COMMENT '总额度',`used` DECIMAL(10, 0) NULL DEFAULT NULL COMMENT '已用额度',`residue` DECIMAL(10, 0) NULL DEFAULT NULL COMMENT '剩余可用额度',PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 2 CHARACTER SET = utf8;INSERT INTO `seata_account`.`t_account`(`id`,`user_id`,`total`,`used`,`residue`)
VALUES ('1','1','1000','0','1000');SELECT * FROM t_account;

【c】分别建立对应业务库的回滚日志表

在下载好的seata安装包的conf目录下已经提供对应的sql脚本给我们,直接执行db_undo_log.sql即可。

我们直接拷贝执行即可:

-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

注意:三个业务库都要建立日志回滚记录表。

至此,我们的业务数据库已经准备完毕,接下来就依次搭建各个微服务。

五、Seata之Account账户微服务搭建

【a】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>springcloud2020</artifactId><groupId>com.wsh.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>springcloudalibaba-seata-account-service2003</artifactId><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>com.wsh.springcloud</groupId><artifactId>springcloud-api-commons</artifactId><version>${project.version}</version></dependency></dependencies>
</project>

注意:记得排除spring-cloud-starter-alibaba-seata包自带的 seata-all依赖包,改为我们自己使用的版本0.0.9版本,尽量跟我们Seata服务端版本保持一致。

【b】application.yml: 注意自定义事务组名称,需要与file.conf中配置的名称对应。

server:port: 2003
spring:application:name: springcloudalibaba-seata-account-servicecloud:alibaba:seata:tx-service-group: wsh_tx_group  #自定义事务组名称,需要与file.conf中配置的名称对应(vgroup_mapping.my_test_tx_group = "wsh_tx_group")nacos:discovery:server-addr: localhost:8848  #指定nacos服务器地址datasource:  #mysql数据源连接配置driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_accountusername: rootpassword: root
feign:hystrix:enabled: false
logging:level:io:seata: info
mybatis:mapperLocations: classpath:mapper/*.xml  #指定mybatis mapper文件地址

【c】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->rgroupvgroup_mapping.wsh_tx_group = "default"#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"
}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 = "root"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}
}

【d】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"}
}

【e】domain实体类

package com.wsh.springcloud.alibaba.domain;import java.math.BigDecimal;public class Account {private Long id;/*** 用户id*/private Long userId;/*** 总额度*/private BigDecimal total;/*** 已用额度*/private BigDecimal used;/*** 剩余额度*/private BigDecimal residue;public Account() {}public Account(Long id, Long userId, BigDecimal total, BigDecimal used, BigDecimal residue) {this.id = id;this.userId = userId;this.total = total;this.used = used;this.residue = residue;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getUserId() {return userId;}public void setUserId(Long userId) {this.userId = userId;}public BigDecimal getTotal() {return total;}public void setTotal(BigDecimal total) {this.total = total;}public BigDecimal getUsed() {return used;}public void setUsed(BigDecimal used) {this.used = used;}public BigDecimal getResidue() {return residue;}public void setResidue(BigDecimal residue) {this.residue = residue;}
}

【f】Mapper接口以及实现类

package com.wsh.springcloud.alibaba.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.math.BigDecimal;@Mapper
public interface AccountMapper {/*** 扣减账户余额*/void decreaseAccount(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

对应的Mapper.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.wsh.springcloud.alibaba.mapper.AccountMapper"><resultMap id="BaseResultMap" type="com.wsh.springcloud.alibaba.domain.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="decreaseAccount">UPDATE t_accountSETresidue = residue - #{money},used = used + #{money}WHEREuser_id = #{userId};</update></mapper>

【g】Service接口以及实现: 提供扣减用户账户余额方法

package com.wsh.springcloud.alibaba.service;import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;public interface AccountService {/*** 扣减账户余额** @param userId 用户id* @param money  金额*/void decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

实现类:

package com.wsh.springcloud.alibaba.service.impl;import com.wsh.springcloud.alibaba.mapper.AccountMapper;
import com.wsh.springcloud.alibaba.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.Date;
import java.util.concurrent.TimeUnit;/*** 账户业务实现类* Created by zzyy on 2019/11/11.*/
@Service
public class AccountServiceImpl implements AccountService {private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);@ResourceAccountMapper accountMapper;/*** 扣减账户余额*/public void decreaseAccount(Long userId, BigDecimal money) {logger.info("账户服务扣减余额[decreaseAccount] start....." + new Date());accountMapper.decreaseAccount(userId, money);logger.info("账户服务扣减余额[decreaseAccount] end....." + new Date());}
}

【h】Controller控制层接口

package com.wsh.springcloud.alibaba.controller;import com.wsh.springcloud.alibaba.service.AccountService;
import com.wsh.springcloud.common.JsonResult;
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/decreaseAccount")public JsonResult decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {accountService.decreaseAccount(userId, money);return new JsonResult(200, "扣减账户余额成功!");}
}

【i】自定义数据源配置类:使用seata对数据源进行代理

package com.wsh.springcloud.alibaba.config;import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;/*** @Description 数据源配置类* @Date 2020/9/11 20:30* @Author weishihuai* 说明:  使用Seata对数据源进行代理*/
@Configuration
public class CustomDataSourceProxyConfig {@Bean@ConfigurationProperties(prefix = "spring.datasource")public DruidDataSource druidDataSource() {return new DruidDataSource();}@Primary@Beanpublic DataSourceProxy dataSource(DruidDataSource druidDataSource) {return new DataSourceProxy(druidDataSource);}
}

【j】自定义Mybatis配置类

package com.wsh.springcloud.alibaba.config;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;/*** @Description Mybatis配置类: 指定mapper接口包扫描路径* @Date 2020/9/11 20:30* @Author weishihuai* 说明:*/
@Configuration
@MapperScan({"com.wsh.springcloud.alibaba.mapper"})
public class MyBatisConfig {
}

【k】主启动类

package com.wsh.springcloud.alibaba;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 SpringCloudAlibabaSeataAccountServiceApplication2003 {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSeataAccountServiceApplication2003.class, args);}
}

启动项目,如果正常启动说明账户微服务就算搭建成功了。

六、Seata之Storage库存微服务搭建

【a】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>springcloud2020</artifactId><groupId>com.wsh.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>springcloudalibaba-seata-storage-service2002</artifactId><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>com.wsh.springcloud</groupId><artifactId>springcloud-api-commons</artifactId><version>${project.version}</version></dependency></dependencies>
</project>

【b】application.yml

server:port: 2002spring:application:name: springcloudalibaba-seata-storage-servicecloud:alibaba:seata:tx-service-group: wsh_tx_group   #自定义事务组名称,需要与file.conf中配置的名称对应(vgroup_mapping.my_test_tx_group = "wsh_tx_group")nacos:discovery:server-addr: localhost:8848  #指定nacos注册地址datasource:   #mysql数据源连接配置driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storageusername: rootpassword: root
logging:level:io:seata: info
mybatis:mapperLocations: classpath:mapper/*.xml   #指定mybatis mapper文件地址

【c】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->rgroupvgroup_mapping.wsh_tx_group = "default"#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"
}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 = "root"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}
}

【d】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"}
}

【e】domain实体类

package com.wsh.springcloud.alibaba.domain;/*** @Description 库存实体类* @Date 2020/9/11 20:50* @Author weishihuai* 说明:*/
public class Storage {private Long id;/*** 产品id*/private Long productId;/*** 总库存*/private Integer total;/*** 已用库存*/private Integer used;/*** 剩余库存*/private Integer residue;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getProductId() {return productId;}public void setProductId(Long productId) {this.productId = productId;}public Integer getTotal() {return total;}public void setTotal(Integer total) {this.total = total;}public Integer getUsed() {return used;}public void setUsed(Integer used) {this.used = used;}public Integer getResidue() {return residue;}public void setResidue(Integer residue) {this.residue = residue;}
}

【f】Mapper接口以及实现类

package com.wsh.springcloud.alibaba.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface StorageMapper {/*** 扣减库存** @param productId* @param count*/void decreaseStorage(@Param("productId") Long productId, @Param("count") Integer count);
}

对应的Mapper.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.wsh.springcloud.alibaba.mapper.StorageMapper"><resultMap id="BaseResultMap" type="com.wsh.springcloud.alibaba.domain.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="decreaseStorage">UPDATEt_storageSETused = used + #{count},residue = residue - #{count}WHEREproduct_id = #{productId}</update></mapper>

【g】Service接口以及实现:提供扣减库存方法

package com.wsh.springcloud.alibaba.service;public interface StorageService {/*** 扣减库存*/void decreaseStorage(Long productId, Integer count);
}

实现类:

package com.wsh.springcloud.alibaba.service.impl;import com.wsh.springcloud.alibaba.mapper.StorageMapper;
import com.wsh.springcloud.alibaba.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.Date;@Service
public class StorageServiceImpl implements StorageService {private static final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);@Resourceprivate StorageMapper storageMapper;/*** 扣减库存*/@Overridepublic void decreaseStorage(Long productId, Integer count) {logger.info("库存服务库存扣减[decreaseStorage] start....." + new Date());storageMapper.decreaseStorage(productId, count);logger.info("库存服务库存扣减[decreaseStorage] end....." + new Date());}}

【h】Controller控制层接口

package com.wsh.springcloud.alibaba.controller;import com.wsh.springcloud.alibaba.service.StorageService;
import com.wsh.springcloud.common.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class StorageController {@Autowiredprivate StorageService storageService;/*** 扣减库存*/@RequestMapping("/storage/decreaseStorage")public JsonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {storageService.decreaseStorage(productId, count);return new JsonResult(200, "扣减库存成功......");}
}

【i】自定义数据源配置:使用Seata对数据源进行代理

package com.wsh.springcloud.alibaba.config;import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;/*** @Description 数据源配置类* @Date 2020/9/11 20:30* @Author weishihuai* 说明:  使用Seata对数据源进行代理*/
@Configuration
public class CustomDataSourceProxyConfig {@Bean@ConfigurationProperties(prefix = "spring.datasource")public DruidDataSource druidDataSource() {return new DruidDataSource();}@Primary@Beanpublic DataSourceProxy dataSource(DruidDataSource druidDataSource) {return new DataSourceProxy(druidDataSource);}
}

【j】自定义Mybatis配置

package com.wsh.springcloud.alibaba.config;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;/*** @Description Mybatis配置类: 指定mapper接口包扫描路径* @Date 2020/9/11 20:30* @Author weishihuai* 说明:*/
@Configuration
@MapperScan({"com.wsh.springcloud.alibaba.mapper"})
public class MyBatisConfig {
}

【k】主启动类

package com.wsh.springcloud.alibaba;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 SpringCloudAlibabaSeataStorageServiceApplication2002 {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSeataStorageServiceApplication2002.class, args);}
}

启动项目,如果正常启动说明库存微服务就算搭建成功了。

七、Seata之Order订单微服务搭建

【a】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>springcloud2020</artifactId><groupId>com.wsh.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>springcloudalibaba-seata-order-service2001</artifactId><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>com.wsh.springcloud</groupId><artifactId>springcloud-api-commons</artifactId><version>${project.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
</project>

【b】application.yml

server:port: 2001
spring:application:name: springcloudalibab-seata-order-servicecloud:alibaba:seata:#自定义事务组名称需要与seata-server中的file.conf中配置的名称对应tx-service-group: wsh_tx_groupnacos:discovery:server-addr: localhost:8848  #指定nacos服务器地址datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_orderusername: rootpassword: root
feign:hystrix:enabled: false   #开启feign对Hystrix断路器功能
logging:level:io:seata: info
mybatis:mapperLocations: classpath:mapper/*.xml   #指定mapper.xml文件路径

【c】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->rgroupvgroup_mapping.wsh_tx_group = "default"#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"
}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 = "root"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}
}

【d】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"}
}

【e】domain实体类

package com.wsh.springcloud.alibaba.domain;import java.math.BigDecimal;public class Order {private Long id;private Long userId;private Long productId;private Integer count;private BigDecimal money;private Integer status; //订单状态:0:创建中;1:已完结public Order(Long id, Long userId, Long productId, Integer count, BigDecimal money, Integer status) {this.id = id;this.userId = userId;this.productId = productId;this.count = count;this.money = money;this.status = status;}public Order() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Long getUserId() {return userId;}public void setUserId(Long userId) {this.userId = userId;}public Long getProductId() {return productId;}public void setProductId(Long productId) {this.productId = productId;}public Integer getCount() {return count;}public void setCount(Integer count) {this.count = count;}public BigDecimal getMoney() {return money;}public void setMoney(BigDecimal money) {this.money = money;}public Integer getStatus() {return status;}public void setStatus(Integer status) {this.status = status;}
}

【f】Mapper接口以及实现类

package com.wsh.springcloud.alibaba.mapper;import com.wsh.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface OrderMapper {/*** 新建订单** @param order*/void createOrder(Order order);/*** 修改订单状态已完成** @param userId* @param status*/void updateOrderStatus(@Param("userId") Long userId, @Param("status") Integer status);
}

对应的Mapper.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.wsh.springcloud.alibaba.mapper.OrderMapper"><resultMap id="BaseResultMap" type="com.wsh.springcloud.alibaba.domain.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="createOrder">insert into t_order (id,user_id,product_id,count,money,status)values (null,#{userId},#{productId},#{count},#{money},0);</insert><!--修改订单状态为已完成--><update id="updateOrderStatus">update t_order set status = 1where user_id=#{userId} and status = #{status};</update></mapper>

【g】Service接口以及实现:提供创建订单方法

package com.wsh.springcloud.alibaba.service;import com.wsh.springcloud.alibaba.domain.Order;/*** @Description 订单业务层接口* @Date 2020/9/11 20:13* @Author weishihuai* 说明:*/
public interface OrderService {/*** 创建订单方法** @param order*/void createOrder(Order order);
}

实现类:

package com.wsh.springcloud.alibaba.service.impl;import com.wsh.springcloud.alibaba.domain.Order;
import com.wsh.springcloud.alibaba.feign.AccountFeignClient;
import com.wsh.springcloud.alibaba.feign.StorageFeignClient;
import com.wsh.springcloud.alibaba.mapper.OrderMapper;
import com.wsh.springcloud.alibaba.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.Date;/*** @Description 订单业务层实现类* @Date 2020/9/11 20:37* @Author weishihuai* 说明:*/
@Service
public class OrderServiceImpl implements OrderService {private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);@Resourceprivate OrderMapper orderMapper;@Resourceprivate StorageFeignClient storageFeignClient;@Resourceprivate AccountFeignClient accountFeignClient;/*** 订单服务创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态*/@Overridepublic void createOrder(Order order) {logger.info("创建订单[createOrder] start....." + new Date());//1 新建订单orderMapper.createOrder(order);logger.info("创建订单[createOrder] end....." + new Date());//2 扣减库存logger.info("调用库存服务扣减库存[decreaseStorage] start....." + new Date());storageFeignClient.decreaseStorage(order.getProductId(), order.getCount());logger.info("调用库存服务扣减库存[decreaseStorage] end....." + new Date());//3 扣减账户logger.info("调用账户服务扣减余额[decreaseAccount] start....." + new Date());accountFeignClient.decreaseAccount(order.getUserId(), order.getMoney());logger.info("调用账户服务扣减余额[decreaseAccount] end....." + new Date());//4 修改订单状态,从零到1, 1代表已经完成logger.info("修改订单状态[updateOrderStatus] start....." + new Date());orderMapper.updateOrderStatus(order.getUserId(), 0);logger.info("修改订单状态[updateOrderStatus] end....." + new Date());}
}

【h】库存微服务Feign远程调用以及失败回调

package com.wsh.springcloud.alibaba.feign;import com.wsh.springcloud.alibaba.feign.fallback.StorageFeignClientFallback;
import com.wsh.springcloud.common.JsonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;/*** @Description 库存Feign远程服务调用接口* @Date 2020/9/11 20:17* @Author weishihuai* 说明:*/
@FeignClient(value = "springcloudalibaba-seata-storage-service", fallback = StorageFeignClientFallback.class)
public interface StorageFeignClient {/*** 扣减库存方法** @param productId* @param count* @return*/@PostMapping(value = "/storage/decreaseStorage")Object decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);}
package com.wsh.springcloud.alibaba.feign.fallback;import com.wsh.springcloud.alibaba.feign.StorageFeignClient;
import com.wsh.springcloud.common.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;@Component
public class StorageFeignClientFallback implements StorageFeignClient {private static Logger logger = LoggerFactory.getLogger(StorageFeignClientFallback.class);@Overridepublic JsonResult decreaseStorage(Long productId, Integer count) {logger.error("远程调用库存服务扣减库存[decreaseStorage]异常...");return null;}
}

【i】账户微服务Feign远程调用以及失败回调

package com.wsh.springcloud.alibaba.feign;import com.wsh.springcloud.alibaba.feign.fallback.AccountFeignClientFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;/*** @Description 账户Feign远程服务调用接口* @Date 2020/9/11 20:16* @Author weishihuai* 说明:*/
@FeignClient(value = "springcloudalibaba-seata-account-service", fallback = AccountFeignClientFallback.class)
public interface AccountFeignClient {/*** 扣减用户余额方法** @param userId* @param money* @return*/@PostMapping(value = "/account/decreaseAccount")Object decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);}
package com.wsh.springcloud.alibaba.feign.fallback;import com.wsh.springcloud.alibaba.feign.AccountFeignClient;
import com.wsh.springcloud.common.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import java.math.BigDecimal;@Component
public class AccountFeignClientFallback implements AccountFeignClient {private static Logger logger = LoggerFactory.getLogger(AccountFeignClientFallback.class);@Overridepublic JsonResult decreaseAccount(Long userId, BigDecimal money) {logger.error("远程调用账户服务扣减余额[decreaseAccount]异常...");return null;}
}

【j】Controller控制层接口

package com.wsh.springcloud.alibaba.controller;import com.wsh.springcloud.alibaba.domain.Order;
import com.wsh.springcloud.alibaba.service.OrderService;
import com.wsh.springcloud.common.JsonResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** @Description 订单控制层接口* @Date 2020/9/11 20:36* @Author weishihuai* 说明:*/
@RestController
public class OrderController {@Resourceprivate OrderService orderService;@GetMapping("/order/createOrder")public JsonResult createOrder(Order order) {orderService.createOrder(order);return new JsonResult(200, "订单创建成功.....");}}

【k】自定义数据源配置:使用Seata对数据源进行代理

package com.wsh.springcloud.alibaba.config;import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;/*** @Description 数据源配置类* @Date 2020/9/11 20:30* @Author weishihuai* 说明:  使用Seata对数据源进行代理*/
@Configuration
public class CustomDataSourceProxyConfig {@Bean@ConfigurationProperties(prefix = "spring.datasource")public DruidDataSource druidDataSource() {return new DruidDataSource();}@Primary@Beanpublic DataSourceProxy dataSource(DruidDataSource druidDataSource) {return new DataSourceProxy(druidDataSource);}
}

【l】自定义Mybatis配置

package com.wsh.springcloud.alibaba.config;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;/*** @Description Mybatis配置类: 指定mapper接口包扫描路径* @Date 2020/9/11 20:30* @Author weishihuai* 说明:*/
@Configuration
@MapperScan({"com.wsh.springcloud.alibaba.mapper"})
public class MyBatisConfig {
}

【j】主启动类

package com.wsh.springcloud.alibaba;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 SpringCloudAlibabaSeataOrderServiceApplication2001 {public static void main(String[] args) {SpringApplication.run(SpringCloudAlibabaSeataOrderServiceApplication2001.class, args);}
}

启动项目,如果正常启动说明订单微服务就算搭建成功了。

八、Seata之@GlobalTransactional验证

【a】测试前首先记录数据库初始数据

【b】正常下单

启动订单、库存、账户三个微服务,浏览器访问:http://localhost:2001/order/createOrder?userId=1&productId=1&count=10&money=100

可以看到, 订单已经创建成功,然后我们观察各个数据库数据:

订单表正常生成订单,并且状态为1

库存表扣减了10个,剩余90个

账户余额表扣减100块,剩余900块

观察后端日志:

2020-09-12 14:47:43.318  INFO 6552 --- [nio-2001-exec-1] c.w.s.a.service.impl.OrderServiceImpl    : 创建订单[createOrder] start.....Sat Sep 12 14:47:43 CST 2020
c.w.s.a.service.impl.OrderServiceImpl    : 创建订单[createOrder] end.....Sat Sep 12 14:47:43 CST 2020
2020-09-12 14:47:43.813  INFO 6552 --- [nio-2001-exec-1] c.w.s.a.service.impl.OrderServiceImpl    : 调用库存服务扣减库存[decreaseStorage] start.....Sat Sep 12 14:47:43 CST 2020
c.w.s.a.service.impl.OrderServiceImpl    : 调用库存服务扣减库存[decreaseStorage] end.....Sat Sep 12 14:47:47 CST 2020
2020-09-12 14:47:47.420  INFO 6552 --- [nio-2001-exec-1] c.w.s.a.service.impl.OrderServiceImpl    : 调用账户服务扣减余额[decreaseAccount] start.....Sat Sep 12 14:47:47 CST 2020
c.w.s.a.service.impl.OrderServiceImpl    : 调用账户服务扣减余额[decreaseAccount] end.....Sat Sep 12 14:47:48 CST 2020
2020-09-12 14:47:48.595  INFO 6552 --- [nio-2001-exec-1] c.w.s.a.service.impl.OrderServiceImpl    : 修改订单状态[updateOrderStatus] start.....Sat Sep 12 14:47:48 CST 2020
2020-09-12 14:47:49.089  INFO 6552 --- [nio-2001-exec-1] c.w.s.a.service.impl.OrderServiceImpl    : 修改订单状态[updateOrderStatus] end.....Sat Sep 12 14:47:49 CST 2020

【c】超时异常,没有加@GlobalTransactional注解

下面我们在账户微服务中模拟超时异常,AccountServiceImpl.java加入休眠,具体如下:

public void decreaseAccount(Long userId, BigDecimal money) {logger.info("账户服务扣减余额[decreaseAccount] start....." + new Date());//feign调用默认调用超时时间为1秒,这里模拟休眠20秒,肯定调用超时//模拟超时异常,全局事务回滚try {TimeUnit.SECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}accountMapper.decreaseAccount(userId, money);logger.info("账户服务扣减余额[decreaseAccount] end....." + new Date());}

重启账户微服务,浏览器再次访问:http://localhost:2001/order/createOrder?userId=1&productId=1&count=10&money=100

观察数据库数据:

订单表生成一条订单记录,并且状态为0

库存扣减10件,剩余80件

账户余额扣减100,剩余800

从上面的数据库数据可以看到,库存和账户金额扣减后,订单状态并没有变成已完成,没有从0变为1,并且由于feign远程调用的重试机制,账户余额还有可能被多次扣减。这其实就是因为我们的事务不同步导致的,下面我们加上@GlobalTransactional注解测试一下。

【d】超时异常,加了@GlobalTransactional注解

@GlobalTransactional注解源码如下:

/*** The interface Global transactional.*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface GlobalTransactional {/*** 全局事务超时时间  单位毫秒** @return timeoutMills in MILLISECONDS.*/int timeoutMills() default TransactionInfo.DEFAULT_TIME_OUT;/*** 全局事务的名称** @return Given name.*/String name() default "";/*** 执行哪些异常需要回滚,指定xxx.class* @return*/Class<? extends Throwable>[] rollbackFor() default {};/***  指定哪些异常类需要回滚,指定类名称* @return*/String[] rollbackForClassName() default {};/*** 指定哪些异常类不需要回滚,指定xxx.class* @return*/Class<? extends Throwable>[] noRollbackFor() default {};/*** 指定哪些异常类不需要回滚,指定类名称* @return*/String[] noRollbackForClassName() default {};}

OrderServiceImpl.java中创建订单的方法createOrder方法上面加上@GlobalTransactional注解:

name属性表示全局事务的名称,一般可以随意取,但要保证唯一性。

rollbackFor属性表示需要回滚的异常类。

 @Override@GlobalTransactional(name = "wsh-create-order", rollbackFor = Exception.class)public void createOrder(Order order) {logger.info("创建订单[createOrder] start....." + new Date());

重启订单微服务,浏览器再次访问:http://localhost:2001/order/createOrder?userId=1&productId=1&count=10&money=100

观察数据库数据:

账户微服务超时异常,导致全局事务回滚,从上面的数据数据可以看到,加了@GlobalTransactional注解后,这一次并没有生成新的订单,并且库存和账户余额也没有变动,成功控制了数据一致性的问题。

九、总结

本篇文章主要介绍了Spring Cloud Alibaba Seata组件如何实现分布式事务控制,并且通过模拟用户下单->扣减库存->扣减用户余额->修改订单状态为已完成的业务流程,详细说明了如何使用Seata控制分布式事务,保证数据在分布式事务中的一致性。此篇篇幅较大,小伙伴们还是需要耐心,自己手敲一遍实现一次,其中肯定会踩很多坑,笔者也不例外,这样印象会比较深刻。

以上相关项目的代码我已经放在Gitee上,有需要的小伙伴可以去拉取进行学习:https://gitee.com/weixiaohuai/springcloud_Hoxton,由于笔者水平有限,如有不对之处,还请小伙伴们指正,相互学习,一起进步。

下面是笔者总结的关于Spring Cloud Alibaba教程系列文章目录,有需要的小伙伴可以前往学习:

1. Spring Cloud Alibaba入门简介

2. Spring Cloud Alibaba Nacos之服务注册中心

3. Spring Cloud Alibaba Nacos之服务配置中心

4. Spring Cloud Alibaba Nacos集群和持久化配置

5. Spring Cloud Alibaba Sentinel之入门篇

6. Spring Cloud Alibaba Sentinel之流控规则篇

7. Spring Cloud Alibaba Sentinel之服务降级篇

8. Spring Cloud Alibaba Sentinel之热点参数限流篇

9. Spring Cloud Alibaba @SentinelResource配置详解

10. Spring Cloud Alibaba Sentinel之服务熔断篇

11. Spring Cloud Alibaba Sentinel之持久化篇

12. Spring Cloud Alibaba Seata处理分布式事务及案例实战

13. Spring Cloud Alibaba Seata工作原理

Spring Cloud Alibaba Seata处理分布式事务及案例实战相关推荐

  1. Spring Cloud Alibaba系列之分布式事务Seata

    Spring Cloud Alibaba系列之分布式事务Seata 1.分布式事务 分布式事务不是在现在微服务分布式架构上才产生的问题,在单体应用同样存在分布式事务问题,典型的场景就是单体应用使用了多 ...

  2. Spring Cloud 整合 seata 实现分布式事务极简入门

    Spring Cloud 整合 seata 实现分布式事务极简入门 seata Spring Cloud 整合 seata 实现分布式事务极简入门 1. 概述 2. 部署nacos 3. 部署seat ...

  3. Spring Cloud Alibaba 高级特性 分布式事务:Alibaba Seata 如何实现分布式事务

    本讲咱们要解决分布式事务这一技术难题,这一讲咱们将介绍三方面内容: 讲解分布式事务的解决方案: 介绍 Alibaba Seata 分布式事务中间件: 分析 Seata 的 AT 模式实现原理. 分布式 ...

  4. Spring Cloud入门-Seata处理分布式事务问题(Hoxton版本)

    我们先从官网下载seata-server,这里下载的是seata-server-1.0.0.zip,下载地址:https://github.com/seata/seata/releases 这里我们使 ...

  5. Dubbo的疑难解答:Spring Cloud Alibaba系列之分布式服务组件

    1.分布式理论 1.1.分布式基本定义 <分布式系统原理与范型>定义: "分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统" 分布式系统(dis ...

  6. Spring Cloud Alibaba —— Seata 分布式事务框架

    导航 一.Seata 介绍 二.Seata 的工作原理 2.1 三个角色 2.2 工作流程 三.Seata AT 工作机制 3.1 一阶段 3.2 二阶段 四.案例演示(待补充) 一.Seata 介绍 ...

  7. SpringCloud Alibaba Seata处理分布式事务-微服务(三十九)

    订单/库存/账户业务微服务准备 业务需求 下订单->减库存->扣余额->改(订单)状态 新建订单Order-Module seata-order-service2001 POM &l ...

  8. spring cloud: TX-LCN解决分布式事务

    分布式事务预备知识 1.本地事务的ACID A:原子性(Atomicity) 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节. 事务在执行过程中发 ...

  9. 【SpringCloud】Spring cloud Alibaba seata 分布式事务

    1.概述 官网:http://seata.io/zh-cn/ 术语官网:http://seata.io/zh-cn/docs/overview/terminology.html 分布式全局事务ID 加 ...

  10. 如何成为 Spring Cloud Alibaba committer ?

    简介:Spring Cloud Alibaba 开源两年时间,已经成为了最受开发者关注.最活跃的 Spring Cloud 实现.它之所以能这么快的受到开发者的认可,一方面是它生态中的组件丰富且经过阿 ...

最新文章

  1. 洛谷P2327 [SCOI2005] 扫雷
  2. CentOS系统设置自动登录
  3. maven <type>pom</type><scope>import</scope>
  4. android读取本地yaml文件_python笔记11-读取yaml配置文件(pyyaml)
  5. 科大星云诗社动态20210516
  6. eclipse插件egit安装使用
  7. /usr/lib/deepin-wine/wine: error while loading shared libraries: libwine.so.1
  8. 程序员每天该做的事情
  9. 股票期货数据的resample处理
  10. 程序员的进阶课-架构师之路(大纲)-思维导图
  11. ios8中百度推送接收不到
  12. matlab中常微分方法,MATLAB解常微分方程组的解法(好东西要共享)
  13. Xcode因为证书问题经常报的那些错
  14. 未来的几年,不可能再有岁月静好
  15. python解析库 爬虫_Python 爬虫 解析库的使用 --- XPath
  16. Dijkstra算法,起点到当前点的最短距离及路径 C++实现
  17. C语言入门之C语言开发环境搭建
  18. html中页面整体排版,html在不同尺寸浏览器窗口中页面排版混乱
  19. windows server 2012 R2密码恢复
  20. [Scala基础]--Either介绍

热门文章

  1. C/C++[PAT B level 1004,1012]
  2. 自动驾驶 2-1 第 1 课补充阅读:传感器和计算硬件 -- 下
  3. Google Code Review在代码审查中寻找什么
  4. 直击架构本质:优秀架构师必须掌握的几种架构思维
  5. 2021-09-08FTRL 跟随正确的领导者
  6. 逻辑斯蒂回归模型为什么用sigmoid函数
  7. linux环境Mechanize安装,在linux下安装activepython2.5 setuptools ClientCookie
  8. linux 桥接stp原理,Linux 中的网桥技术
  9. Django MySQL 多用户_django使用多个数据库的方法实例
  10. 桥式整流以及电容作用