环境介绍
注册中心:eureka
服务间调用:feign
持久层:mybatis
数据库:mysql 5.7.20
Springboot : 2.2.2.RELEASE
Springcloud : Hoxton.SR4
jdk : 1.8
seata : 1.4
Seata有3个基本组成部分:
事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚。
事务管理器(TM):定义全局事务的范围:开始全局事务,提交或回滚全局事务。
资源管理器(RM):管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。
Seata管理的分布式事务的典型生命周期:
TM要求TC开始一项新的全球交易。TC生成代表全局交易的XID。
XID通过微服务的调用链传播。
RM将本地事务注册为XID到TC的相应全局事务的分支。
TM要求TC提交或回退相应的XID全局事务。
TC驱动XID对应的全局事务下的所有分支事务,以完成分支的提交或回滚。
一、搭建 TC(事务协调器)
下载seata-server
我是MacOS系统,下载了seata-server.zip
解压
解压后文件目录:
修改管理TC的配置文件file.conf和registry.conf
解压后进入conf目录:
修改file.conf和registry.conf两个配置文件
建表
如果mode配置为db
的话需要手动新建几张表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
启动
首先启动eureka注册中心。
配置完成后进入bin
目录:
注意:使用command+c
结束任务,如果使用command+z
是后台运行,再次启动后报端口已经被使用的错误。
seata-server
已经成功注册到eureka
:
如果使用Nacos作为注册中心:
二、搭建项目
建业务表
并初始化数据
账号表 用户id为1 .总额度为1000,已用额度0,剩余可用额度1000。
订单表为空。
库存表库存为100
-- 账号表
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
-- 订单表
CREATE TABLE `order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;
-- 库存表
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`total` int(11) DEFAULT NULL COMMENT '总库存',
`used` int(11) DEFAULT NULL COMMENT '已用库存',
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
2.0 父工程
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lsh</groupId>
<artifactId>seata-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>seata-eureka</module>
<module>seata-account</module>
<module>seata-order</module>
<module>seata-storage</module>
</modules>
<properties>
<java.version>1.8</java.version>
<mysql-connector-java.version>5.1.37</mysql-connector-java.version>
<mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version>
<druid-spring-boot-starter.version>1.1.10</druid-spring-boot-starter.version>
<lombok.version>1.18.8</lombok.version>
<seata.version>1.4.0</seata.version>
</properties>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--SpringBootweb-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--pringBoot测试类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
<!--阿里巴巴json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.1 eureka
pom文件
<dependencies>
<!-- eureka服务端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
启动类:
package com.lsh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* @author :LiuShihao
* @date :Created in 2021/3/22 9:54 上午
* @desc :
*/
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class);
}
}
yml配置文件
server:
port: 8081
spring:
application:
name: seata-eureka
eureka:
instance:
hostname: localhost
client:
#registerWithEureka表示是否注册自身到eureka服务器
register-with-eureka: false
#fetchRegistry表示是否从eureka服务器获取注册信息。
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
2.2 order
pom依赖
<dependencies>
<!-- eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--Druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--openfeifn-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-web</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<!--Ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>
</dependencies>
启动类
package com.lsh;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author :LiuShihao
* @date :Created in 2021/3/22 11:52 上午
* @desc :
*/
//发现服务
@EnableDiscoveryClient
@EnableFeignClients
@EnableEurekaClient
@MapperScan("com.lsh.dao.*")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}
}
Controller
package com.lsh.controller;
import com.lsh.entity.Order;
import com.lsh.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* @author IT云清
*/
@RestController
@RequestMapping(value = "order")
public class OrderController {
@Autowired
private OrderService orderServiceImpl;
/**
* 创建订单
* @param order
* @return
*/
@GetMapping("create")
public String create(Order order){
orderServiceImpl.create(order);
return "Create order success";
}
/**
* 修改订单状态
* @param userId
* @param money
* @param status
* @return
*/
@RequestMapping("update")
String update(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money, @RequestParam("status") Integer status){
orderServiceImpl.update(userId,money,status);
return "订单状态修改成功";
}
}
ServiceImpl
package com.lsh.service.impl;
import com.lsh.dao.OrderDao;
import com.lsh.entity.Order;
import com.lsh.feign.AccountApi;
import com.lsh.feign.StorageApi;
import com.lsh.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* @author IT云清
*/
@Service("orderServiceImpl")
public class OrderServiceImpl implements OrderService {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private OrderDao orderDao;
@Autowired
private StorageApi storageApi;
@Autowired
private AccountApi accountApi;
/**
* 创建订单
* @param order
* @return
* 测试结果:
* 1.添加本地事务:仅仅扣减库存
* 2.不添加本地事务:创建订单,扣减库存
*/
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
LOGGER.info("------->交易开始");
//本地方法
orderDao.create(order);
//远程方法 扣减库存
storageApi.decrease(order.getProductId(),order.getCount());
//远程方法 扣减账户余额
LOGGER.info("------->扣减账户z开始order中");
accountApi.decrease(order.getUserId(),order.getMoney());
LOGGER.info("------->扣减账户结束order中");
LOGGER.info("------->交易结束");
}
/**
* 修改订单状态
*/
@Override
public void update(Long userId,BigDecimal money,Integer status) {
LOGGER.info("修改订单状态,入参为:userId={},money={},status={}",userId,money,status);
orderDao.update(userId,money,status);
}
}
entity
package com.lsh.entity;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author :LiuShihao
* @date :Created in 2021/3/22 4:06 下午
* @desc :
*/
@Data
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
/**
* BigDecimal 高精度
*/
private BigDecimal money;
/**订单状态:0:创建中;1:已完结*/
private Integer status;
}
dao
package com.lsh.dao;
import com.lsh.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
/**
* @author IT云清
*/
@Mapper
public interface OrderDao {
/**
* 创建订单
* @param order
* @return
*/
void create(Order order);
/**
* 修改订单金额
* @param userId
* @param money
*/
void update(@Param("userId") Long userId, @Param("money") BigDecimal money, @Param("status") Integer status);
}
数据源代理类
对于事务的处理,最重要的是要拿到数据源,由于经过数据源咱们能够控制事务何时回滚或提交,因此数据源咱们须要让seata来代理
持久层使用的是MyBatis框架
package com.lsh.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 数据源代理
* @author wangzhongxiang
*/
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
持久层使用JPA
package io.seata.sample.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* 数据源配置
*
* @author HelloWoodes
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
*
* @param druidDataSource The DruidDataSource
* @return The default datasource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
feign
package com.lsh.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
/**
* @author IT云清
*/
@Component
@FeignClient(value = "account-server")
public interface AccountApi {
/**
* 扣减账户余额
* @param userId 用户id
* @param money 金额
* @return
*/
@RequestMapping("/account/decrease")
String decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
package com.lsh.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author IT云清
*/
@Component
@FeignClient(value = "storage-server")
public interface StorageApi {
/**
* 扣减库存
* @param productId
* @param count
* @return
*/
@GetMapping(value = "/storage/decrease")
String decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lsh.dao.OrderDao" >
<resultMap id="BaseResultMap" type="com.lsh.entity.Order" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="count" property="count" jdbcType="INTEGER" />
<result column="money" property="money" jdbcType="DECIMAL" />
<result column="status" property="status" jdbcType="INTEGER" />
</resultMap>
<insert id="create">
INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
VALUES(NULL, #{userId}, #{productId}, #{count}, #{money},0);
</insert>
<update id="update">
UPDATE `order` SET money = money - #{money},status = 1
where user_id = #{userId} and status = #{status};
</update>
</mapper>
2.3 account
具体代码不在展示。
2.4 storage
具体代码不在展示。
2.5 各项目通用到的类
数据源代理
package com.lsh.config;
/**
* @author :LiuShihao
* @date :Created in 2021/3/22 4:49 下午
* @desc :
*/
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 数据源代理
* @author wangzhongxiang
*/
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
yml配置文件
各个项目的yml配置文件都差不多,就不一一放出来了。
注意修改服务端口、项目名、注册到eureka的服务名、
server:
port: 8084
spring:
application:
name: storage-server
cloud:
alibaba:
seata:
#这个fsp_tx_group自定义命名很重要,server,client都要保持一致
tx-service-group: fsp_tx_group
datasource:
# driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata
username: root
password: 123456
mybatis:
mapperLocations: classpath:mapper/*.xml
typeAliasesPackage: com.lsh.entity.*
eureka:
instance:
#主机名称:服务名称修改
instance-id: storage-server
# 访问路径可以显示IP地址
ip-address: true
client:
service-url:
defaultZone: http://localhost:8081/eureka/
☆☆☆重要 file.conf配置
注意:
此处的file.conf
和registry.conf
文件不是第一步的那两个文件,这是管理RM和TM的文件,第一步的两个文件是管理TC配置的文件。
事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚。
事务管理器TM:定义全局事务的范围:开始全局事务,提交或回滚全局事务。
资源管理器(RM):管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。
项目源码已上传码云仓库。
三、测试
使用@GlobalTransactional
开启全局事务注解。
正常情况
浏览器访问http://localhost:8082/order/create?userId=1&productId=1&count=10&money=100
创建订单
正常情况的业务流程是:
1.(order服务)订单表中插入一条记录。
2.(order服务)调用(storage服务)扣减库存。
3.(order服务)调用(account服务)账户扣减金额
4.(account服务)账户扣减金额成功后,调用(order服务)修改订单状态。
异常情况:
在上面第四步的时候,account服务扣减账户金额失败(抛出一个异常),导致事务全局回滚
数据库中的业务表没有变化。
undo_log表虽然为空,但其实是事务执行成功了,记录已经删除了。
四、源码
https://gitee.com/L1692312138/seata-tx-eureka-fegin
Nacos
以上是使用file
作为配置文件,
如果将配置设置为nacos
的话可以参考