一、服务保护和分布式事务
二、雪崩问题
微服务调用链路中某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
1. 雪崩问题产生的原因是什么?
- 微服务相互调用,服务提供者出现故障或阻塞;
- 服务调用者没有做好异常处理,导致自身故障;
- 调用链中的所有服务级联失败,导致整个集群故障
2. 解决问题的思路有哪些?
尽量避免服务出现故障或阻塞
- 保证代码的健壮性
- 保证网络畅通;
- 能应对较高的并发请求
服务调用者做好远程调用异常的后备方案,避免故障扩散。
服务保护技术 | ||
Sentinel | Hystrix | |
线程隔离 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断策略 | 基于慢调用比例或异常比例 | 基于异常比率 |
限流 | 基于QPS,支持流量整形 | 有限的支持 |
Fallback | 支持 | 支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
配置方式 | 基于控制台,重启后失效 | 基于注解或配置文件,永远生效 |
3. 服务保护方案 - 请求限流
请求限流:限制访问微服务的请求并发量,避免服务因流量激增出现故障。
4. 服务保护方案 - 线程隔离
线程隔离:也叫做舱壁模式,模拟船舱隔板的防水原理。通过限定每个业务能使用的线程数量二将故障业务隔离,避免故障扩散。
5. 服务保护方案 - 服务熔断
服务熔断:由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,拦截该接口的请求。熔断期间,所有请求快速失败,全都走fallback逻辑。
失败处理:定义fallback逻辑,让业务失败时不再抛出异常,而是返回默认数据或友好提示。
三、Sentinel
1. 初识Sentinel
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:home | Sentinel
使用步骤:
(1)sentinel的安装
①下载jar包:Releases · alibaba/Sentinel · GitHub
②运行
将jar包放在任意非中文,不包含任何特殊字符的目录下,重命名为sentinel-dashboard.jar
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
③访问 http://localhost:8090
用户名和密码都是sentinel
(2)微服务整合
我们在cart-servicr模块中整合sentinel,连接sentinel-dashboard控制台,步骤如下:
①引入sentinel依赖 pom.xml(cart-service)
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
②修改application.yaml文件,添加下面内容
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # sentinel的控制台地址
③访问cart-service
的任意端点
重启cart-service
,然后访问查询购物车接口,sentinel的客户端就会将服务访问的信息提交到sentinel-dashboard
控制台。并展示出统计信息:
④点击簇点链路菜单,会看到下面的页面:
⑤簇点链路
簇点链路
簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源链。默认Sentinel会监控SpringMVC的每一个Endpoint(http接口)。限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径:
Restful风格的API请求路径一般都相同,这会导致簇点资源名称重复。因此我们需要修改配置,把请求方式+请求路径作为簇点资源名称。
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # sentinel的控制台地址
http-method-specify: true # 开启请求方式前缀
重启CartApplication,访问购物车相关接口
2. 请求限流
使用步骤:
①在簇点链路后面点击流控按钮,即可对其做限流设置
②双击运行jemter
③把day05资料中的雪崩测试.jmx拖入jmeter
④启动测试
⑤察看结果树和汇总报告
3. 线程隔离
当商品服务出现阻塞或故障时,调用商品服务的购物车服务可能因此而被拖慢,甚至资源耗尽。所以必须限制购物车服务中查询商品这个业务的可用线程数,实现线程隔离。
使用步骤:
①在sentinel控制台中,会出现Feign接口的簇点资源,点击后面的流控按钮,即可配置线程隔离
(5个并发线程,如果单线程QPS为2,则5线程QPS为10)
②修改item-service的ItemController里的queryItemByIds进行测试
@ApiOperation("根据id批量查询商品")
@GetMapping
public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){
// 模拟业务延迟
ThreadUtil.sleep(500);
return itemService.queryItemByIds(ids);
}
③在cart-service里的application.yaml新增tomcat限制
server:
port: 8082
tomcat:
threads:
max: 50
accept-count: 25
max-connections: 100
④重启ItemApplication、ItemApplication2、CartApplication,进行线程隔离测试
⑤查询购物车异常,但其他服务正常
4. Fallback
使用步骤:
①将FeignClient作为Sentinel的簇点资源:application.yaml(cart-service)
feign:
sentinel:
enabled: true
②FeignClient的Fallback有两种配置方式:
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,通常都会选择这种。
案例:给FeignClient编写Fallback逻辑
假如我们有一个FiegnClient如下:
@FeignClient(value = "userservice")
public interface UserClient {
@GetMapping("/user/{id})
User findById(@PathVariable("id") Long id);
}
为其编写Fallback逻辑
使用步骤:
①自定义类,实现FallbackFactory,编写对某个FeignClient的fallback逻辑
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
// 创建UserClient接口实现类,实现其中的方法,编写失败降级的处理逻辑
return new UserClient() {
@Override
public User findById(Long id) {
// 记录异常信息,可以返回空或抛出异常
log.error("查询用户失败", throwable);
return null;
}
};
}
}
②将刚刚定义的UserClientFallbackFactory注册为一个Bean:
@Bean
public UserClientFallbackFactory userClientFallback() {
return new UserClientFallbackFactory();
}
③在UserClient接口中使用UserClientFallbackFactory
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
步骤:hm-api模块
①在hm-api模块下的client新建fallback.ItemClientFallbackFactory
package com.hmall.api.client.fallback;
import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import com.hmall.common.utils.CollUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import java.util.Collection;
import java.util.List;
@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);
}
};
}
}
②在DefaultFeignConfig中新增
@Bean
public ItemClientFallbackFactory itemClientFallbackFactory() {
return new ItemClientFallbackFactory();
}
③ItemClient
@FeignClient(value = "item-service", fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
// ... ...
}
④开启sentinel监控 -> cart-service模块的application.yaml
feign:
sentinel:
enabled: true
⑤重启CartApplication进行测试
⑥新增流控(注意要删除之前配置的流控规则)
5. 服务熔断
熔断是解决雪崩问题的重要手段。思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
点击控制台中簇点资源后的熔断按钮,即可配置熔断策略:
步骤:
①新增熔断规则
②线程隔离测试
四、分布式事务
1. 分布式事务
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务的事务就是一个分支事务。整个业务称为全局事务。
下单业务,前端请求首先进入订单服务,创建订单并写入数据库。然后订单服务调用购物车服务和库存服务:
- 购物车服务负责清理购物车信息
- 库存服务负责扣减商品库存
2. 初始Seata
Seata是2019年1月蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:Apache Seata,其中的文档、博客中提供了大量的使用说明、源码分析。
分布式事务解决思路
解决分布式事务,各个子事务之间必须能感知到彼此的事务状态,才能保证状态一致。
2.1 Seata架构
Seata事务管理中有三个重要角色:
- TC(Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM(Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM(Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态。
3. 部署TC服务
参考文档进行部署:Docs
使用步骤:
①准备数据库表:执行课前资料里提供的seata-tc.sql
②准备配置文件
把课前资料里的下图两个文件上传到虚拟机root根目录下
(注意要根据自己的实际情况更改application.yml里数据库的连接密码)
③加载seata镜像
④检测mysql和nacos是否在hm-net网络里,如果不在则添加进入
查看:
docker inspect nacos
docker inspect mysql
添加:
docker network connect hm-net nacos
docker network connect hm-net mysql
⑤在虚拟机/root下执行以下命令(根据自己的情况改IP地址)
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.126.151 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2
⑥查看seata运行日志
docker logs -f seata
⑦在nacos查看服务列表 http://192.168.126.151:8848/
⑧访问 http://192.168.126.151:7099/
用户名和密码都是admin
4. 微服务集成Seata
步骤:以cart-service模块为例
①在项目中引入Seata依赖 pom.xml(cart-service、item-service、trade-service)
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
②在application.yaml里添加配置,让微服务找到TC服务地址
注意,需要改成自己的虚拟机地址
同时,在cart-service、item-service、trade-service这三个模块的bootstrap,yaml里配置如下
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.126.151 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- data-id: shared-jdbc.yaml # 共享mybatis配置
- data-id: shared-log.yaml # 共享日志配置
- data-id: shared-swagger.yaml # 共享日志配置
- data-id: shared-seata.yaml # 共享seata配置
application.yaml实例如下
server:
port: 8081
hm:
db:
database: hm-item
swagger:
title: "黑马商城商品管理接口文档"
desc: "黑马商城商品管理接口文档"
package: com.hmall.item.controller
③重新启动CartApplication、TradeApplication、ItemApplication、ItemApplication2
5. XA模式
XA规范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的关系型数据库都对XA规范提供了支持。Seata的XA模式如下:
一阶段的工作:
①RM注册分支事务到TC
②RM执行分支业务sql但不提交
③RM报告执行状态到TC
二阶段的工作:
①TC检测各分支事务执行状态
- 如果都成功,通知所有RM提交事务
- 如果有失败,通知所有RM回滚事务
②RM接收TC指令,提交或回滚事务
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
实现XA模式
Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:
1. 修改application.yaml文件(每个参与事务的微服务),开启XA模式:
seata:
data-source-proxy-mode: XA # 开启数据源代理的XA模式
在shared-seata.yaml里编辑
2. 给发起全局事务的入口方法添加@GlobalTransactional注解,本例中是OrderServiceImpl中的createOrder方法:
@Override
@GlobalTransactional
public Long createOrder(OrderFormDTO orderFormDTO) {
// ... ...
}
CartServiceImpl的removeByItemIds()
@Override
@Transactional
public void removeByItemIds(Collection<Long> itemIds) {
// ... ...
}
ItemServiceImpl里的deductStock()
@Override
@Transactional
public void deductStock(List<OrderDetailDTO> items) {
// ... ...
}
3. 重启服务并测试
6. AT模式
Seata主推的是AT模式,AT模式同样是分阶段提交的事务类型,不过弥补了XA模型中资源锁定周期过长的缺陷。
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源;
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚;
- XA模式强一致;AT模式最终一致
实现AT模式
步骤:
①添加资料中的seata-at.sql到微服务对应的数据库中:hm-cart、hm-item、hm-trade三个数据库
②修改application.yaml文件,将事务模式修改为AT模式:(不填就默认为AT模式)
③其次,我们要利用@GlobalTransactional
标记分布式事务的入口方法:
④重启服务并测试
7. 练习
除了下单业务以外,用户如果选择余额支付,前端会将请求发送到pay-service模块。而这个模块要做三件事情:
- 直接从user-service模块调用接口,扣除余额付款
- 更新本地(pay-service)交易流水表状态
- 通知交易服务(trade-service)更新其中的业务订单状态
显然,这里也存在分布式事务问题。
步骤:
①添加资料中的seata-at.sql到微服务对应的数据库中:hm-pay、hm-trade、hm-user三个数据库
②修改application.yaml文件,将事务模式修改为AT模式(上面已经改过,这里就不演示了)
③在pay-service、trade-service、user-service这三个模块中引入seata的依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
④同时,在pay-service、trade-service、user-service这三个模块的bootstrap,yaml里配置如下
spring:
application:
name: pay-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.126.151 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- data-id: shared-jdbc.yaml # 共享mybatis配置
- data-id: shared-log.yaml # 共享日志配置
- data-id: shared-swagger.yaml # 共享日志配置
- data-id: shared-seata.yaml # 共享seata配置
④利用@GlobalTransactional
标记分布式事务的入口方法:
com.hmall.pay.service.impl.PayOrderServiceImpl
类的tryPayOrderByBalance
@Override
@GlobalTransactional
public void tryPayOrderByBalance(PayOrderFormDTO payOrderFormDTO) {
// 1.查询支付单
PayOrder po = getById(payOrderFormDTO.getId());
// 2.判断状态
if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
// 订单不是未支付,状态异常
throw new BizIllegalException("交易已支付或关闭!");
}
// 3.尝试扣减余额
userClient.deductMoney(payOrderFormDTO.getPw(), po.getAmount());
// 4.修改支付单状态
boolean success = markPayOrderSuccess(payOrderFormDTO.getId(), LocalDateTime.now());
if (!success) {
throw new BizIllegalException("交易已支付或关闭!");
}
// 5.修改订单状态
tradeClient.markOrderPaySuccess(po.getBizOrderNo());
}
trade-service模块下的OrderServiceImpl
user-service模块下的UserServiceImpl
⑤把user表的balance字段的类型改为无符号整型
ALTER TABLE `user` MODIFY `balance` INT UNSIGNED DEFAULT NULL COMMENT '账户余额';
⑥问题:在PayOrder里amount的单位为分,而扣减余额的时候单位按元来算了,或者balance字段的单位也是分???不太清楚
在UserMapper里修改如下进行模拟测试
⑦重启服务进行测试
把Jack用户的余额改为2000,进行测试
ALTER TABLE `user` MODIFY `balance` FLOAT UNSIGNED DEFAULT NULL COMMENT '账户余额';
支付密码:123