文章目录
- 前言
- 一、架构图
- 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!