【第36章】Spring Cloud之Seata分布式事务

news2024/11/15 11:05:29

文章目录

  • 前言
  • 一、架构图
    • 1. 介绍
    • 2. 项目结构
    • 3. 功能描述
  • 二、用例
    • 1. 准备
      • 1.1 系统表
      • 1.2 业务表
      • 1.3 初始化数据
    • 2. 项目搭建
      • 2.1 项目结构
      • 2.2 主要依赖
      • 2.3 主要配置
  • 三、主要业务代码
    • 1. 仓储服务
      • 1.1 controller
      • 1.2 service
      • 1.3 dao
    • 2. 订单服务
      • 1.1 controller
      • 1.2 service
      • 1.3 dao
    • 3. 帐户服务
      • 1.1 controller
      • 1.2 service
      • 1.3 dao
    • 4. 商品服务
      • 1.1 controller
      • 1.2 service
      • 1.3 rest
  • 四、业务单元测试
    • 1. 准备
    • 2. 成功测试
      • 2.1 结果
      • 2.2 日志
    • 3. 失败测试
      • 3.1 数据重置
      • 3.2 结果
      • 3.3 日志
      • 3.4 控制台
  • 总结


前言

上一章我们已经搭建好了Seata服务端,这里我们根据官方案例来完成分布式事务。


一、架构图

1. 介绍

在这里插入图片描述
我们以传统电商购物系统作为案例,我们有4个服务分别是

  • Business(商品服务)
  • Storage(仓储服务)
  • Order(订单服务)
  • Account(帐户服务)

2. 项目结构

seata-server负责管理Maven依赖及版本管理,4个子SpringBoot服务,负责各自的职责,共同完成整个下单的流程。

在这里插入图片描述

3. 功能描述

这里我们主要使用openfeign(服务调用)+mybatis(数据存储),使用Seata的AT模式完成分布式事务。

二、用例

1. 准备

1.1 系统表

Seata AT 模式 需要使用到 undo_log 表。

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

1.2 业务表

CREATE TABLE IF NOT EXISTS  `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`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS  `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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS  `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 AUTO_INCREMENT=1  DEFAULT CHARSET=utf8;

1.3 初始化数据

delete from account_tbl;
delete from order_tbl;
delete from storage_tbl;
insert into account_tbl(user_id,money) values('U100001','10000');
insert into storage_tbl(commodity_code,count) values('C00321','100');

2. 项目搭建

这里我们主要以账户服务为主,内容过多其他内容不便演示

2.1 项目结构

在这里插入图片描述

2.2 主要依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.3 主要配置

应用配置(application.yml)

base:
  config:
    mdb:
      hostname: 127.0.0.1 #your mysql server ip address
      dbname: seata #your database name for test
      port: 3306 #your mysql server listening port
      username: seata #your mysql server username
      password: seata #your mysql server password
server:
  port: 18084
spring:
  application:
    name: account-service
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_SERVER_ADDR}
        namespace: ${NACOS_NAMESPACE}
        username: ${NACOS_USERNAME}
        password: ${NACOS_PASSWORD}
        group: SEATA_GROUP
  datasource:
    name: storageDataSource
    #    druid don't support GraalVM now because of there is CGlib proxy
    #    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${base.config.mdb.hostname}:${base.config.mdb.port}/${base.config.mdb.dbname}?useSSL=false&serverTimezone=UTC
    username: ${base.config.mdb.username}
    password: ${base.config.mdb.password}
#    druid:
#      max-active: 20
#      min-idle: 2
#      initial-size: 2
seata:
  enabled: true
  application-id: ${spring.application.name}
#  高可用:应用级别的控制,可动态切换
#  tx-service-group: ${spring.application.name}-tx-group
  tx-service-group: default_tx_group
  config:
    type: nacos
    nacos:
      dataId: "seata-client.yaml"
      server-addr: ${NACOS_SERVER_ADDR}
      namespace: ${NACOS_NAMESPACE}
      username: ${NACOS_USERNAME}
      password: ${NACOS_PASSWORD}
  registry:
    type: nacos
    nacos:
      server-addr: ${NACOS_SERVER_ADDR}
      namespace: ${NACOS_NAMESPACE}
      username: ${NACOS_USERNAME}
      password: ${NACOS_PASSWORD}
      group: SEATA_GROUP
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

客户端配置(seata-client.yaml)

seata:
  enabled: true
  application-id: applicationName
  tx-service-group: default_tx_group
  enable-auto-data-source-proxy: true
  data-source-proxy-mode: AT
  use-jdk-proxy: false
  scan-packages: firstPackage,secondPackage
  excludes-for-scanning: firstBeanNameForExclude,secondBeanNameForExclude
  excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
  client:
    rm:
      async-commit-buffer-limit: 10000
      report-retry-count: 5
      table-meta-check-enable: false
      report-success-enable: false
      saga-branch-register-enable: false
      saga-json-parser: fastjson
      saga-retry-persist-mode-update: false
      saga-compensate-persist-mode-update: false
      tcc-action-interceptor-order: -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
      sql-parser-type: druid
      lock:
        retry-interval: 10
        retry-times: 30
        retry-policy-branch-rollback-on-conflict: true
    tm:
      commit-retry-count: 5
      rollback-retry-count: 5
      default-global-transaction-timeout: 60000
      degrade-check: false
      degrade-check-period: 2000
      degrade-check-allow-times: 10
      interceptor-order: -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
    undo:
      data-validation: true
      log-serialization: jackson
      log-table: undo_log
      only-care-update-columns: true
      compress:
        enable: true
        type: zip
        threshold: 64k
    load-balance:
      type: XID
      virtual-nodes: 10
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 192.168.145.128:8091
    enable-degrade: false
    disable-global-transaction: false      
  transport:
    shutdown:
      wait: 3
    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
      worker-thread-size: default
      boss-thread-size: 1
    type: TCP
    server: NIO
    heartbeat: true
    serialization: seata
    compressor: none
    enable-tm-client-batch-send-request: false
    enable-rm-client-batch-send-request: true
    rpc-rm-request-timeout: 15000
    rpc-tm-request-timeout: 30000
  log:
    exception-rate: 100
  tcc:
    fence:
      log-table-name: tcc_fence_log
      clean-period: 1h
  saga:
    enabled: false
    state-machine:
      table-prefix: seata_
      enable-async: false
      async-thread-pool:
        core-pool-size: 1
        max-pool-size: 20
        keep-alive-time: 60
      trans-operation-timeout: 1800000
      service-invoke-timeout: 300000
      auto-register-resources: true
      resources:
        - classpath*:seata/saga/statelang/**/*.json
      default-tenant-id: 000001
      charset: UTF-8          

事务分组配置(service.vgroupMapping.default_tx_group)

default

更多配置请参考官方脚本

三、主要业务代码

1. 仓储服务

1.1 controller

package org.example.storage.controller;

import io.seata.core.context.RootContext;
import org.example.storage.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/storage")
public class StorageController {

	private static final Logger LOGGER = LoggerFactory.getLogger(StorageController.class);

	private static final String SUCCESS = "SUCCESS";

	private static final String FAIL = "FAIL";
	@Autowired
	private StorageService storageService;

	@GetMapping(value = "/deduct/{commodityCode}/{count}", produces = "application/json")
	public String deduct(@PathVariable("commodityCode") String commodityCode, @PathVariable("count") int count) {
		LOGGER.info("Storage Service Begin ... xid: " + RootContext.getXID());
		int result = storageService.deduct(commodityCode, count);
		LOGGER.info("Storage Service End ... ");
		if (result == 1) {
			return SUCCESS;
		}
		return FAIL;
	}

}

1.2 service

package org.example.storage.service;

public interface StorageService {

    /**
     * 扣减库存
     *
     * @param commodityCode 商品编号
     * @param count         扣减数量
     * @return result   执行结果
     */
    int deduct(String commodityCode, int count);
}

package org.example.storage.service.impl;

import io.seata.core.context.RootContext;
import org.example.storage.dao.StorageMapper;
import org.example.storage.model.Storage;
import org.example.storage.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class StorageServiceImpl implements StorageService {

    private static final Logger LOGGER = LoggerFactory.getLogger(StorageService.class);

    @Resource
    private StorageMapper storageMapper;

    @Override
    public int deduct(String commodityCode, int count) {
        LOGGER.info("Stock Service Begin ... xid: " + RootContext.getXID());
        LOGGER.info("Deducting inventory SQL: update stock_tbl set count = count - {} where commodity_code = {}", count,
                commodityCode);
        Storage stock = storageMapper.findByCommodityCode(commodityCode);
        stock.setCount(stock.getCount() - count);
        int result = storageMapper.updateById(stock);
        LOGGER.info("Stock Service End ... ");
        return result;
    }

}

1.3 dao

package org.example.storage.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.example.storage.model.Storage;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface StorageMapper {

    Storage selectById(@Param("id") Integer id);

    Storage findByCommodityCode(@Param("commodityCode") String commodityCode);

    int updateById(Storage record);

    void insert(Storage record);

    void insertBatch(List<Storage> records);

    int updateBatch(@Param("list") List<Long> ids, @Param("commodityCode") String commodityCode);
}

2. 订单服务

1.1 controller

package org.example.order.controller;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Random;

import io.seata.core.context.RootContext;
import org.example.order.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/order")
public class OrderController {

	private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);

	private static final String SUCCESS = "SUCCESS";

	private static final String FAIL = "FAIL";

	@Autowired
	private OrderService orderService;

	@PostMapping(value = "/create", produces = "application/json")
	public String create(@RequestParam("userId") String userId,@RequestParam("commodityCode") String commodityCode,@RequestParam("orderCount") int orderCount) {
		LOGGER.info("Order Service Begin ... xid: " + RootContext.getXID());
		int result = orderService.create(userId, commodityCode, orderCount);
		LOGGER.info("Order Service End ... Created " + result);
		if (result == 2) {
			return SUCCESS;
		}
		return FAIL;
	}

}

1.2 service

package org.example.order.service;


public interface OrderService {

    /**
     * 创建订单
     *
     * @param userId        用户ID
     * @param commodityCode 商品编号
     * @param orderCount    订购数量
     * @return result   执行结果
     */
    int create(String userId, String commodityCode, int orderCount);
}

package org.example.order.service.impl;

import org.example.order.dao.OrderMapper;
import org.example.order.feign.AccountService;
import org.example.order.model.Order;
import org.example.order.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class OrderServiceImpl implements OrderService {

    private static final Logger LOGGER = LoggerFactory.getLogger(OrderService.class);

    private static final String SUCCESS = "SUCCESS";

    @Resource
    private AccountService accountService;
    @Resource
    private OrderMapper orderMapper;

    @Override
    public int create(String userId, String commodityCode, int count) {
        int result=0;
        Order order = new Order();
        order.setUserId(userId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        int orderMoney = calculate(count);
        order.setMoney(orderMoney);
        //保存订单
        result+=orderMapper.insert(order);
        //扣减余额
        if(SUCCESS.equalsIgnoreCase(accountService.debit(userId, orderMoney))){
            result+=1;
        }
        return result;
    }

    private int calculate(int count) {
        return 200 * count;
    }

}

1.3 dao

package org.example.order.dao;


import org.apache.ibatis.annotations.Mapper;
import org.example.order.model.Order;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface OrderMapper {

    int insert(Order record);

}

3. 帐户服务

1.1 controller

package org.example.account.controller;

import io.seata.core.context.RootContext;
import org.example.account.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/account")
public class AccountController {

	private static final Logger LOGGER = LoggerFactory.getLogger(AccountController.class);

	private static final String SUCCESS = "SUCCESS";

	private static final String FAIL = "FAIL";
	@Autowired
	private AccountService accountService;

	@PostMapping(value = "/debit", produces = "application/json")
	public String debit(@RequestParam("userId") String userId,@RequestParam("money") int money) {
		LOGGER.info("Account Service ... xid: " + RootContext.getXID());
		int result = accountService.debit(userId, money);
		LOGGER.info("Account Service End ... ");
		if (result == 1) {
			return SUCCESS;
		}
		return FAIL;
	}

}

1.2 service

package org.example.account.service;

public interface AccountService {

    /**
     * 余额扣款
     *
     * @param userId 用户ID
     * @param money  扣款金额
     * @return result 处理结果
     */
    int debit(String userId, int money);
}

package org.example.account.service.impl;

import org.example.account.dao.AccountMapper;
import org.example.account.model.Account;
import org.example.account.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountService.class);

    @Resource
    private AccountMapper accountMapper;

    @Override
    public int debit(String userId, int money) {
        LOGGER.info("Deducting balance SQL: update account_tbl set money = money - {} where user_id = {}", money,
                userId);
        Account account = accountMapper.selectByUserId(userId);
        account.setMoney(money);
        return accountMapper.updateById(account);
    }
}

1.3 dao

package org.example.account.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.example.account.model.Account;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface AccountMapper {

    Account selectByUserId(@Param("userId") String userId);

    int updateById(Account record);

}

4. 商品服务

商品服务包含了rest和feign两种请求方式

1.1 controller

package org.example.business.controller;

import io.seata.spring.annotation.GlobalTransactional;
import org.example.business.feign.OrderService;
import org.example.business.feign.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class BusinessController {

	private static final Logger LOGGER = LoggerFactory.getLogger(BusinessController.class);

	private static final String SUCCESS = "SUCCESS";

	private static final String FAIL = "FAIL";

	private static final String USER_ID = "U100001";

	private static final String COMMODITY_CODE = "C00321";

	private static final int ORDER_COUNT = 2;
	@Autowired
	private RestTemplate restTemplate;
	@Autowired
	private OrderService orderService;
	@Autowired
	private StorageService storageService;
	@GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-demo-tx")
	@GetMapping(value = "/seata/rest", produces = "application/json")
	public String rest() {
		String result = restTemplate.getForObject("http://storage-service/storage/deduct/" + COMMODITY_CODE + "/" + ORDER_COUNT,
				String.class);
		if (!SUCCESS.equals(result)) {
			throw new RuntimeException();
		}
		String url = "http://order-service/order/create";
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
		map.add("userId", USER_ID);
		map.add("commodityCode", COMMODITY_CODE);
		map.add("orderCount", ORDER_COUNT + "");
		HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
		ResponseEntity<String> response;
		try {
			response = restTemplate.postForEntity(url, request, String.class);
		}catch (Exception exx) {
			throw new RuntimeException("mock error");
		}
		result = response.getBody();
		if (!SUCCESS.equals(result)) {
			throw new RuntimeException();
		}
		return SUCCESS;
	}

	@GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-demo-tx")
	@GetMapping(value = "/seata/feign", produces = "application/json")
	public String feign() {
		String result = storageService.deduct(COMMODITY_CODE, ORDER_COUNT);
		if (!SUCCESS.equals(result)) {
			throw new RuntimeException();
		}
		result = orderService.create(USER_ID, COMMODITY_CODE, ORDER_COUNT);
		if (!SUCCESS.equals(result)) {
			throw new RuntimeException();
		}
		return SUCCESS;

	}

}

1.2 service

package org.example.business.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "storage-service")
public interface StorageService {

    /**
     * 扣减库存
     *
     * @param commodityCode 商品编号
     * @param count         扣减数量
     * @return result   执行结果
     */
    @GetMapping(value = "/storage/deduct/{commodityCode}/{count}")
    String deduct(@PathVariable("commodityCode") String commodityCode, @PathVariable("count") int count);
}

package org.example.business.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "order-service")
public interface OrderService {

    /**
     * 创建订单
     *
     * @param userId        用户ID
     * @param commodityCode 商品编号
     * @param orderCount    订购数量
     * @return result   执行结果
     */
    @PostMapping(value = "/order/create")
    String create(@RequestParam("userId") String userId,@RequestParam("commodityCode") String commodityCode,@RequestParam("orderCount") int orderCount);
}

1.3 rest

package org.example.business.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * Create by zjg on 2024/9/15
 */
@Configuration
public class BusinessConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

四、业务单元测试

1. 准备

首先启动我们的服务,并保证服务正常注册到Nacos

在这里插入图片描述
在这里插入图片描述

2. 成功测试

2.1 结果

localhost:18081/seata/feign
在这里插入图片描述
在这里插入图片描述

2.2 日志

我们每个服务都能从上下文中获取到xid

2024-09-15T21:37:32.765+08:00  INFO 10488 --- [business-service] [io-18081-exec-9] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.145.128:8091:2162292867793093336]
2024-09-15T21:37:32.977+08:00  INFO 10488 --- [business-service] [io-18081-exec-9] i.seata.tm.api.DefaultGlobalTransaction  : transaction 192.168.145.128:8091:2162292867793093336 will be commit
2024-09-15T21:37:32.984+08:00  INFO 10488 --- [business-service] [io-18081-exec-9] i.seata.tm.api.DefaultGlobalTransaction  : transaction end, xid = 192.168.145.128:8091:2162292867793093336
2024-09-15T21:37:32.984+08:00  INFO 10488 --- [business-service] [io-18081-exec-9] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.145.128:8091:2162292867793093336] commit status: Committed

2024-09-15T21:37:32.768+08:00  INFO 4560 --- [storage-service] [io-18082-exec-5] o.e.s.controller.StorageController       : Storage Service Begin ... xid: 192.168.145.128:8091:2162292867793093336
2024-09-15T21:37:32.769+08:00  INFO 4560 --- [storage-service] [io-18082-exec-5] o.e.storage.service.StorageService       : Stock Service Begin ... xid: 192.168.145.128:8091:2162292867793093336
2024-09-15T21:37:32.769+08:00  INFO 4560 --- [storage-service] [io-18082-exec-5] o.e.storage.service.StorageService       : Deducting inventory SQL: update stock_tbl set count = count - 2 where commodity_code = C00321
2024-09-15T21:37:32.777+08:00  INFO 4560 --- [storage-service] [io-18082-exec-5] io.seata.rm.AbstractResourceManager      : branch register success, xid:192.168.145.128:8091:2162292867793093336, branchId:2162292867793093338, lockKeys:storage_tbl:1
2024-09-15T21:37:32.781+08:00  INFO 4560 --- [storage-service] [io-18082-exec-5] o.e.storage.service.StorageService       : Stock Service End ... 
2024-09-15T21:37:32.781+08:00  INFO 4560 --- [storage-service] [io-18082-exec-5] o.e.s.controller.StorageController       : Storage Service End ... 
2024-09-15T21:37:33.600+08:00  INFO 4560 --- [storage-service] [h_RMROLE_1_7_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:BranchCommitRequest{xid='192.168.145.128:8091:2162292867793093336', branchId=2162292867793093338, branchType=AT, resourceId='jdbc:mysql://192.168.145.128:3306/seata', applicationData='null'}
2024-09-15T21:37:33.601+08:00  INFO 4560 --- [storage-service] [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.145.128:8091:2162292867793093336 2162292867793093338 jdbc:mysql://192.168.145.128:3306/seata null
2024-09-15T21:37:33.601+08:00  INFO 4560 --- [storage-service] [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed

2024-09-15T21:37:32.785+08:00  INFO 11676 --- [order-service] [io-18083-exec-9] o.e.order.controller.OrderController     : Order Service Begin ... xid: 192.168.145.128:8091:2162292867793093336
2024-09-15T21:37:32.796+08:00  INFO 11676 --- [order-service] [io-18083-exec-9] io.seata.rm.AbstractResourceManager      : branch register success, xid:192.168.145.128:8091:2162292867793093336, branchId:2162292867793093340, lockKeys:order_tbl:2
2024-09-15T21:37:32.975+08:00  INFO 11676 --- [order-service] [io-18083-exec-9] o.e.order.controller.OrderController     : Order Service End ... Created 2
2024-09-15T21:37:33.695+08:00  INFO 11676 --- [order-service] [h_RMROLE_1_7_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:BranchCommitRequest{xid='192.168.145.128:8091:2162292867793093336', branchId=2162292867793093340, branchType=AT, resourceId='jdbc:mysql://192.168.145.128:3306/seata', applicationData='null'}
2024-09-15T21:37:33.695+08:00  INFO 11676 --- [order-service] [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.145.128:8091:2162292867793093336 2162292867793093340 jdbc:mysql://192.168.145.128:3306/seata null
2024-09-15T21:37:33.695+08:00  INFO 11676 --- [order-service] [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed

2024-09-15T21:37:32.803+08:00  INFO 12564 --- [account-service] [io-18084-exec-5] o.e.a.controller.AccountController       : Account Service ... xid: 192.168.145.128:8091:2162292867793093336
2024-09-15T21:37:32.803+08:00  INFO 12564 --- [account-service] [io-18084-exec-5] o.e.account.service.AccountService       : Deducting balance SQL: update account_tbl set money = money - 400 where user_id = U100001
2024-09-15T21:37:32.970+08:00  INFO 12564 --- [account-service] [io-18084-exec-5] io.seata.rm.AbstractResourceManager      : branch register success, xid:192.168.145.128:8091:2162292867793093336, branchId:2162292867793093342, lockKeys:account_tbl:1
2024-09-15T21:37:32.973+08:00  INFO 12564 --- [account-service] [io-18084-exec-5] o.e.a.controller.AccountController       : Account Service End ... 
2024-09-15T21:37:33.697+08:00  INFO 12564 --- [account-service] [h_RMROLE_1_7_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:BranchCommitRequest{xid='192.168.145.128:8091:2162292867793093336', branchId=2162292867793093342, branchType=AT, resourceId='jdbc:mysql://192.168.145.128:3306/seata', applicationData='null'}
2024-09-15T21:37:33.698+08:00  INFO 12564 --- [account-service] [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.145.128:8091:2162292867793093336 2162292867793093342 jdbc:mysql://192.168.145.128:3306/seata null
2024-09-15T21:37:33.698+08:00  INFO 12564 --- [account-service] [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed

3. 失败测试

失败的案例怎么做呢?简单,帐户服务作为作为购物生命周期的最后一环,我们在账户服务产生异常即可

3.1 数据重置

truncate table account_tbl;
truncate table order_tbl;
truncate table storage_tbl;
insert into account_tbl(user_id,money) values('U100001','10000');
insert into storage_tbl(commodity_code,count) values('C00321','100');

3.2 结果

localhost:18081/seata/rest
在这里插入图片描述
在这里插入图片描述

可以看到我们的数据是没有发生变化的

3.3 日志

此处日志过多,仅剪辑关键部分

2024-09-15T21:58:20.193+08:00  INFO 10488 --- [business-service] [io-18081-exec-6] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.145.128:8091:2162292867793093649]
2024-09-15T21:58:20.331+08:00  INFO 10488 --- [business-service] [io-18081-exec-6] i.seata.tm.api.DefaultGlobalTransaction  : transaction 192.168.145.128:8091:2162292867793093649 will be rollback
2024-09-15T21:58:20.381+08:00  INFO 10488 --- [business-service] [io-18081-exec-6] i.seata.tm.api.DefaultGlobalTransaction  : transaction end, xid = 192.168.145.128:8091:2162292867793093649
2024-09-15T21:58:20.381+08:00  INFO 10488 --- [business-service] [io-18081-exec-6] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.145.128:8091:2162292867793093649] rollback status: Rollbacked
2024-09-15T21:58:20.381+08:00 ERROR 10488 --- [business-service] [io-18081-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: try to proceed invocation error] with root cause
java.lang.RuntimeException: mock error
	at org.example.business.controller.BusinessController.rest(BusinessController.java:59) ~[classes/:na]

2024-09-15T21:58:20.195+08:00  INFO 4560 --- [storage-service] [io-18082-exec-8] o.e.s.controller.StorageController       : Storage Service Begin ... xid: 192.168.145.128:8091:2162292867793093649
2024-09-15T21:58:20.195+08:00  INFO 4560 --- [storage-service] [io-18082-exec-8] o.e.storage.service.StorageService       : Stock Service Begin ... xid: 192.168.145.128:8091:2162292867793093649
2024-09-15T21:58:20.195+08:00  INFO 4560 --- [storage-service] [io-18082-exec-8] o.e.storage.service.StorageService       : Deducting inventory SQL: update stock_tbl set count = count - 2 where commodity_code = C00321
2024-09-15T21:58:20.204+08:00  INFO 4560 --- [storage-service] [io-18082-exec-8] io.seata.rm.AbstractResourceManager      : branch register success, xid:192.168.145.128:8091:2162292867793093649, branchId:2162292867793093651, lockKeys:storage_tbl:1
2024-09-15T21:58:20.207+08:00  INFO 4560 --- [storage-service] [io-18082-exec-8] o.e.storage.service.StorageService       : Stock Service End ... 
2024-09-15T21:58:20.207+08:00  INFO 4560 --- [storage-service] [io-18082-exec-8] o.e.s.controller.StorageController       : Storage Service End ... 
2024-09-15T21:58:20.337+08:00  INFO 4560 --- [storage-service] [_RMROLE_1_14_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:BranchRollbackRequest{xid='192.168.145.128:8091:2162292867793093649', branchId=2162292867793093651, branchType=AT, resourceId='jdbc:mysql://192.168.145.128:3306/seata', applicationData='null'}
2024-09-15T21:58:20.337+08:00  INFO 4560 --- [storage-service] [_RMROLE_1_14_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.145.128:8091:2162292867793093649 2162292867793093651 jdbc:mysql://192.168.145.128:3306/seata
2024-09-15T21:58:20.344+08:00  INFO 4560 --- [storage-service] [_RMROLE_1_14_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.145.128:8091:2162292867793093649 branch 2162292867793093651, undo_log deleted with GlobalFinished
2024-09-15T21:58:20.344+08:00  INFO 4560 --- [storage-service] [_RMROLE_1_14_16] i.seata.rm.datasource.DataSourceManager  : branch rollback success, xid:192.168.145.128:8091:2162292867793093649, branchId:2162292867793093651
2024-09-15T21:58:20.344+08:00  INFO 4560 --- [storage-service] [_RMROLE_1_14_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked

2024-09-15T21:58:20.210+08:00  INFO 11676 --- [order-service] [io-18083-exec-7] o.e.order.controller.OrderController     : Order Service Begin ... xid: 192.168.145.128:8091:2162292867793093649
2024-09-15T21:58:20.221+08:00  INFO 11676 --- [order-service] [io-18083-exec-7] io.seata.rm.AbstractResourceManager      : branch register success, xid:192.168.145.128:8091:2162292867793093649, branchId:2162292867793093653, lockKeys:order_tbl:2
2024-09-15T21:58:20.330+08:00 ERROR 11676 --- [order-service] [io-18083-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: feign.FeignException$InternalServerError: [500] during [POST] to [http://account-service/account/debit?userId=U100001&money=400] [AccountService#debit(String,int)]: [{"timestamp":"2024-09-15T13:58:20.329+00:00","status":500,"error":"Internal Server Error","path":"/account/debit"}]] with root cause
feign.FeignException$InternalServerError: [500] during [POST] to [http://account-service/account/debit?userId=U100001&money=400] [AccountService#debit(String,int)]: [{"timestamp":"2024-09-15T13:58:20.329+00:00","status":500,"error":"Internal Server Error","path":"/account/debit"}]
2024-09-15T21:58:20.348+08:00  INFO 11676 --- [order-service] [_RMROLE_1_14_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:BranchRollbackRequest{xid='192.168.145.128:8091:2162292867793093649', branchId=2162292867793093653, branchType=AT, resourceId='jdbc:mysql://192.168.145.128:3306/seata', applicationData='null'}
2024-09-15T21:58:20.349+08:00  INFO 11676 --- [order-service] [_RMROLE_1_14_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.145.128:8091:2162292867793093649 2162292867793093653 jdbc:mysql://192.168.145.128:3306/seata
2024-09-15T21:58:20.376+08:00  INFO 11676 --- [order-service] [_RMROLE_1_14_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.145.128:8091:2162292867793093649 branch 2162292867793093653, undo_log deleted with GlobalFinished
2024-09-15T21:58:20.377+08:00  INFO 11676 --- [order-service] [_RMROLE_1_14_16] i.seata.rm.datasource.DataSourceManager  : branch rollback success, xid:192.168.145.128:8091:2162292867793093649, branchId:2162292867793093653
2024-09-15T21:58:20.377+08:00  INFO 11676 --- [order-service] [_RMROLE_1_14_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked

2024-09-15T21:58:20.328+08:00  INFO 12564 --- [account-service] [io-18084-exec-3] o.e.a.controller.AccountController       : Account Service ... xid: 192.168.145.128:8091:2162292867793093649
2024-09-15T21:58:20.328+08:00 ERROR 12564 --- [account-service] [io-18084-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: seata fail test] with root cause
java.lang.RuntimeException: seata fail test
	at org.example.account.controller.AccountController.debit(AccountController.java:29) ~[classes/:na]

从日志中可以看出,仓储服务和订单服务已经执行,发生异常后进行了回退的动作,并且在这个过程中之前创建的undo_log表,也就是AT需要借助数据库完成分布式事务的功能

3.4 控制台

从控制台也能看到我们的事务执行信息,但是这个过程很短,事务执行完成几秒后可能就删除了

在这里插入图片描述


总结

回到顶部

分布式事务到这里就要告一段落了,希望大家都能学到东西吧,这部分的源码已经上传附件,over!

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

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

相关文章

对操作系统(OS)管理和进程的理解

文章目录 从冯诺依曼体系入手来了解计算机硬件部分操作系统操作系统的概念设计操作系统&#xff08;OS&#xff09;的目的对下&#xff08;硬件&#xff09;OS的管理对上如何理解系统调用 进程 在计算机系统中&#xff0c;硬件、操作系统和进程是三个至关重要的概念。它们相互协…

【数据库】MySQL内置函数

本篇分享一些在MySQL中常见的一些内置函数&#xff0c;如日期函数&#xff0c;字符串函数和数学函数&#xff0c;以方便于操作数据库中的数据。 1.日期函数 我们先整体观察一下这些函数再讲解案例 日期函数使用起来都非常就简单 获得年月日&#xff1a; select current_dat…

LocalMamba: Visual State Space Model with Windowed Selective Scan 论文总结

题目&#xff1a;LocalMamba: Visual State Space Model&#xff08;视觉状态空间模型&#xff09; with Windowed Selective Scan&#xff08;窗口化的选择扫描&#xff09; 论文&#xff1a;[2403.09338] LocalMamba: Visual State Space Model with Windowed Selective Scan…

opencv彩色图像转灰度图原理

opencv彩色图像转灰度图原理 在OpenCV中&#xff0c;将彩色图像转换为灰度图像的基本原理是使用颜色空间转换的方法。具体来说&#xff0c;OpenCV提供了cvtColor函数&#xff0c;它可以将图像从一个颜色空间转换到另一个。 对于从BGR颜色空间&#xff08;OpenCV中的默认彩色图…

少儿编程Scratch中秋节动画贺卡免费下载,让孩子轻松学编程

Scratch节日动画 – 中秋节动画贺卡免费下载-小虎鲸Scratch资源站 中秋佳节将至&#xff0c;如何让孩子在节日中既能感受到传统文化的魅力&#xff0c;又能学到编程知识&#xff1f;小虎鲸Scratch资源站特别推出了中秋节动画贺卡&#xff0c;不仅能让孩子通过简单的编程技巧制作…

【AI大模型】OpenAI API实现翻译助手场景

一、OpenAI简介 OpenAI,美国人工智能研究公司&#xff0c;是一家开放人工智能研究和部署公司&#xff0c;其使命是确保通用人工智能造福全人类 。创立于2015年12月&#xff0c;总部位于美国旧金山。现由营利性公司OpenAI LP及非营利性母公司OpenAI Inc组成。 随着2018年埃隆马…

24年云南省下半年事业单位少有人知的10个真相

云南下半年事业单位&#xff0c;已经确定了9月19号报名&#xff0c;11月2日笔试&#xff0c;关于下半年事业单位联考的一些考情&#xff0c;一次看懂: . 1⃣️专科生的岗位很多 根据过往三年的情况来看&#xff0c;云南下半年的事业单位考试&#xff0c;其实专科生有不少的岗位…

java项目之在线考试与学习交流网页平台源码(springboot)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的在线考试与学习交流网页平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 基于JAVA语言…

基于Springboot的校园防疫管理系统的设计与实现

文未可获取一份本项目的java源码和数据库参考。 1&#xff0e;本毕业设计&#xff08;论文&#xff09;课题应达到的目的&#xff1a; 1.1选题意义&#xff1a; 高校作为一种人群高度密集、人口来源地组成复杂的公共场所&#xff0c;一旦发生因传染病疫情导致的公共卫生安全事…

wpf触发与模板的使用示例:批量生产工具

批量生产工具 <Window x:Class"WpfM20UpdateFW.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schemas.microsoft.com/expressio…

java项目之基于工程教育认证的计算机课程管理平台(源码+论文)

项目简介 基于工程教育认证的计算机课程管理平台的主要管理员可以管理教师&#xff0c;可以对教师信息修改删除以及查询操作&#xff1b;可以对通知公告信息进行添加&#xff0c;修改&#xff0c;删除以及查询操作&#xff1b;可以对学生信息进行添加&#xff0c;修改&#xf…

anaconda安装manim

anaconda安装manim &#xff08;安装时间2024年9月15日&#xff0c;后续版本变化再做更新&#xff09; 创建环境test 等一下我们创建一个manim项目作为试水 切换到test环境 {:height 430, :width 780} 输入以下代码 # using conda or mamba conda create -n my-manim-envi…

qt绘制时钟

代码 #include "widget.h" #include "ui_widget.h"#include <QWidget> #include <QPaintEvent> //绘图事件 #include <QDebug> //测试 #include <QPainter> //画家 #include <QPen> //笔 #include <QBrush> //画刷 …

java写s7和plc通讯

pom.xml <dependency><groupId>com.github.s7connector</groupId><artifactId>s7connector</artifactId><version>2.1</version></dependency>maven下载不了的&#xff0c;下载包&#xff0c;评论或者私自内免费给 DB212 类&a…

AI老照片修复神器,Anole下载介绍

最近AI老照片修复上色&#xff0c;再一次火出圈&#xff0c;一些社交平台关于此话题内容流量满满&#xff0c;尤其是在小红书和抖音火的不得了&#xff0c;本期文章就来给大家分享下AI修复老照片的方式方法 本文主要介绍使用Anole修复老照片的方法&#xff0c;只需输入一张黑白…

响应式CSS 媒体查询——WEB开发系列39

CSS媒体查询&#xff08;Media Queries&#xff09;是响应式设计中的核心技术之一&#xff0c;帮助我们在不同设备上展示不同的样式。通过媒体查询&#xff0c;开发者可以检测用户设备的特性&#xff0c;如屏幕宽度、高度、分辨率、方向等&#xff0c;针对性地调整网页布局。 一…

「数组」十大排序:精讲与分析(C++)

概述 截止目前&#xff0c;我们已经讲解并分析了十种最常见的排序算法&#xff0c;下附对应文章链接和全体Code。 链接 「数组」冒泡排序|选择排序|插入排序 / 及优化方案&#xff08;C&#xff09; 「数组」归并排序 / if语句优化|小区间插入优化&#xff08;C&#xff09…

使用Jlink给AT32下载程序

点击下载之后选择target device&#xff0c;这里我使用的是AT32F403ACGT7&#xff0c;M4内核&#xff0c;就选择Cortex-M4. 如果已经选过了&#xff0c;但不知道选没选对&#xff0c;就把ini文件删除即可再次进行选择。 我这里使用SW接线方式&#xff0c;选好后立马识别出来了…

Java【集合】

一、集合的概述 集合建立在数组基础上&#xff0c;主要位于java.util包中&#xff0c;用来存储Java类对象&#xff0c;并且可以实现各种数据结构。 集合大小可以改变&#xff0c;可以存放不同数据类型数据。集合不能存放基本类型数据&#xff0c;只能存放引用数据类型数据。集…

浅谈树型结构——树

文章目录 一、什么是树&#xff1f;二、树的特点三、树的概念四、树的表示形式五、树的应用 一、什么是树&#xff1f; 树是一种 非线性 的数据结构&#xff0c;是树型结构。是一个由n个有限结点组成的一个具有层次关系的集合&#xff0c;这种集合因为看起来像一颗倒挂的树&am…