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

目录

一、分布式事务由来

二、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-service
  cloud:
    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.Driver
    url: jdbc:mysql://localhost:3306/seata_account
    username: root
    password: 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-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  vgroup_mapping.wsh_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## 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 = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}

【d】registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "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 = 6000
    connect.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、etcd3
  type = "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 = 6000
    connect.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_account
        SET
          residue = residue - #{money},used = used + #{money}
        WHERE
          user_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);

    @Resource
    AccountMapper 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 {

    @Resource
    AccountService 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
    @Bean
    public 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: 2002

spring:
  application:
    name: springcloudalibaba-seata-storage-service
  cloud:
    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.Driver
    url: jdbc:mysql://localhost:3306/seata_storage
    username: root
    password: root
logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml   #指定mybatis mapper文件地址


 

【c】file.conf

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  vgroup_mapping.wsh_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## 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 = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}

【d】registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "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 = 6000
    connect.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、etcd3
  type = "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 = 6000
    connect.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">
        UPDATE
            t_storage
        SET
            used = used + #{count},residue = residue - #{count}
        WHERE
            product_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);

    @Resource
    private StorageMapper storageMapper;

    /**
     * 扣减库存
     */
    @Override
    public 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 {

    @Autowired
    private 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
    @Bean
    public 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-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的file.conf中配置的名称对应
        tx-service-group: wsh_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848  #指定nacos服务器地址
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 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-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  vgroup_mapping.wsh_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## 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 = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}

【d】registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "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 = 6000
    connect.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、etcd3
  type = "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 = 6000
    connect.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 = 1
        where 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);

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageFeignClient storageFeignClient;
    @Resource
    private AccountFeignClient accountFeignClient;

    /**
     * 订单服务创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     */
    @Override
    public 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);

    @Override
    public 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);

    @Override
    public 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 {
    @Resource
    private 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
    @Bean
    public 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工作原理

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值