Sentinel服务保护 + Seata分布式事务
服务保护
【雪崩问题
】微服务调用链路中某个服务,引起整个链路中所有微服务都不可用。
【原因
】:
- 微服务相互调用,服务提供者出现故障。
- 服务调用这没有做好异常处理,导致自身故障。
- 调用链中所有服务级联失败,导致整个集群故障。
【解决方案
】:
请求限流、线程隔离、服务熔断
【服务保护技术
】:
Sentinel服务保护
官方文档:Sentinel
使用步骤
1. 使用docker部署sentinel
创建并运行sentinel容器:
docker run -d \
--net=host \
--name sentinel \
--restart=always \
-e AUTH_USERNAME=admin \
-e AUTH_PASSWORD=admin \
bladex/sentinel-dashboard:1.8.6
完成后在浏览器输入:192.168.140.101:8858
,用户名admin,密码admin
2.在微服务中连接sentinel控制台
引入sentinel依赖:
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置控制台:
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.140.101:8858 # sentinel的控制台地址
http-method-specify: true # 开启请求方式前缀
Restful风格的API请求路径一般相同,会导致簇点资源名称重复。所以我们要修改配置,把
请求方式 + 请求路径
作为簇点资源名称
簇点链路
簇点链路就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源链。默认Sentinel会监控SpringMVC的每一个Endpoint(Http接口)。限流、熔断等都是争对簇点链路中的资源设置的,资源名默认就是接口的请求路径。
解决方案
1. 请求限流
限制访问微服务的请求的并发量,避免服务因流量激增而出现故障。
这个接口每秒钟只能处理6个请求,使用ApiFox进行测试,会有部分请求失败【失败返回状态码429】。
2. 线程隔离
通过限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散。
【场景
】:假设有大量的查询购物车的请求,通过对查询购物车这个线程做线程隔离,可以保证购物车这个微服务的资源不会被耗尽,不会对修改购物车等其他业务造成影响。
线程隔离和请求限流的区别:
请求限流:控制接受请求的速度
(每秒访问几次)
线程隔离:控制最多能接收请求的次数
(一次访问的线程数)
就算请求限流设置的再慢,如果线程卡住的话,不设置线程隔离,也会导致资源占用。
3. fallback
一、 将FeignClient作为Sentinel的簇点资源:
feign:
sentinel:
enabled: true # 开启流量控制
二、 为FeignClient添加Fallback:
- 方法1:FallbackClass,无法对远程调用的异常做处理
- 方法2:
FallbackFactory
,可以对远程调用的异常做处理
- 自定义类,实现FallbackFactory,编写对某个FeignClient的fallback逻辑:
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
// 编写失败的处理逻辑(失败后就会走里边的方法)
return new ItemClient() {
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("查询商品失败,"+ cause);
return CollUtils.emptyList();
}
@Override
public void deductStock(List<OrderDetailDTO> items) {
log.error("扣减商品库存失败,"+ cause);
throw new RuntimeException(cause);
}
};
}
}
- 将定义的FallbackFactory注册为一个Bean:
public class DefaultFeignConfig {
@Bean
public ItemClientFallbackFactory itemClientFallbackFactory() {
return new ItemClientFallbackFactory();
}
}
- 在ItemClient接口中使用FallbackFactory:
@FeignClient(value = "item-service", fallbackFactory = ItemClientFallbackFactory.class)
@FeignClient(value = "item-service", fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
@PutMapping("/items/stock/deduct")
void deductStock(@RequestBody List<OrderDetailDTO> items);
}
4. 服务熔断
由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截改接口的请求。熔断期间,所有请求快速失败
,全部走fallback逻辑。当服务恢复时,断路器会放行访问该服务的请求。
断路器工作原理:
默认情况:Closed状态
如果失败的比例过高:就会进入Open状态
【拦截一切请求,快速失败】
在Open状态
下会尝试放行一次请求,进入Half-Open状态
,如果仍然失败,再次返回Open状态;如果成功,回到Closed状态
。
配置熔断策略
分布式事务
如果一个业务需要多个服务合作完成,而且每个服务都有事务,多个事务必须同时成功或同时失败,这样的事务就是分布式事务
。其中每一个服务的事务就是一个分支事务
。整个业务称为全局事务
。
【场景
】:用户下单后,订单服务首先创建订单,随后调用购物车服务清理购物车,最后调用库存服务扣减商品的库存。如果在调用库存服务的时候商品库存为0,此时扣减库存失败,订单服务和购物车服务应该同时失败。
【出现问题的原因
】:各个分支服务不知道对方的情况
【解决思路
】:让各个分支事务感受到对方的存在,让所有的微服务向事务协调者报告当前的状态。
Seata架构
- 事务协调者(TC):维护
全局和分支事务
的状态,协调全局事务提交或回滚。 - 事务管理器(TM):定义
全局事务
的范围、开始全局事务、提交或回滚全局事务。 - 资源管理器(RM):管理
分支事务
,与TC交谈以注册分支事务和报告分支事务的状态。
1. 部署TC服务
- 创建数据库,导入sql文件
CREATE DATABASE IF NOT EXISTS `seata`;
USE `seata`;
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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
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 = utf8mb4;
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),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
- 准备配置文件
seata运行时所需的配置文件
上传前先看看application.yml,里边可能有些配置需要改一下
把上边的配置文件丢到root根目录下
- docker部署
在/root目录下执行以下命令,创建并允许seata容器
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.140.101 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
--restart=always \
-d \
seataio/seata-server:1.5.2
- 以上操作都完成后,在浏览器输入:
http://192.168.140.101:7099/
后即可登录seata控制台。(初始账号:admin、密码:admin)
2. 微服务集成Seata
- 引入Seata依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
- 配置TC服务地址,让微服务找到TC服务地址
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.140.101:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
因为很多服务都需要实现分布式事务,所以可以把对于seata的配置抽取成一个共享配置写在nacos里。所以添加依赖的时候,检查一下是否有
bootstrap
和nacos配置管理
的依赖。
查看seata的日志文件,可以看到购物车服务、交易服务、商品服务已经全部和seata的TC服务建立连接。
3. Seata解决分布式事务问题
XA模式—强一致
- 一阶段工作:
- RM注册分支事务
- RM执行分支事务sql
但不提交
- RM报告执行状态到TC
- 二阶段工作:
- TC检测各分支事务执行状态:
- 如果都成功,通知所有RM
提交事务
- 如果有失败,通知所有RM
回滚事务
- 如果都成功,通知所有RM
- RM接收到TC指令,提交或回滚事务
通过“等待”的方式,确保了全局事务的ACID特性。但是一阶段需要锁定数据库的资源,到二阶段才释放,性能差。
实现步骤
- 修改(每个参与事务的微服务)application.yml文件,开启XA模式
seata:
data-source-proxy-mode: XA
- 给发起全局事务的入口添加
@GlobalTransactional
注解
AT模式(主推)—最终一致
AT模式弥补了XA模式中资源锁定周期过长的缺陷。
- 一阶段RM的工作:
- 注册分支事务
- 记录
undo-log
(数据快照) - 执行业务sql
并提交
- 报告事务状态
- 二阶段提交时RM的工作:
- 删除undo-log即可
- 二阶段回滚时RM的工作:
- 根据undo-log恢复数据到更新前
AT模式相比于XA模式的优点在于:在一阶段不需要等待彼此执行,而是各自提交,这样资源就没有锁定,性能也会好。
但是如果二阶段需要进行回滚,在回滚之前,会出现数据短暂的不一致。
【AT模式与XA模式的区别
】:
- XA模式一阶段不提交事务,锁定资源
AT模式一阶段直接提交,不锁定资源- XA模式依赖数据库机制实现回滚
AT模式利用数据快照实现回滚- XA模式强一致
AT模型最终一致
实现步骤
- 创建数据表,导入用来记录数据快照的undo_log表
【注
】:每个分支事务都需要有自己的undo_log表
-- 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`
(
`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';
- 修改application.yml文件,将事务模式修改为AT模式
seata:
data-source-proxy-mode: AT
数据快照(undo_log表):