Table of Contents
1.Seata 介绍
http://seata.io/zh-cn/docs/overview/what-is-seata.html
https://github.com/seata/seata/tags
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
术语:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata管理的分布式事务的典型生命周期:
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
2.Seata-Server 安装
1.下载
链接:https://pan.baidu.com/s/1xBTHgXk3jzJ38qVIMaGDVw
提取码:snnm
2.修改file.conf
自定义事务组名称 + 事务日志存储模式为db + 数据库连接信息
3.执行sql
https://github.com/seata/seata/blob/1.0.0/script/server/db/mysql.sql
注意:Seata 1.0.0版本下载的文件里面 /conf目录下面不会有db_store.sql文件
-- 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,
`gmt_modified` DATETIME,
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(96),
`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;
4.修改registry.conf
5.启动Nacos、Seata
启动参数设置
启动文件:seata-server.bat
用文本编辑器打开文件,找到文件中这一行:
%JAVACMD% %JAVA_OPTS% -server -Xmx2048m -Xms2048m -Xmn1024m -Xss512k -XX:Sur......
看到 Seata Server 默认使用 2G 内存,测试环境我们可以把内存调低:
%JAVACMD% %JAVA_OPTS% -server -Xmx256m -Xms256m -Xmn128m -Xss512k -XX:Sur......
3.订单、库存、账户分布式搭建
1.场景
- 创建三个服务,一个订单服务,一个库存服务,一个账户服务
- 当用户下单时,会在订单服务中创建一个订单,
- 然后通过远程调用库存服务来扣减下单商品的库存,
- 再通过远程调用账户服务来扣减用户账户里面的余额,
- 最后在订单服务中修改订单状态为已完成.
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题
2.分布式架构图
3.业务数据库创建
/**订单数据库**/
CREATE DATABASE seata_order DEFAULT CHARACTER SET utf8;
/**订单表**/
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 0 COMMENT '数量',
money DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
STATUS INT(1) DEFAULT 0 COMMENT '订单状态: 0:创建中:1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
--------------------------------------------------------------
/**库存数据库**/
CREATE DATABASE seata_storage DEFAULT CHARACTER SET utf8;
/**库存表**/
CREATE TABLE t_storage(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
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 '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(id,product_id,total,used,residue)
VALUES(1,1,100,0,100);
--------------------------------------------------------------
/**账户数据库**/
CREATE DATABASE seata_account DEFAULT CHARACTER SET utf8;
/**账户表**/
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=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(id,user_id,total,used,residue)
VALUES(1,1,1000,0,1000);
4.对应库分别创建回滚日志表
https://github.com/seata/seata/blob/1.0.0/script/client/at/db/mysql.sql
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
5.业务微服务创建
@GlobalTransactional
代码:https://github.com/akeung/springclouddemo
pom
<dependencies>
<!--整合 seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.seata/seata-spring-boot-starter -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!--整合 open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.ak.demo</groupId>
<artifactId>api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<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 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.verison}</version>
</dependency>
<!-- mybatis-springboot整合 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.verison}</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
yml
server:
port: 8911
spring:
application:
name: seata-order
cloud:
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.71.128:3306/seata_order?useSSL=false
username: root
password: 123456
feign:
hystrix:
enabled: true
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.ak.demo.entities
logging:
level:
io:
seata: info
com.ak.demo.dao: debug
seata:
enabled: true #默认
tx-service-group: ak_tx_group #与seata-server的file.conf的对应
registry:
type: nacos
nacos:
server-addr: localhost:8848
client:
support:
spring:
datasource-autoproxy: true
service:
disable-global-transaction: false
测试:
http://localhost:8911/order/create?userId=1&productId=1&money=10&count=10
4.Seata 事务模式
- Seata AT模式
- Seata TCC模式
- Seata Saga 模式
- Seata XA模式
1.Seata AT模式
机制
两阶段提交协议的演变:
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
上面的案例为该模式,seata的默认模式
2.Seata TCC模式
全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
TCC 与 Seata AT 事务一样都是两阶段事务,它与 AT 事务的主要区别为:
- TCC 对业务代码侵入严重
每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。 - TCC 效率更高
不必对数据加全局锁,允许多个事务同时操作数据。
举例:
通过 @LocalTCC 每个服务分别自定义自己的 TccAction,@TwoPhaseBusinessAction在prepare方法,并注明commit方法和rollback方法。以下只举例order模块,storage和account模块可参照实现 TccAction 和 TccActionImpl
@LocalTCC
public interface OrderTccAction {
/*
第一阶段的方法
通过注解指定第二阶段的两个方法名
BusinessActionContext 上下文对象,用来在两个阶段之间传递数据
@BusinessActionContextParameter 注解的参数数据会被存入 BusinessActionContext
*/
@TwoPhaseBusinessAction(name = "orderTccAction", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepareCreateOrder(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "orderId") Long orderId,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
// 第二阶段 - 提交
boolean commit(BusinessActionContext businessActionContext);
// 第二阶段 - 回滚
boolean rollback(BusinessActionContext businessActionContext);
}
@Component
@Slf4j
public class OrderTccActionImpl implements OrderTccAction {
@Autowired
private OrderMapper orderMapper;
@Transactional
@Override
public boolean prepareCreateOrder(BusinessActionContext businessActionContext, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
log.info("创建 order 第一阶段,预留资源 - "+businessActionContext.getXid());
Order order = new Order(orderId, userId, productId, count, money, 0);
orderMapper.create(order);
//事务成功,保存一个标识,供第二阶段进行判断
ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext businessActionContext) {
log.info("创建 order 第二阶段提交,修改订单状态1 - "+businessActionContext.getXid());
// 防止幂等性,如果commit阶段重复执行则直接返回
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}
//Long orderId = (Long) businessActionContext.getActionContext("orderId");
long orderId = Long.parseLong(businessActionContext.getActionContext("orderId").toString());
orderMapper.updateStatus(orderId, 1);
//提交成功是删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
log.info("创建 order 第二阶段回滚,删除订单 - "+businessActionContext.getXid());
//第一阶段没有完成的情况下,不必执行回滚
//因为第一阶段有本地事务,事务失败时已经进行了回滚。
//如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
//幂等性控制:如果重复执行回滚则直接返回
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}
//Long orderId = (Long) businessActionContext.getActionContext("orderId");
long orderId = Long.parseLong(businessActionContext.getActionContext("orderId").toString());
orderMapper.deleteById(orderId);
//回滚结束时,删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}
}
@Service
public class OrderServiceImpl implements OrderService {
// @Autowired
// private OrderMapper orderMapper;
@Autowired
EasyIdGeneratorClient easyIdGeneratorClient;
@Autowired
private AccountClient accountClient;
@Autowired
private StorageClient storageClient;
@Autowired
private OrderTccAction orderTccAction;
@GlobalTransactional
@Override
public void create(Order order) {
// 从全局唯一id发号器获得id
Long orderId = easyIdGeneratorClient.nextId("order_business");
order.setId(orderId);
// orderMapper.create(order);
// 这里修改成调用 TCC 第一节端方法
orderTccAction.prepareCreateOrder(
null,
order.getId(),
order.getUserId(),
order.getProductId(),
order.getCount(),
order.getMoney());
// 修改库存
storageClient.decrease(order.getProductId(), order.getCount());
// 修改账户余额
accountClient.decrease(order.getUserId(), order.getMoney());
}
}