上面讲的是A--Feign/Dubbo->B服务时候,对B的做熔断、限流,可如果我只想对A本身做限流怎么实现?
1.4.1 问题描述
下边我们对/carts接口进行限流测试,找到GET/carts簇点
点击“流控” 设置QPS 单机阈值为6
截止目前,我们一共设置了两个流控规则
用jmeter测试,通过sentinel进行实时监控,通过QPS为6,拒绝QPS为4,符合我们的预期结果
当/carts接口被限流时我们访问此接口(需要在Jmeter压测期间测试),需要连续多点击几次
结果内容:Blocked by Sentinel (flow limiting),从字面内容看是被sentinel限流。
为什么访问item-service的商品查询接口走了降级方法,而访问/carts没有走降级方法呢?
1.4.2 sentinel实现降级
这里我们需要重新梳理下:
前边cart-service远程调用item-service服务的商品查询接口正常走降级方法,这是因为我们编写了ItemClient接口的降级类ItemClientFallbackFactory,并且我们在cart-service服务中集成了sentinel,在cart-service的application.yml配置文件中配置了feign使用sentinel,如下:
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
也就说,在cart-service通过openfeign远程调用item-service时通过sentinel进行降级,最终执行ItemClientFallbackFactory类中指定的降级方法。这里我们是对/carts进行限流,并没有针对此接口编写降级方法。
AI:sentinel实现降级
针对feign远程调用我们通过实现FallbackFactory接口去编写远程调用接口对应的降级逻辑,而针对非feign远程调用的降级逻辑我们需要使用@SentinelResource注解去实现
首先使用 @SentinelResource 定义资源,编写降级方法
@ApiOperation("查询购物车列表")
@SentinelResource(value = "queryMyCarts", fallback = "queryMyCartsFallback", blockHandler = "queryMyCartsBlockHandler")
@GetMapping
public List queryMyCarts(){
return cartService.queryMyCarts();
}
//当发生非限流非熔断异常走此方法
public List queryMyCartsFallback(Throwable throwable){
log.error("非限流、非熔断异常执行的降级方法,throwable:", throwable);
return new ArrayList<>();
}
//当发生熔断、限流走此方法
public List queryMyCartsBlockHandler( BlockException blockException){
log.error("触发限流、熔断时执行的降级方法,blockException:", blockException);
return new ArrayList<>();
}
然后在sentinel中设置资源的流控
删除针对/carts设置的流控规则
再次使用jmeter进行压力测试
观察cart-service控制台,当/carts限流后正常执行降级方法,日志如下:
18:37:31:767 ERROR 4696 --- [nio-8082-exec-9] c.hmall.cart.controller.CartController : 触发限流、熔断时执行的降级方法,blockException:
在降级方法中可以返回特殊的信息,或指定特殊的状态码,根据特殊值前端可展示为类似“网络忙请稍后重试” 这样的信息。此时限流期间访问购物车接口,发现就走了降级逻辑,返回空集合:
1.4.3 小节
sentienl降级怎么实现?
对于Feign远程调用对每个远程调用接口实现降级方法,通过实现FallbackFactory接口实现降级方法。
对于非Feign远程调用我们使用@SentinelResource注解编写自定义降级方法。
1.5. 线程隔离
1.5.1 方案介绍
首先我们来看下线程隔离功能,无论是Hystix还是Sentinel都支持线程隔离。不过其实现方式不同。
线程隔离有两种方式实现:
● 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
● 信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求
如图:
Sentinel的线程隔离就是基于信号量隔离实现的,而Hystix两种都支持,但默认是基于线程池隔离。
1.5.2 Sentinel线程隔离(练习)
下边我们使用Sentinel实现基于信号量的线程隔离。
点击查询商品的FeignClient对应的簇点资源后面的流控按钮:
在弹出的表单中填写下面内容:
注意,这里勾选的是并发线程数限制,也就是说这个查询功能最多使用5个线程,而不是5QPS。如果查询商品的接口每秒处理2个请求,则5个线程的实际QPS在10左右,而超出的请求自然会被拒绝。
cart-service调用商品查询控制在5个线程内,通过线程隔离即使商品服务出现问题也不会影响cart-service服务。
下边修改商品查询接口的代码,添加休眠500毫秒的代码,模拟一次请求需要500毫秒,一秒则可处理2次请求,5个线程可接收10 QPS左右。
将/carts的限流规则调大以免影响商品查询接口的限流控制。
下边使用jmeter进行测试,更改线程数为10000,时间为100秒,每秒需要发送100个请求。
启用测试,通过sentinel实时监控,符合我们的预期。
不知道细心的你是否发现了一个事情:我们设置的Sentinel通过数量,为什么不那么精准呢?
● 窗口边界:一个请求可能刚好跨越两个窗口,导致单个窗口的统计值略高于设定值。
● 滑动窗口精度:滑动窗口的划分粒度可能无法完全精确,尤其在请求密集时。
1.6 面试题
你们是怎么做微服务保护的?
Sentinel实现熔断、限流的底层原理是什么?(在面试篇进行详细讲解)
2 分布式事务
2.1. 什么是分布式事务
2.1.1 概念
首先我们看看项目中的下单业务整体流程:
交互流程如下:
用户创建订单,客户端请求交易服务创建订单
创建订单成功,交易服务请求购物车服务清理购物车,请求库存服务扣减库存
由于订单、购物车、商品分别是三个不同的微服务,而每个微服务都有自己独立的数据库,一次下单事务需要订单、购物车、商品服务分别执行自己的本地事务,是跨多个数据库完成这次下单的事务,像这种,在分布式系统环境下由多个服务通过网络通信协作去完成一次事务,这称之为分布式事务。可简单理解为一个分布式事务等于多个本地事务。
2.1.2 测试分布式事务
分布式事务是无法通过单个数据库事务去控制的,每个微服务都有自己的数据库,一次下单事务需要订单、购物车、商品服务分别执行自己的本地事务,其中一个执行失败其它本地事务是无法回滚的,比如:扣减库存失败无法回滚清理购物车及创建订单的事务。
下边我们测试下单扣减库存,首先部署交易微服务,交易微服务是第一天布置的作业,这里有几点需要注意:
1.需要在ItemClient接口中增加扣减库存的方法,以供交易服务远程调用,还需要在ItemClientFallbackFactory增加扣减库存的降级逻辑,即现在已有的代码【已实现】
@PutMapping("/stock/deduct")
public void deductStock(@RequestBody List items);
2.商品服务中的OrderDetailDTO移到了hm-api工程,凡是引用该类的微服务都需要统一引用hm-api下的OrderDetailDTO【已实现】
3.交易服务启动类 trade-service 注意添加扫描hm-api下的feign接口
@MapperScan("com.hmall.trade.mapper")
@EnableFeignClients(basePackages = {"com.hmall.api"})
@SpringBootApplication(scanBasePackages = {"com.hmall.trade","com.hmall.api"})
4.暂时将“UserContext.getUser()”获取用户id代码固定为“1”。
我们在下单方法代码的最后位置制造异常,如下【注意要有下面的:@Transactional】:
@Override
@Transactional
public Long createOrder(OrderFormDTO orderFormDTO) {
....
if(1==1){
throw new RuntimeException("测试异常");
}
return order.getId();
}
预期结果是:当扣减库存成功,下单失败,最终扣减库存事务进行回滚。
此时商品库存我们截个图
下边启动:item-service、trade-service,打开交易服务的swagger文档找到下单接口进行测试:
从商品数据库找一个商品id填入请求参数
最终抛出异常,查看商品的库存正常扣除,但是订单数据没有创建成功,最终导致数据不一致。
测试结论:
通过本地事务控制注解 @Transactional是无法控制分布式事务的。
2.1.3 认识分布式事务
下边为了简化分析过程 我们仍然以下单扣减库存为例说明:
在单体架构下实现下单减库存,如下图:
用户请求订单服务,订单服务请求数据库完成创建订单扣减库存,通过本地事务实现,代码如下:
begin transaction;
//1.本地数据库操作:创建订单
//2.本地数据库操作:减去库存
commit transation;
如果是在微服务架构下,如下图:
用户请求订单服务下单,订单服务请求库存服务扣减库存。
此时代码变为下边这样:
begin transaction;
//1.本地数据库操作:创建订单
//2.远程调用:减去库存
commit transation;
设想: 当远程调用扣减库存成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了创建订单的操作,此时订单没有创建成功而库存却扣减了,最终就导致了下单扣减库存整个事务的数据不一致。
因此在分布式架构下,基于数据库的事务控制无法满足要求,下单操作是一次本地事务,扣减库存是一次本地事务,两次本地事务组成一个完整的事务即下单扣减库存,数据库的本地事务只能控制一次本地事务即下单操作控制下单的本地事务,扣减库存操作控制扣减库存的本地事务,无法保证下单和扣减库存整体事务的原子性和一致性。
造成分布式事务无法控制的根本原因是不同业务的数据通常不在一个数据库中或者不在一个系统中,一次事务需要由多个服务或多个系统远程调用协作完成,远程协作依赖网络,由于网络问题会导致整体事务不能正常完成。
分布式事务的典型场景是:业务的数据分布在多个数据库,一次事务操作需要跨多个数据库去完成,
需要由多个服务远程调用协作去完成,远程调用依赖网络,由于网络问题会导致整体事务不能正常完成。
如下图所示:
还有非典型的分布式事务场景也需要了解下。
1)单服务请求多数据库完成一次事务
下图中虽然没有跨服务远程调用但一次事务请求两个不同的数据库也属于分布式事务的场景,创建订单会和订单数据库创建连接通过一次本地事务提交数据,减库存会和商品数据库创建连接通过一次本地事务提交数据,因为下单扣减库存是通过两个数据库连接完成,仍然是多次本地事务共同完成一个完整的事务。
2)多服务请求单数据库完成一次事务
下图中虽然用的一个数据库但是通过跨服务远程调用去完成一次事务,也属于分布式事务的场景。
思考下这种场景为什么也属于分布式事务?
2.1.4 小结
什么是本地事务?
基于应用自己的关系型数据库的事务称为本地事务,在service方法通过添加@Transactional注解进行本地事务控制。
什么是分布式事务?
在分布式系统环境下由多个服务通过网络通信协作去完成一次事务,这称之为分布式事务。
分布式事务的场景有哪些?
多个微服务之间通过远程调用完成一次分布式事务,即:跨服务完成一次事务
单服务请求多数据库完成一次事务,即:跨数据源完成一次事务
多服务请求单数据库完成一次事务,即:跨服务完成一次事务