Seata的基本使用
Seata基本介绍
Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
Seata术语
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
简单来说
TM开启分布式事务(TM向TC注册全局事务记录);
按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态)
TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务);
TC汇总事务信息,决定分布式事务是提交还是回滚;
TC通知所有RM提交/回滚资源,事务二阶段结束。
Seata Server 安装
Srata下载地址:https://github.com/seata/seata/releases
或者:http://seata.io/zh-cn/blog/download.html
下载安装完成打开file.conf文件修改配置




找到script\config-center下的config.txt修改配置


找到script\config-center\nacos,点击nacos-config.sh提交到nacos中


复制sql到seata数据库中
drop table if exists `global_table`;
create table `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`)
);
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);

启动seata-server.bat即可

查看naocs

Spring Boot整合Seata实现下订单->扣库存->减余额->改状态
数据准备
#创建三个数据库
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
#创表
USE seata_order;
CREATE TABLE t_order(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
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 '金额',
status INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO seata_order.t_order
(user_id, product_id, count, money, status)
VALUES(1, 1, 10, 100, 1),
(1, 1, 10, 100, 1),
(1, 1, 10, 100, 1),
(1, 1, 10, 100, 1),
(1, 1, 10, 100, 0);
USE seata_account;
CREATE TABLE t_account(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
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 '剩余可用额度'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
use seata_storage;
CREATE TABLE `t_storage` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`product_id` bigint(0) NULL DEFAULT NULL COMMENT '产品id',
`total` int(0) NULL DEFAULT NULL COMMENT '总库存',
`used` int(0) NULL DEFAULT NULL COMMENT '已用库存',
`residue` int(0) NULL DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
INSERT INTO `seata_storage`.`t_storage`(`id`, `product_id`, `total`, `used`, `residue`) VALUES (1, 1, 100, 0, 100);
#在每个数据库中加入日志表,日志数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
<!--seata依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--排除默认的seata版本-->
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--和自己下载的seata版本一致-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
搭建2001,2002,2004公共依赖



依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--seata依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--排除默认的seata版本-->
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--和自己下载的seata版本一致-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
config
package com.seatastorageservice2002.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.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 使用Seata对数据源进行代理
*/
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis-plus.mapperLocations}") #扫描配置applicaton.yml的mybatis-plus配置
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.seatastorageservice2002.mapper")//其他的改查成扫描对应的接口
public class SeataStorageService2002Application {
public static void main(String[] args) {
SpringApplication.run(SeataStorageService2002Application.class, args);
}
}、
每个domain加上
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
搭建2001模块
controller
@RestController
public class OrderController
{
@Resource
private TOrderService orderService;
@GetMapping("/order/create")
public CommonResult create(TOrder order)
{
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
domaim
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_order")
public class TOrder implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户id
*/
@TableField(value = "user_id")
private Long userId;
/**
* 产品id
*/
@TableField(value = "product_id")
private Long productId;
/**
* 数量
*/
@TableField(value = "count")
private Integer count;
/**
* 金额
*/
@TableField(value = "money")
private Long money;
/**
* 订单状态:0创建中,1已完结
*/
@TableField(value = "status")
private Integer status;
}
mapper
public interface TOrderMapper extends BaseMapper<TOrder> {
//1 新建订单
void create(TOrder order);
//2 修改订单状态,从零改为1
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
<?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.seataorderservice2001.mapper.TOrderMapper">
<update id="update">
update seata_order.t_order set status = 1
where user_id=#{userId} and status = #{status};
</update>
<insert id="create">
insert into seata_order.t_order (user_id,product_id,count,money,status)
values (#{userId},#{productId},#{count},#{money},0);
</insert>
</mapper>
service
@FeignClient(value = "seata-storage-service")//seata-storage-service 对应2002模块的服务名的名称
public interface TStorageService{
@GetMapping(value = "/storage/decrease")//调用的2002模块的conroller
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")//seata-account-service 对应2003模块的服务名的名称
public interface TAccountService {
@PostMapping(value = "/account/decrease")//调用的2003模块的conroller
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money")Long money);
}
public interface TOrderService extends IService<TOrder>{
public void create(TOrder tOrder);
}
impl
@Service
@Slf4j
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements TOrderService{
@Autowired
private TOrderMapper tOrderMapper;
@Autowired
private TAccountService tAccountService;
@Autowired
private TStorageService tStorageService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
*下订单->扣库存->减余额->改状态
*/
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) //一但有一个地方报错所以都回滚,fsp-create-order自己去的名字,不冲突即可
public void create(TOrder order)
{
log.info("----->开始新建订单");
//1 新建订单
tOrderMapper.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
tStorageService.decrease(order.getProductId(),order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
tAccountService.decrease(order.getUserId(),order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
tOrderMapper.update(order.getUserId(),0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
application.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
# seata 配置
seata:
# 使用哪个事务组
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis-plus:
mapperLocations: classpath:mapper/*.xml
搭建2002模块
controller
@RestController
public class StorageController {
@Autowired
private TStorageService storageService;
/**
* 扣减库存
*/
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200,"扣减库存成功!");
}
}
domaim
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_storage")
public class TStorage {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 产品id
*/
@TableField(value = "product_id")
private Long productId;
/**
* 总库存
*/
@TableField(value = "total")
private Integer total;
/**
* 已用库存
*/
@TableField(value = "used")
private Integer used;
/**
* 剩余库存
*/
@TableField(value = "residue")
private Integer residue;
}
mapper
public interface TStorageMapper extends BaseMapper<TStorage> {
//扣减库存
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
<?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.seatastorageservice2002.mapper.TStorageMapper">
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},residue = residue - #{count}
WHERE
product_id = #{productId}
</update>
</mapper>
service
public interface TStorageService extends IService<TStorage>{
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
@Service
public class TStorageServiceImpl extends ServiceImpl<TStorageMapper, TStorage> implements TStorageService{
private static final Logger LOGGER = LoggerFactory.getLogger(TStorageServiceImpl.class);
@Autowired
private TStorageMapper tStorageMapper;
/**
* 扣减库存
* @param productId
* @param count
*/
@Override
public void decrease(Long productId, Integer count) {
LOGGER.info("------->storage-service中扣减库存开始");
tStorageMapper.decrease(productId,count);
LOGGER.info("------->storage-service中扣减库存结束");
}
}
application.yml
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
# seata 配置
seata:
# 使用哪个事务组
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
logging:
level:
io:
seata: info
mybatis-plus:
mapperLocations: classpath:mapper/*.xml
搭建2003模块
controller
@RestController
public class AccountController {
@Resource
TAccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
public CommonResult decrease(Long userId, Long money){
accountService.decrease(userId,money);
return new CommonResult(200,"扣减账户余额成功!");
}
}
TAccount
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_account")
public class TAccount {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户id
*/
@TableField(value = "user_id")
private Long userId;
/**
* 总额度
*/
@TableField(value = "total")
private Long total;
/**
* 已用额度
*/
@TableField(value = "used")
private Long used;
/**
* 剩余可用额度
*/
@TableField(value = "residue")
private Long residue;
}
mapper
public interface TAccountMapper extends BaseMapper<TAccount> {
/**
* 扣减账户余额
*/
void decrease(@Param("userId") Long userId, @Param("money") Long money);
}
<?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.seataaccountservice2003.mapper.TAccountMapper">
<update id="decrease">
UPDATE t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE
user_id = #{userId};
</update>
</mapper>
service
public interface TAccountService extends IService<TAccount>{
/**
* 扣减账户余额
*/
void decrease( Long userId, Long money);
}
/**
* 账户业务实现类
*/
@Service
public class TAccountServiceImpl extends ServiceImpl<TAccountMapper, TAccount> implements TAccountService{
private static final Logger LOGGER = LoggerFactory.getLogger(TAccountServiceImpl.class);
@Autowired
public TAccountMapper tAccountMapper;
/**
* 扣减账户余额
*
* @param userId
* @param money
*/
@Override
public void decrease(Long userId, Long money) {
LOGGER.info("------->account-service中扣减账户余额开始");
//模拟超时异常,全局事务回滚
//暂停几秒钟线程
// try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
tAccountMapper.decrease(userId,money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
}
application.yml
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
logging:
level:
io:
seata: info
mybatis-plus:
mapperLocations: classpath:mapper/*.xml
启动三个模块浏览器访问
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100