
SpringCloud 服务保护 & 分布式事务
书接上回:SpringCloud 微服务
一、服务保护
1.1 雪崩问题
微服务调用链路中的某个服务故障,导致整个链路中的所有微服务都不可用。

如图所示:购物车服务调用商品服务,若商品服务出现了宕机或者处理速度太慢(单个请求要好几秒),就会出现商品服务上级资源得不到释放,使得上级的 Tomcat 线程资源被耗尽。在多级服务相互调用的情况下,这种阻塞的问题会不断向上传递,进而产生雪崩问题:

1.1.1 问题分析
① 雪崩问题产生的原因是什么?
- 微服务相互调用,服务提供者(下级微服务)出现故障或阻塞;
- 服务调用者没有做好异常处理,导致自身故障(如例子中的购物车服务及其上层服务);
- 调用链路中的所有服务级联失败,导致整个集群故障。
② 解决问题的思路有哪些?
- 尽量避免服务出现故障或阻塞。
- 保证代码的健壮性;
- 保证网络通畅;
- 能应对较高的并发请求;
- 服务调用者做好远程调用异常的后备方案,避免故障扩散。
1.1.2 解决方案
① 请求限流:限制访问微服务请求的并发量,避免服务因流量激增出现故障。

这种方式也称为流量整形:

② 线程隔离:通过限定每个业务能使用的线程数量而将故障业务隔离。

③ 失败处理:给业务编写一个调用失败时的处理的逻辑,称为fallback。当调用出现故障(比如无线程可用)时,按照fallback处理业务,而不是直接抛出异常

④ 服务熔断:由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,拦截该接口的请求。熔断期间,所有请求快速失败,全都走fallback逻辑。
例如一个广告业务,如果查询广告失败,可以返回一个默认的广告

1.1.3 服务保护技术

1.2 Sentinel
Sentinel是阿里巴巴开源的一款面向分布式、多语言异构化服务架构的流量治理组件。官网地址: home | Sentinel
1.2.1 运行 Sentinel
在 Github 上下载到 jar 包,用以下命令执行:
java -Dserver.port=8090 -Dscp.sentienl.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar更多启动参数 - 参考官方文档:startup-configuration
启动后访问网址:http://localhost:8090/,默认的用户名和密码都是sentinel
1.2.2 微服务整合依赖
① 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>② 添加配置
# application.yml (cart-service)
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # sentinel 控制台地址③ 重启 cart-service
但是此时 sentinel dashboard 里并不会显示 cart-service,因为它做的是流量监控,所以我们访问购物车几次,就可以在控制台看到了。

1.2.3 簇点链路
簇点链路,就是单机调用链路。是一次请求进入服务器后经过的每一个被sentinel监控的资源链。默认sentinel会监控springMVC的每一个Endpoint(http接口)。限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径:

简单来说,簇点链路(Cluster Node Link)就是在单台应用实例上,对一次入站请求从进入到离开的全过程中,所有被 Sentinel 监控的“资源”节点的调用顺序的汇总。它帮助我们在单机层面实时查看该请求走过了哪些接口或方法,以及每个节点的统计数据(如 QPS、响应时间、阻塞或异常数等)。例如:
HTTP GET /api/order -> OrderController.createOrder() ->
InventoryService.reserve() -> PaymentService.process()不过对于 Restful 风格的接口,这种 Endpoint 的监控方式是有问题的,因为不同的接口路径都是 /carts,只有请求方式不一样,例如:
package com.hmall.cart.controller;
@RestController
@RequestMapping("/carts")
@RequiredArgsConstructor
public class CartController {
private final ICartService cartService;
@PostMapping
public void addItem2Cart(@Valid @RequestBody CartFormDTO cartFormDTO) {
cartService.addItem2Cart(cartFormDTO);
}
@PutMapping
public void updateCart(@RequestBody Cart cart) {
cartService.updateById(cart);
}
...
}
Restful 风格的 API 请求路径一般都相同,这会导致簇点资源名称重复。因此我们要修改配置,把请求方式+请求路径作为簇点资源名称。
# application.yml (cart-service)
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # sentinel 控制台地址
http-method-specify: true # <--- 使簇点资源区分请求方式重启 cart-service,重新查看控制台:

1.2.4 请求限流
每一个被访问的接口都会在簇点链路显示,找到需要限流的接口,按【➕流控】
限流的规则中,有一个阈值类型:
- QPS:控制单位时间内允许进入系统的请求数,主要用于防止流量洪峰。
- 并发线程数:系统中同时处理的请求数量,受 QPS 和平均响应时间的共同影响。

点击新增之后,可以在这里找到和编辑新增的规则:

验证配置结果:

HTTP 响应状态码:429(flow limiting)表示被限流
Sentinel Dashboard:

1.2.5 线程隔离
当商品服务出现阻塞或者故障时,调用商品服务的购物车服务可能因此被拖慢,甚至资源耗尽。所以必须限制购物车服务中查询商品服务的可用线程数,实现线程隔离。
为什么需要线程隔离?
即便对 QPS 做了限制,只要每个请求的平均响应时间(latency)较长,系统中的并发量依然会很高——因为并发量近似等于 QPS 与平均响应时间的乘积:
concurrency ≈ QPS × 平均响应时间
这意味着,QPS 限制只能控制“到达速率”;如果处理慢或阻塞,允许流入的请求都会在系统中累积,形成高并发。
① Sentinel 控制台配置线程隔离

如果这个接口的处理能力是 500ms/个,那么 5 个线程的处理能力就是 10 QPS
这里的规则先不急着新增,第 ③ 步再新增
② 修改商品服务,模拟延时
package com.hmall.item.controller;
@RestController
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {
private final IItemService itemService;
@ApiOperation("根据id批量查询商品")
@GetMapping
public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids) {
// 模拟业务延迟 500ms
ThreadUtil.sleep(500);
return itemService.queryItemByIds(ids);
}
...
}重启 item-service
Spring boot 集成的 Tomcat 默认最大连接数高达8192:

为了使用 Jmeter 进行测试,需要对 Tomcat 线程数和最大连接数进行限制(因为我们单机 Jmeter 无法达到这么高的并发量)。
server:
port: 8082
tomcat:
threads:
max: 50
# accept-count:允许排队等待的连接数,50个用户可立即建立连接,50个用户等待连接释放,
# 其余用户建立不了连接直接失败
accept-count: 50
max-connections: 100重启 cart-service,测试查询购物车的请求速度500ms+:

启动 Jmeter 测试:

再次去购物车进行增减数量操作:

发现商品查询操作>1s,增减数量操作由十几ms变为500+ms,明显被拖慢。(下面的请求是恢复正常的)。
③ 利用线程隔离:防止购物车查询影响购物车的别的业务

④ 再次用 Jmeter 开启压力测试,参数与上面相同,继续测试:

现在只有查询购物车的接口有问题了(被流控了),但是商品的增减是正常的(增减按钮被遮住了)。后端返回 429 Too Many Requests。
1.2.6 Fallback
① 定义 FallbackFactory
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 lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
/*log.error("item-service服务调用失败", cause);*/
return new ItemClient() {
@Override
public List<ItemDTO> queryItemsByIds(Collection<Long> ids) {
log.error("查询商品失败!", cause);
return Collections.emptyList();
}
@Override
public void deductStock(List<OrderDetailDTO> items) {
log.error("扣减库存失败!", cause);
throw new RuntimeException(cause);
}
};
}
}
② 将其注册为 Bean
package com.hmall.api.config;
import com.hmall.api.client.fallback.ItemClientFallbackFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FallbackConfig {
@Bean
public ItemClientFallbackFactory itemClientFallbackFactory() {
return new ItemClientFallbackFactory();
}
}
③ 给 FeignClient 的注解添加新属性
package com.hmall.api.client;
import com.hmall.api.client.fallback.ItemClientFallbackFactory;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.List;
// 旧的注解配置
//@FeignClient("item-service")
// 新的注解配置
@FeignClient(value = "item-service",
fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemsByIds(@RequestParam("ids") Collection<Long> ids);
@PutMapping("/items/stock/deduct")
void deductStock(@RequestBody List<OrderDetailDTO> items);
}
④ 修改 cart-service 的 application.yml
这里是把 Feign 的远程调用作为簇点资源,这是比较推荐的 Sentinel 监控/限流方式,而非监控/限制整个 /carts 请求路径
feign:
okhttp:
enabled: true
sentinel:
# If true, an OpenFeign client will be wrapped with a Sentinel circuit breaker.
enabled: true⑤ config文件,cart-service默认扫描不到新加的 Configuration,因为和启动类不在同一个包下面,所以要做一下配置:
package com.hmall.cart;
import com.hmall.api.config.DefaultFeignConfig;
import com.hmall.api.config.FallbackConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients(
basePackages = "com.hmall.api.client",
/*defaultConfiguration = DefaultFeignConfig.class) 这一行是旧配置 */
defaultConfiguration = {DefaultFeignConfig.class, FallbackConfig .class})
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}⑥ 添加流控规则

⑦ 开启 Jmeter 压力测试,不再直接失败,而是走 fallback 逻辑,只是查不到最新的商品信息:

ERROR 40596 --- [8082-exec-48] c.h.a.c.f.ItemClientFallbackFactory: 查询商品失败![
{
"id": "7",
"itemId": "100000006163",
"num": 1,
"name": "巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)",
"spec": "{}",
"price": 67100,
"newPrice": null, // <---- 调用 item-service 但是失败了,所以为 null
"status": 1,
"stock": 10,
"image": "....",
"createTime": "2023-05-20T21:07:09"
}
]1.2.7 服务熔断
以一个 Feign 远程调用为例,服务熔断是指当请求数超过流控规则的限制,就会熔断远程调用服务,即不再走远程调用,而是直接走 Fallback 逻辑。
熔断是解决雪崩问题的重要手段。思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求(直接走fallback);而当服务恢复时,断路器会放行访问该服务的请求。

Sentinel 配置如下:

注:熔断状态(Open)下,并不是直接失败,而是走 fallback 逻辑
二、分布式事务
