Spring Cloud Alibaba学习笔记
Seata分布式事务
Seata概述
分布式事务简介
- 对于分布式事务,通俗地说就是,一次操作由若干分支操作组成,这些分支操作分属不同应用,分布在不同服务器上。分布式事务需要保证这些分支操作要么全部成功,要么全部失败。分布式事务与普通事务一样,就是为了保证操作结果的一致性。
Seata简介
- Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务(官网)。
- Seata 官网:http://seata.io/zh-cn/
Seata术语
TC
- Transaction Coordinator,事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM
- Transaction Manager,事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务。它实际是全局事务的发起者。
RM
- Resource Manager,资源管理器。管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
- 常见的 DBMS 在分布式系统中都是以 RM 的角色出现的。
分布式事务模式
- Seata 提供了 AT、TCC、SAGA 与 XA 事务模式。
业务场景模拟
- 为了方便以下分布式模式的理解,下面以特定的场景来分析。
- 如下原理假设这样一种场景:某企业完成了一次采购,需要修改 DRP 系统中的数据。在系统中提交了本次采购清单后,本次提交操作实际修改了分属两个 DBMS 的
采购表 purchase
与库存表 stock
。而这两个表的修改是需要满足一致性,即本次操作实际是由应用程序 发起了一个分布式的事务。
XA模式
- 官方文档:http://seata.io/zh-cn/docs/dev/mode/xa-mode.html
- XA (Unix Transaction) 模型是基于 XA 协议的。 XA 协议由 Tuxedo Transaction for Unix has been Extended for Distributed Operation 分布式操作扩展之后的 Unix 事务系统 )首先提出的并交给 X/Open 组织,作为资源管理器与事务管理器的接口标准。
XA 模式是一个典型的 2PC ,其执行原理如下:
- TM 向 TC 发起指令,开启一个全局事务。
- 根据业务要求,各个 RM 会逐个向 TC 注册分支事务,然后 TC 会逐个向 RM 发出预执行指令。(第一阶段提交)
- 各个 RM 在接收到指令后会在 本地 进行 事务预执行。
- RM 将预执行结果 Report 给 TC 。当然,这个结果可能是成功,也可能是失败。
- TC 在接收到各个 RM 的 Report 后会将汇总结果上报给 TM,根据汇总结果 TM 会向 TC 发出确认指令。
- TC 在接收到指令后再次向 RM 发送确认指令。(第二阶段提交)
AT模式
- 官方文档:http://seata.io/zh-cn/docs/dev/mode/at-mode.html
- 原理:AT Automatic Transaction。AT 模式是 Seata 默认的分布式事务模型,是由 XA 模式演变而来的,通过全局锁对 XA 模式中的一些问题进行了改 进。
- 自动清理回滚日志:当所有 RM 行执行完毕第二阶段的 Global Commit 后, AT 模式能够自动以异步方式批量清理掉回滚日志,而 XA 模式不会清理,需要手动清理。
TCC模式
- 官方文档:http://seata.io/zh-cn/docs/dev/mode/tcc-mode.html
- TCC Try Confirm/Cancel。
- TCC 同样也是 2PC 的,其与 AT 的重要区别是,支持将自定义的分支事务纳入到全局事务管理中。
- 该原理图与 XA (AT)模式的原理图是没有区别的。
2PC 缺陷
2PC 最大的特点就是简单:原理简单,实现简单。但却存在先天缺陷:同步阻塞、中心化问题、数据不一致、太过保守等。不会,若实现方案设计的较好,这些缺陷是可以弱化的。
Saga模式
- 官方文档:http://seata.io/zh-cn/docs/user/saga.html
- Saga 模式是 1987 年由普林斯顿大学的 Kenneth Salem 和 Hector Garcia Molina 共同提出的。该模式不同于前面的模式,它不是 2PC 的,其是一种长事务解决方案 。
- 其应用场景是:在无法提供 TC、TM、RM 接口的情况下,对于一个流程很长的复杂业务,其会包含很多的子流程(事务)。每个子流程都让它们真实提交它们真正的执行结果。只有当前子流程执行成功后才能执行下一个子流程。 若整个流程中所有子流程全部执行成功,则整个业务流程成功;只要有一个子流程执行失败,则可采用两种补偿方式:
- 向后恢复:对于其前面所有成功的子流程,其执行结果全部撤销。
- 向前恢复:重试失败的子流程直到其成功。
Seata-Server的配置与启动
- 我们下面就以 Seata 默认的 AT 事务模式来实现分布式事务。
Seata 下载
- 从官网下载 Seata Server,源码与打过包的都需要下载。源码中包含很多需要运行的脚本文件,而打过包的则是可运行的服务器本身。
- 版本选择:因为我们现在使用的 Spring Cloud Alibaba 版本是 2.2.3.RELEASE,所以选择 Seata 版本是 1.3.0
- 下载地址:http://seata.io/zh-cn/blog/download.html
wget https://github.com/seata/seata/archive/v1.3.0.zip
wget https://github.com/seata/seata/releases/download/v1.3.0/seata-server-1.3.0.zip
运行 mysql.sql 脚本
- 在 seata 源码解压目录的 script/server/db 下找到 mysql.sql 文件运行。该脚本会创建三张表。这三张表都是用于保存整个系统中分布式事务相关日志数据的。
-- create database
create database IF NOT EXISTS `seata`;
use `seata`;
-- 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(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;
修改 file.conf
- 修改 seata-server 解压目录下 conf 中的 file.conf 文件。该配置文件用于指定 seata server 存放日志的位置。
[root@centos130 seata]# vim conf/file.conf
修改 registry.conf
- 修改 seata-server 解压目录下 conf 中的 registry.conf 文件。该配置文件用于指定 seata 要连接的注册中心与配置中心。
[root@centos130 seata]# vim conf/registry.conf
修改 config.txt
- 将 seata 源码解压目录的 script/config-center 下的 config.txt 文件复制到 seata-server 解压目录的根目录中,然后再进行修改。
MacBook-Pro:config-center yangwei$ scp config.txt root@192.168.254.130:/usr/apps/seata
[root@centos130 seata]# vim config.txt
运行 nacos-config.sh 脚本
- 在 seata 源码解压目录的 script/config-center/nacos 下有 nacos-config.sh 脚本文件。将该脚本文件复制到 config.txt 的下一级目录中。
- 在运行脚本前,先确保 nacos 已启动。
MacBook-Pro:nacos yangwei$ scp nacos-config.sh root@192.168.254.130:/usr/apps/seata/bin
# 运行脚本,192.168.0.100 是 nacos地址
[root@centos130 bin]# sh nacos-config.sh -h 192.168.0.100
- 去到 nacos 可以查看到seata的相关配置:
新建日志目录
- 在 seata server 的解压目录的根目录下新建一目录 logs,将来用于存放 seata 的运行日志。
启动 Seata-server
- 在 seata-server 解压目录下的 bin 目录中有启动的脚本文件 seata-server.sh【mac、linux】和 seata-server.bat【windows】。
# seata-server.sh -m db -p 8091 -h 127.0.0.1
[root@centos130 bin]# ./seata-server.sh -m db -h 192.168.254.130
- 在 nacos 查看服务列表:
测试环境搭建
需求
- 使用 DRP 系统中的 purchase 采购模块调用 stock 库存模块来模拟消费者调用提供者。
- 当采购了新的生产资料时,会调用采购模块,增加采购表 purchase 中的相应生产资料数量,并调用库存模块增加库存表 stock 中相应生产资料数量。只有这样才能保证了数据的一致性。
库存工程 07-drp-stock-8081
- 该工程充当着 Provider 工程。
- 创建工程:复制 03-provider-nacos-config-8081 工程,并重命名为 07-drp-stock-8081,将原来工程中的所有类删除,保留原有的配置文件。
- 定义实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "fieldHandler"})
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
// 库存
private Integer total;
}
- 定义 StockRepository 接口:
public interface StockRepository extends JpaRepository<Stock, Integer> {
Stock findByName(String name);
}
- 定义 StockService 接口及其实现类:
public interface StockService {
boolean addStock(Stock stock);
Stock getStockByName(String name);
}
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockRepository stockRepository;
@Override
public boolean addStock(Stock stock) {
Stock originalStock = getStockByName(stock.getName());
originalStock.setTotal(originalStock.getTotal() + stock.getTotal());
return stockRepository.save(originalStock) != null;
}
@Override
public Stock getStockByName(String name) {
Stock stock = stockRepository.findByName(name);
return stock == null ? new Stock(null, name, 0) : stock;
}
}
- 定义 StockController 类
@RestController
public class StockController {
@Autowired
private StockService stockService;
@PostMapping("/stock/add")
public boolean addStock(@RequestBody Stock stock) {
return stockService.addStock(stock);
}
}
- 启动类:
@SpringBootApplication
public class Stock078081 {
public static void main(String[] args) {
SpringApplication.run(Stock078081.class, args);
}
}
- 修改配置文件:
spring:
application:
name: msc-stock-service
cloud:
nacos:
config:
file-extension: yml
server-addr: 192.168.0.100:8848
group: SEATA_GROUP
- 在 nacos-config 中新建配置文件:
server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.0.100:8848
jpa:
generate-ddl: true
show-sql: true
hibernate:
ddl-auto: none
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.254.128:3306/test?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
logging:
pattern:
console: level-%level %msg%n
level:
root: info
org.hibernate: info
org.hibernate.type.descriptor.sql.BasicBinder: trace
org.hibernate.type.descriptor.sql.BasicExtractor: trace
com.yw.sca: debug
- 启动工程,服务自动注册到 nacos,并获取到配置、自动创建 stock 表,接下来调用接口给 stock 表添加数据:
采购工程 07-drp-purchase-8080
- 该工程充当着 Consumer 工程。
- 创建工程:复制 07-drp-stock-8081 工程,并重命名为 07-drp-purchase-8080,将原来工程中的所有类删除,保留原有的配置文件。
- 定义实体类 Purchase 和 Stock:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "fieldHandler"})
public class Purchase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// 采购资源名称
private String name;
// 采购量
private Integer count;
}
@Data
public class Stock {
private Integer id;
private String name;
private Integer total;
}
- 定义 PurchaseRepository 接口:
public interface PurchaseRepository extends JpaRepository<Purchase, Integer> {
Purchase findByName(String name);
}
- 定义 PurchaseService 接口及其实现类:
public interface PurchaseService {
boolean addPurchase(Purchase purchase);
Purchase getPurchaseByName(String name);
}
public class PurchaseServiceImpl implements PurchaseService {
@Autowired
private PurchaseRepository purchaseRepository;
@Override
public boolean addPurchase(Purchase stock) {
Purchase originalPurchase = getPurchaseByName(stock.getName());
originalPurchase.setCount(originalPurchase.getCount() + stock.getCount());
return purchaseRepository.save(originalPurchase) != null;
}
@Override
public Purchase getPurchaseByName(String name) {
Purchase purchase = purchaseRepository.findByName(name);
return purchase == null ? new Purchase(null, name, 0) : purchase;
}
}
- 定义 PurchaseController 类:
@RestController
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@Autowired
private RestTemplate restTemplate;
private static final String STOCK_SERVICE_URL = "http://msc-stock-service";
@PostMapping("/purchase/add/{deno}")
public Boolean addPurchase(@RequestBody Purchase purchase, @PathVariable("deno") int deno) {
purchaseService.addPurchase(purchase);
int i = 3 / deno;
Stock stock = new Stock();
stock.setName(purchase.getName());
stock.setTotal(purchase.getCount());
String url = STOCK_SERVICE_URL + "/stock/add";
return restTemplate.postForObject(url, stock, Boolean.class);
}
}
- 定义启动类:
@SpringBootApplication
public class Purchase078080 {
public static void main(String[] args) {
SpringApplication.run(Purchase078080.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 修改配置文件:
spring:
application:
name: msc-purchase-service
cloud:
nacos:
config:
file-extension: yml
server-addr: 192.168.0.100:8848
group: SEATA_GROUP
- 在 nacos-config 中新建配置文件:克隆 msc-stock-service 的配置,修改端口号为 8080 即可。
- 启动工程,服务自动注册到 nacos,并获取到配置、自动创建 purchase 表,接下来调用接口进行采购:
- 模拟正常情况调用
http://localhost:8080/purchase/add/1
进行添加,没有出现异常,stock 和 purchase 表都各自增加了 50。 - 模拟异常情况调用
http://localhost:8080/purchase/add/0
进行添加,会出现异常,stock 表没有增加,而 purchase 表增加了,这样就造成了数据的不一致。
配置Seata-Client
- Seata-Client,即安装有 Seata 依赖的应用。
创建两个工程
- 为了保留下原始代码,我们这里复制 07-drp-stock-8081 与 07-drp-purchase-8080 两个工程,分别重新命名为 07-seata-stock-8081 与 07-seata-purchase-8080,将这两个新工程改造为Seata-Client。
添加 undo_log 表
- 在客户端处理的业务相关的每个数据库中都要添加 undo_log 表,用于保存需要回滚的数据。建表语句脚本在 seata 源码解压目录的 script/client/at/db 目录下的 mysql.sql 中。
CREATE TABLE IF NOT EXISTS `undo_log`
(
`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(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 = utf8 COMMENT ='AT transaction mode undo table';
添加依赖
- 在这两个工程添加 seata 依赖:
<!-- seata依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
修改 nacos 配置文件
- 分别在 msc-stock-service 和 msc-purchase-service 两个配置文件中增加如下配置。
添加@GlobalTransactional 注解
- 在需要开启分布式事务的方法上添加@GlobalTransactional 注解。
- 本例就是在 07-seata-purchase-8080 工程的 PurchaseController 的 addPurchase()方法上添加 @GlobalTransactional 注解。
测试
- 关系梳理:07-seata-drp-purchase-8080 工程既是 TM 又是 RM,07-seata-drp-stock-8081 是 RM,Seata Sever 是 TC。
- 启动 07-seata-drp-stock-8081、07-seata-drp-purchase-8080 这两个工程,查看控制台输出如下信息,说明 seata client 注册成功。
- 再次模拟异常情况调用
http://localhost:8080/purchase/add/0
,验证下当 purchase 添加后出现异常时,是否都发生了回滚。 - 为了查看细节,我们以debug方式运行 07-seata-drp-purchase-8080 工程,在发生异常的地方
int i = 3 / deno
那里打上断点,然后查看数据库 undo_log 表:
- 回滚日志输出:
调用链路跟踪SkyWalking
- 随着分布式系统规模的越来越大,各微服务间的调用关系也变得越来越复杂。一般情况下,一个由客户端发起的请求在后端系统中会经过许多不同的微服务调用才能完成最终处理,而这些调用过程形成了一个复杂的分布式服务调用链路。
- 那么也就带来一系列问题:怎么样快速发现并定位问题?怎么样判断故障影响范围?各部分调用链路性能是怎样的?对于这些问题,通过分布式服务跟踪系统可以解决。
- 生产环境下,Spring Cloud Alibaba 中一般会使用 SkyWalking 作为调用链跟踪系统。
- 国内的分布式跟踪系统:
- 阿里:eagleeye,鹰眼(未开源)
- 京东:Hydra
- 新浪:watchman
- 唯品会:microScope
- 大众点评:cat
- Spring Cloud Netflix 中一般会使用 Spring Cloud Sleuth + ZipKin + Kafka
- 业界的分布式服务跟踪理论基础主要来自于 Google 的一篇论文《Dapper》。
SkyWalking 简介
- SkyWalking 是由国内开源爱好者吴晟开源并提交到 Apache 孵化器的产品,现在是 Apache 的顶级项目。其是一个开源的 APM(应用性能管理系统)和可观测性分析平台。
- 其是通过在被监视应用中插入探针,以无侵入方式自动收集所需指标,并自动推送到OAP 系统。OAP 系统会将收集到的数据存储到指定的存储介质 Storage。UI 系统通过调用 OAP 提供的接口,可以实现对相应数据的查询。
系统架构
SkyWalking 系统整体由四部分构成:
- Agent:探针,无侵入收集,是被插入到被监测应用中的一个进程。其会将收集到的监控指标自动推送给 OAP 系统。
- OAP:Observability Analysis Platform,可观测性分析平台,其包含一个收集器 Collector,能够将来自于 Agent 的数据收集并存储到相应的存储介质 Storage。
- Storage:用于存储由 OAP 系统收集到的数据,支持 H2、MySQL、ElasticSearch 等。默认使用的是 H2,推荐使用 ElasticSearch。
- UI:一个独立运行的可视化 Web 平台,其通过调用 OAP 系统提供的接口,可以对存储在 Storage 中的数据进行多维度查询。
SkyWalking的安装启动
下载
wget https://www.apache.org/dyn/closer.cgi/skywalking/8.1.0/apache-skywalking-apm-es7-8.1.0.tar.gz
配置
- 配置 storage:
[root@centos130 apache-skywalking-apm-bin-es7]# vim config/application.yml
- 修改 UI 端口:默认情况下,SW 的 UI 应用端口号为 8080,其会与很多的应用端口号冲突。该端口号的修改位置是在 SW 解压目录下的 webapp 目录中的 webapp.yml 文件中。
[root@centos130 apache-skywalking-apm-bin-es7]# vim webapp/webapp.yml
启动
- 在 skywalking 目录的 bin 下有 startup.sh [mac、linux] 和 startup.bat [windows] 的启动脚本。
[root@centos130 bin]# ./startup.sh
SkyWalking OAP started successfully!
SkyWalking Web Application started successfully!
- 访问地址:http://192.168.254.130:10080/,正常显示以下界面说明安装成功。
agent的安装配置
- 下面要将 Agent 安装配置到准备由 SW 跟踪的应用。我们这里创建两个应用,provider 与 consumer。
查找 agent 目录
- agent 目录在 skywalking 解压目录下。
定义工程 08-provider-skywalking-8081
- 创建工程:复制工程 02-provider-nacos-8081 并重命名为 08-provider-skywalking-8081,该工程中的代码与配置完全不用修改。
- 复制 agent:将 agent 目录全部复制到该工程中。
- 修改 agent.config:
- 启动:在设置了 VM options 后启动应用即可。
定义工程 08-consumer-skywalking-8080
- 创建工程:复制工程 02-consumer-nacos-8080 并重命名为 08-consumer-skywalking-8080,该工程中的代码与配置完全不用修改。
- 和 provider 工程一样复制 agent,并修改 agent.config,设置 VM options 并启动。
测试
- 访问地址:http://localhost:8080/consumer/depart/get/1、http://localhost:8080/consumer/depart/list,多调用几次,然后去到 skywalking 的 UI 界面查看。
- 仪表盘:
- 拓扑图:
- 追踪: