分布式事务管理

文章目录

    • :orange: `seata` 简介
    • :tomato: 安装 `seata-server`
    • :potato: 案例
        • :strawberry: 准备
        • :strawberry: 创建 `account` 模块
        • :strawberry: 创建 `order` 模块
        • :strawberry: 创建 `storage` 模块
        • :strawberry: 创建 `business` 模块

seata 简介

分布式事务管理_第1张图片

  • TC (Transaction Coordinator) - 事务协调者
    维护全局和分支事务的状态,驱动全局事务提交或回滚。

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

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


四种事务模式:

  • AT 模式:提供无侵入自动补偿的事务模式。自动补偿的 sql 是系统生成的。
  • TCC 模式: 反向补偿的 sql 需要自己手动去写。
  • XA 模式:支持已实现 XA 接口的数据库的 XA 模式。
  • SAGA 模式:为长事务提供有效的解决方案,提供编排式与注解式。

自动补偿(又称反向补偿): 例如有 a,b,c 三个服务,现在 a 分别调用服务 bc,为了确保 bc 的调用同时成功或者同时失败,那么就要使用分布式事务。假设先调用 b 在调用 c, b 调用完成之后,事务就提交了,然后调用 cc 服务出错,那么现在需要回滚。此时 b 需要回滚,但是回滚并不是我们传统意义上的回滚,而是通过一条 sqlb 服务中的数据进行复原,这个过程就是反向补偿。


安装 seata-server

seata 所提供的 seata-server 本质上就是⼀个 SpringBoot

  1. 下载地址: https://github.com/seata/seata/releases/tag/v1.5.2
  2. 启动 bin 目录下的 seata-server.bat 文件
  3. 本地启动后访问地址:http://localhost:7091/ ,7091 是后台管理页面访问的端口,7091 是通信端口。
  4. 配置文件:application.example.yml 案例的配置文件

分布式事务管理_第2张图片
6. 如果存储是 db 形式,数据库脚本的位置:
分布式事务管理_第3张图片


分布式事务管理_第4张图片

案例

准备

地址:https://seata.io/zh-cn/docs/user/quickstart.html

分布式事务管理_第5张图片

新建两个数据库,并在每个数据库中创建 UNDO_LOG

account 库:

# ************************************************************
# Sequel Pro SQL dump
# Version 5446
#
# https://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 127.0.0.1 (MySQL 5.7.26)
# Database: account
# Generation Time: 2022-06-04 04:01:18 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
SET NAMES utf8mb4;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table account_tbl
# ------------------------------------------------------------

DROP TABLE IF EXISTS `account_tbl`;

CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `account_tbl` WRITE;
/*!40000 ALTER TABLE `account_tbl` DISABLE KEYS */;

INSERT INTO `account_tbl` (`id`, `user_id`, `money`)
VALUES
	(1,'javaboy',999000);

/*!40000 ALTER TABLE `account_tbl` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table undo_log
# ------------------------------------------------------------

DROP TABLE IF EXISTS `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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;




/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

order 库:

# ************************************************************
# Sequel Pro SQL dump
# Version 5446
#
# https://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 127.0.0.1 (MySQL 5.7.26)
# Database: order
# Generation Time: 2022-06-04 04:01:35 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
SET NAMES utf8mb4;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table order_tbl
# ------------------------------------------------------------

DROP TABLE IF EXISTS `order_tbl`;

CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `order_tbl` WRITE;
/*!40000 ALTER TABLE `order_tbl` DISABLE KEYS */;

INSERT INTO `order_tbl` (`id`, `user_id`, `commodity_code`, `count`, `money`)
VALUES
	(16,'javaboy','1111',10,1000),
	(17,'javaboy','1111',10,1000),
	(18,'javaboy','1111',10,1000),
	(20,'javaboy','1111',10,1000);

/*!40000 ALTER TABLE `order_tbl` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table undo_log
# ------------------------------------------------------------

DROP TABLE IF EXISTS `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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;




/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

storage 库:

# ************************************************************
# Sequel Pro SQL dump
# Version 5446
#
# https://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 127.0.0.1 (MySQL 5.7.26)
# Database: storage
# Generation Time: 2022-06-04 04:01:47 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
SET NAMES utf8mb4;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table storage_tbl
# ------------------------------------------------------------

DROP TABLE IF EXISTS `storage_tbl`;

CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `storage_tbl` WRITE;
/*!40000 ALTER TABLE `storage_tbl` DISABLE KEYS */;

INSERT INTO `storage_tbl` (`id`, `commodity_code`, `count`)
VALUES
	(2,'1111',60);

/*!40000 ALTER TABLE `storage_tbl` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table undo_log
# ------------------------------------------------------------

DROP TABLE IF EXISTS `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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;




/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

seata AT 模式需要 UNDO_LOG 表

**-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  -- 分支事务的 id
  `branch_id` bigint(20) NOT NULL,
  -- 全局事务的 id
  `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;**

分布式事务管理_第6张图片

新建 seata-axmaven 项目,并在该项目下创建子模块(spring boot项目):

分布式事务管理_第7张图片

创建 eurekaspirng boot 项目,并添加如下依赖:

    <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        dependency>

添加配置:

# 应用名称
spring.application.name=eureka

# 应用服务 WEB 访问端口
server.port=8761

# 不获取其他的服务,也不将自己注册到其他的 eureka 上面去
eureka.client.fetch-registry=false

eureka.client.register-with-eureka=false

启动类上添加开启 eureka 的注解:

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }

}

创建 maven 项目,common 模块:

创建同一的响应处理类:

public class RespBean {

    private Integer status;

    private  String msg;

    private Object data;

    public static RespBean ok(String msg,Object data){
        return  new RespBean(200,msg,data);
    }

    public static RespBean ok(String msg){
        return  new RespBean(200,msg,null);
    }

    public static RespBean error(String msg,Object data){
        return  new RespBean(400,msg,data);
    }

    public static RespBean error(String msg){
        return  new RespBean(200,msg,null);
    }

    public RespBean() {
    }

    public RespBean(Integer status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

创建全局异常处理类:

@RestControllerAdvice
public class GlobalException {

    @ExceptionHandler(RuntimeException.class)
    // 给一个错误的状态码,让事务回滚
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RespBean runtimeException(RuntimeException e){
        return RespBean.error(e.getMessage());
    }
}


创建 account 模块

添加项目依赖:

分布式事务管理_第8张图片


        <dependency>
            <groupId>org.javaboygroupId>
            <artifactId>commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        dependency>

        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>

添加配置:

# 应用名称
spring.application.name=account

# 应用服务 WEB 访问端口
server.port=1111


#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=org.javaboy.account.mybatis.entity

# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://192.168.10.10:3306/account?serverTimezone=UTC
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=root

# 注册到 eureka 上
eureka.client.service-url.defalutZone=http://localhost:8761/eureka

# seata 事务分组信息
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group


resource 文件目录下添加 file.confregistry.conf 文件,将 account 项目注册到 seata TC(全局事务协调者)

file.conf 文件:

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.my_test_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

registry.conf 文件:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  nacos {
    application = "seata-server"
    serverAddr = "localhost"
    namespace = ""
    username = ""
    password = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    timeout = "0"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

添加 AccountMapper 接口:

@Mapper
@Repository
public interface AccountMapper {

   @Update("update account_tbl set money=money-#{money} where user_id=#{account} ")
   int  updateAccount(@Param("account") String account,@Param("money") double money);

   @Select("select money from account_tbl where user_id =#{account} ")
   double getMoneyByAccount(String account);
}

添加 AccountService

@Service
public class AccountService {

    @Autowired
    private AccountMapper accountMapper;

    // 扣款接口
    public boolean deducAccount(String account,Double money){
        accountMapper.updateAccount(account,money);
        double m = accountMapper.getMoneyByAccount(account);
        if (m>=0){
            return true;
        }
        throw new RuntimeException("账户余额不足扣款失败!");
    }
}

添加 AccountController 接口:

@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    @PostMapping("/deductAccount")
    public RespBean deduct(String account,Double money){
        if (accountService.deducAccount(account,money)){
            return RespBean.ok("扣款成功!");
        }
        return RespBean.error("口款失败!");
    }
}



创建 order 模块

添加依赖:

分布式事务管理_第9张图片


        <dependency>
            <groupId>org.javaboygroupId>
            <artifactId>commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        dependency>

        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>

添加 配置:

# 应用名称
spring.application.name=order

# 应用服务 WEB 访问端口
server.port=1113


#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=org.javaboy.order.mybatis.entity

# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://192.168.10.10:3306/order?serverTimezone=UTC
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=root

# 注册到 eureka 上
eureka.client.service-url.defalutZone=http://localhost:8761/eureka

# seata 事务分组信息
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group

resource 文件目录下添加 file.confregistry.conf 文件。(文件和上面模块一样此处省略…)

启动类上添加 @EnableEurekaClient @EnableFeignClients 注解。

创建 AccountFeign 调用 account 模块中的接口:

@FeignClient("ACCOUNT")
public interface AccountFeign {

    @PostMapping("/deductAccount")
    RespBean deduct(@RequestParam("account") String account, @RequestParam("money") Double money);
}

创建 OrderMapper 接口:

@Mapper
@Repository
public interface OrderMapper {

    @Insert("insert into order_tbl(user_id,commodity_code,count,money) values(#{userId},#{commodityCode},#{count},#{money})")
    int addOrder(@Param("userId") String userId, @Param("commodityCode") String commodityCode,
                 @Param("count") Integer count,@Param("money") Double money);
}

创建 Orderservice 类:

@Service
public class Orderservice {

    @Autowired
    private AccountFeign accountFeign;

    @Autowired
    private OrderMapper orderMapper;

    /**
     *  创建订单
     * @param account 账号
     * @param productId 商品 id
     * @param count 购买数量
     * @return
     */
    public  boolean createOrder(String account,String productId,Integer count){
        // 先去扣款,假设每件商品都是 100 元
        RespBean respBean = accountFeign.deduct(account, count * 100.0);
        int i = orderMapper.addOrder(account, productId, count, count * 100.0);
        return  i==1 && respBean.getStatus() == 200;
    }
}

创建 OrderController 类:

@RestController
public class OrderController {

    @Autowired
    private Orderservice orderservice;

    @PostMapping("createOrder")
    public RespBean creatOrder(String account,String productId,Integer count){
        if (orderservice.createOrder(account,productId,count)){
            return RespBean.ok("下单成功!~");
        }
        return RespBean.error("下单失败!");
    }
}


创建 storage 模块

添加依赖:

分布式事务管理_第10张图片


        <dependency>
            <groupId>org.javaboygroupId>
            <artifactId>commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        dependency>

        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>

添加配置:

# 应用名称
spring.application.name=storage

# 应用服务 WEB 访问端口
server.port=1114


#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=org.javaboy.storage.mybatis.entity

# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://192.168.10.10:3306/storage?serverTimezone=UTC
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=root

# 注册到 eureka 上
eureka.client.service-url.defalutZone=http://localhost:8761/eureka

# seata 事务分组信息
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group

resource 文件目录下添加 file.confregistry.conf 文件。(文件和上面模块一样此处省略…)

创建 StorageMapper 接口:

@Mapper
@Repository
public interface StorageMapper {

    @Update("update storage_tbl set count=count-#{count} where commodity_code=#{productId}")
    int deductStorage(@Param("productId") String productId, @Param("count") Integer count);

    @Select("select count from storage_tbl where commodity_code=#{commodityCode}")
    int getCountByCommodityCode(String commodityCode);
}

创建 StorageService 业务类:

@Service
public class StorageService {

    @Autowired
    private StorageMapper storageMapper;

    public boolean deduct(String productId,Integer count){
        int i = storageMapper.deductStorage(productId, count);
        int resut = storageMapper.getCountByCommodityCode(productId);
        if (resut>=0){
            return true;
        }
        throw new RuntimeException("库存不足,扣库存失败!");
    }
}

创建接口类:

@RestController
public class StorageController {

    @Autowired
    private StorageService storageService;

    @PostMapping("deduct")
    public RespBean deduct(String prodectId,Integer count){
        if (storageService.deduct(prodectId,count)){
            return  RespBean.ok("扣库存成功!");
        }
        return RespBean.error("扣库存失败!");
    }
}


创建 business 模块

添加项目依赖:


        <dependency>
            <groupId>org.javaboygroupId>
            <artifactId>commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>

添加配置:

# 应用名称
spring.application.name=business

# 应用服务 WEB 访问端口
server.port=1112

# 注册到 eureka 上
eureka.client.service-url.defalutZone=http://localhost:8761/eureka

# seata 事务分组信息
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group

启动类添加注解:

@EnableFeignClients
@EnableDiscoveryClient

添加 StorageFeignOrderFeign 接口:

@FeignClient("STORAGE")
public interface StorageFeign {

    @PostMapping("/deduct")
    RespBean deduct(@RequestParam("prodectId") String prodectId, @RequestParam("count") Integer count);
}

@FeignClient("ORDER")
public interface OrderFeign {

    @PostMapping("/createOrder")
    RespBean creatOrder(@RequestParam("account") String account, @RequestParam("productId")String productId,@RequestParam("count") Integer count);
}

添加业务类,并添加全局异常处理的注解:

@Service
public class BusinessService {

    @Autowired
    private StorageFeign storageFeign;

    @Autowired
    private OrderFeign orderFeign;

    @GlobalTransactional
    public void purchase(String account,String productId,Integer count){
        orderFeign.creatOrder(account,productId,count);
        storageFeign.deduct(productId,count);
    }
}

创建 BusinessController 接口:

@RestController
public class BusinessController {

    @Autowired
    private BusinessService businessService;


    @PostMapping("order")
    public RespBean order(String account, String productId, Integer count) {
        try {
            businessService.purchase(account, productId, count);
            return RespBean.ok("下单成功!");
        } catch (Exception e) {
            e.printStackTrace();
            return RespBean.error("下单失败!");
        }
    }
}

测试:

分布式事务管理_第11张图片

你可能感兴趣的:(spring,boot,tienchin,数据库,服务器,java)