Sentinel-流量控制

This commit is contained in:
bunny 2025-05-27 12:14:22 +08:00
parent 71049d5251
commit f589e0751e
26 changed files with 604 additions and 19 deletions

View File

@ -240,7 +240,7 @@ spring:
active: dev
cloud:
nacos:
server-addr: ${NACOS_HOST:192.168.95.135}:8848
server-addr: ${NACOS_HOST:192.168.3.150}:8848
config:
namespace: ${spring.profiles.active:dev} # 动态匹配当前profile
group: DEFAULT_GROUP
@ -425,15 +425,15 @@ public interface BunnyFeignClient {
### 负载均衡对比
| 特性 | 客户端负载均衡 (OpenFeign) | 服务端负载均衡 (Nginx等) |
| ------------ | ----------------------------------- | ------------------------ |
| **实现位置** | 客户端实现 | 服务端实现 |
| **依赖关系** | 需要服务注册中心 | 不依赖注册中心 |
| **性能** | 直接调用,减少网络跳转 | 需要经过代理服务器 |
| **灵活性** | 可定制负载均衡策略 | 配置相对固定 |
| **服务发现** | 集成服务发现机制 | 需要手动维护服务列表 |
| **适用场景** | 微服务内部调用 | 对外暴露API或跨系统调用 |
| **容错能力** | 集成熔断机制如Sentinel、Hystrix | 依赖代理服务器容错配置 |
| 特性 | 客户端负载均衡 (OpenFeign) | 服务端负载均衡 (Nginx等) |
|----------|---------------------------|------------------|
| **实现位置** | 客户端实现 | 服务端实现 |
| **依赖关系** | 需要服务注册中心 | 不依赖注册中心 |
| **性能** | 直接调用,减少网络跳转 | 需要经过代理服务器 |
| **灵活性** | 可定制负载均衡策略 | 配置相对固定 |
| **服务发现** | 集成服务发现机制 | 需要手动维护服务列表 |
| **适用场景** | 微服务内部调用 | 对外暴露API或跨系统调用 |
| **容错能力** | 集成熔断机制如Sentinel、Hystrix | 依赖代理服务器容错配置 |
### 高级配置
@ -578,3 +578,209 @@ public interface ProductFeignClient {
// 方法定义
}
```
## Sentinel 使用指南
> [!NOTE]
> 如果安装完Sentinel打开控制面板可以看到服务但簇点链路为空可能原因
>
> 1. 微服务与Sentinel不在同一IP段
> 2. 服务未发送心跳到Sentinel Dashboard
> 3. 未正确配置`spring.cloud.sentinel.transport.dashboard`
### 依赖引入
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2022.0.0.0</version> <!-- 建议指定版本 -->
</dependency>
```
### 基础配置
```yaml
spring:
cloud:
sentinel:
enabled: true
eager: true # 提前初始化
transport:
dashboard: 192.168.3.150:8858 # Sentinel控制台地址
port: 8719 # 本地启动的HTTP Server端口
client-ip: ${spring.cloud.client.ip-address} # 客户端IP
filter:
enabled: true
web-context-unify: false # 关闭统一上下文(链路模式需要)
```
### 自定义异常处理
#### MVC接口自定义返回
```java
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
String s, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
response.setStatus(429); // 建议使用429 Too Many Requests
Map<String, Object> result = Map.of(
"code", 429,
"message", "请求被限流",
"timestamp", System.currentTimeMillis(),
"rule", e.getRule()
);
objectMapper.writeValue(response.getWriter(), result);
}
}
```
#### REST接口全局异常处理
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BlockException.class)
public ResponseEntity<Object> handleBlockException(BlockException e) {
return ResponseEntity.status(429)
.body(Map.of(
"error", "Too Many Requests",
"rule", e.getRule().toString()
));
}
}
```
### @SentinelResource详解
#### 作用
- 定义资源名称(用于流量控制)
- 指定限流/降级处理逻辑
- 配置异常降级策略
#### blockHandler vs fallback区别
| 特性 | blockHandler | fallback |
| -------- | ------------------------ | ------------------- |
| 触发条件 | 流量控制/熔断时触发 | 业务异常时触发 |
| 参数要求 | 需包含BlockException参数 | 需包含Throwable参数 |
| 优先级 | 更高 | 更低 |
| 典型场景 | 限流/熔断处理 | 业务降级处理 |
#### blockHandler使用示例
```java
@Operation(summary = "创建订单")
@SentinelResource(
value = "orderService",
blockHandler = "createBlockHandler",
fallback = "createFallback"
)
@GetMapping("create")
public Order createOrder(Long userId, Long productId) {
return orderService.createOrder(productId, userId);
}
// 限流处理(参数需匹配原方法且最后加BlockException)
public Order createBlockHandler(Long userId, Long productId, BlockException ex) {
log.warn("触发限流rule={}", ex.getRule());
return Order.fallbackOrder(userId, "限流:" + ex.getClass().getSimpleName());
}
// 降级处理(参数需匹配原方法且最后加Throwable)
public Order createFallback(Long userId, Long productId, Throwable t) {
log.error("业务异常", t);
return Order.fallbackOrder(userId, "降级:" + t.getMessage());
}
```
### 流控规则详解
#### 阈值类型
| 类型 | 说明 | 适用场景 |
| ------ | -------------------------------- | -------------------- |
| QPS | 每秒请求数 | 绝大多数场景推荐使用 |
| 线程数 | 并发线程数(统计服务内部线程数量) | 同步服务/耗时操作 |
> [!WARNING]
> 线程数模式需要统计服务内部线程数量,性能开销较大,非必要不推荐使用
#### 集群模式
| 模式 | 说明 | 示意图 |
| -------- | ---------------------------------- | ------------------------------------------------- |
| 单机均摊 | 总阈值=单机阈值×节点数(均匀分配) | ![单机均摊](./images/image-20250527112921793.png) |
| 总体阈值 | 所有节点共享总阈值(按实际请求分配) | - |
### 流控模式
#### 1. 直接模式
```mermaid
graph LR
流量 --直接--> 资源A
```
- 最简单的模式,直接对资源生效
#### 2. 关联模式
```mermaid
graph LR
流量 --入口A--> 资源A[[不限流]]
流量 --入口B--> 资源B[[限流]]
```
配置要求:
```yaml
spring.cloud.sentinel.web-context-unify: false
```
典型场景:写操作触发时限制读操作
#### 3. 链路模式
```mermaid
graph TD
入口A --> 服务1 --> 资源X
入口B --> 服务2 --> 资源X
```
- 只针对特定入口的调用链路限流
- 需要配合`@SentinelResource`标注资源点
### 流控效果
#### 1. 快速失败
- 直接抛出FlowException
- 支持所有流控模式
- 配置简单,性能最好
#### 2. Warm Up(预热)
![预热模式](./images/image-20250527112649302.png)
- 冷启动阶段逐步提高阈值
- 防止冷系统被突发流量击垮
- 需配置预热时长(秒)
#### 3. 匀速排队
![排队模式](./images/image-20250527120421676.png)
- 以恒定间隔处理请求
- 需配置超时时间(毫秒)
- 不支持QPS>1000的场景

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -22,5 +22,20 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.springdoc</groupId> -->
<!-- <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> -->
<!-- <version>2.8.6</version> -->
<!-- </dependency> -->
<!-- <dependency> -->
<!-- <groupId>io.swagger</groupId> -->
<!-- <artifactId>swagger-annotations</artifactId> -->
<!-- <version>1.6.14</version> -->
<!-- </dependency> -->
</dependencies>
</project>

View File

@ -0,0 +1,90 @@
package cn.bunny.model.common.enums;
import lombok.Getter;
/**
* 统一返回结果状态信息类
*/
@Getter
public enum ResultCodeEnum {
// 成功操作 200
SUCCESS(200, "操作成功"),
CREATE_SUCCESS(200, "添加成功"),
UPDATE_SUCCESS(200, "修改成功"),
DELETE_SUCCESS(200, "删除成功"),
SORT_SUCCESS(200, "排序成功"),
SUCCESS_UPLOAD(200, "上传成功"),
SUCCESS_LOGOUT(200, "退出成功"),
LOGOUT_SUCCESS(200, "退出成功"),
EMAIL_CODE_REFRESH(200, "邮箱验证码已刷新"),
EMAIL_CODE_SEND_SUCCESS(200, "邮箱验证码已发送"),
// 验证错误 201
USERNAME_OR_PASSWORD_NOT_EMPTY(201, "用户名或密码不能为空"),
EMAIL_CODE_NOT_EMPTY(201, "邮箱验证码不能为空"),
SEND_EMAIL_CODE_NOT_EMPTY(201, "请先发送邮箱验证码"),
EMAIL_CODE_NOT_MATCHING(201, "邮箱验证码不匹配"),
LOGIN_ERROR(500, "账号或密码错误"),
LOGIN_ERROR_USERNAME_PASSWORD_NOT_EMPTY(201, "登录信息不能为空"),
GET_BUCKET_EXCEPTION(201, "获取文件信息失败"),
SEND_MAIL_CODE_ERROR(201, "邮件发送失败"),
EMAIL_CODE_EMPTY(201, "邮箱验证码过期或不存在"),
EMAIL_EXIST(201, "邮箱已存在"),
REQUEST_IS_EMPTY(201, "请求数据为空"),
DATA_TOO_LARGE(201, "请求数据为空"),
UPDATE_NEW_PASSWORD_SAME_AS_OLD_PASSWORD(201, "新密码与密码相同"),
// 数据相关 206
ILLEGAL_REQUEST(206, "非法请求"),
REPEAT_SUBMIT(206, "重复提交"),
DATA_ERROR(206, "数据异常"),
EMAIL_USER_TEMPLATE_IS_EMPTY(206, "邮件模板为空"),
EMAIL_TEMPLATE_IS_EMPTY(206, "邮件模板为空"),
EMAIL_USER_IS_EMPTY(206, "关联邮件用户配置为空"),
DATA_EXIST(206, "数据已存在"),
DATA_NOT_EXIST(206, "数据不存在"),
ALREADY_USER_EXCEPTION(206, "用户已存在"),
USER_IS_EMPTY(206, "用户不存在"),
FILE_NOT_EXIST(206, "文件不存在"),
NEW_PASSWORD_SAME_OLD_PASSWORD(206, "新密码不能和旧密码相同"),
MISSING_TEMPLATE_FILES(206, "缺少模板文件"),
// 身份过期 208
LOGIN_AUTH(208, "请先登陆"),
AUTHENTICATION_EXPIRED(208, "身份验证过期"),
SESSION_EXPIRATION(208, "会话过期"),
FAIL_NO_ACCESS_DENIED_USER_LOCKED(208, "该账户已封禁"),
// 209
THE_SAME_USER_HAS_LOGGED_IN(209, "相同用户已登录"),
// 提示错误
UPDATE_ERROR(216, "修改失败"),
URL_ENCODE_ERROR(216, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR(217, "非法回调请求"),
FETCH_USERINFO_ERROR(219, "获取用户信息失败"),
ILLEGAL_DATA_REQUEST(219, "非法数据请求"),
CLASS_NOT_FOUND(219, "类名不存在"),
ADMIN_ROLE_CAN_NOT_DELETED(219, "无法删除admin角色"),
ROUTER_RANK_NEED_LARGER_THAN_THE_PARENT(219, "设置路由等级需要大于或等于父级的路由等级"),
// 无权访问 403
FAIL_NO_ACCESS_DENIED(403, "无权访问"),
FAIL_NO_ACCESS_DENIED_USER_OFFLINE(403, "用户强制下线"),
TOKEN_PARSING_FAILED(403, "token解析失败"),
// 系统错误 500
UNKNOWN_EXCEPTION(500, "服务异常"),
SERVICE_ERROR(500, "服务异常"),
UPLOAD_ERROR(500, "上传失败"),
FAIL(500, "失败"),
;
private final Integer code;
private final String message;
ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}

View File

@ -0,0 +1,34 @@
package cn.bunny.model.common.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Schema(name = "PageResult 对象", title = "分页返回结果", description = "分页返回结果")
public class PageResult<T> implements Serializable {
@Schema(name = "pageNo", title = "当前页")
private Long pageNo;
@Schema(name = "pageSize", title = "每页记录数")
private Long pageSize;
@Schema(name = "total", title = "总记录数")
private Long total;
@Schema(name = "list", title = "当前页数据集合")
private List<T> list;
}

View File

@ -0,0 +1,174 @@
package cn.bunny.model.common.result;
import cn.bunny.model.common.enums.ResultCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
// 状态码
private Integer code;
// 返回消息
private String message;
// 返回数据
private T data;
/**
* * 自定义返回体
*
* @param data 返回体
* @return Result<T>
*/
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<>();
result.setData(data);
return result;
}
/**
* * 自定义返回体使用ResultCodeEnum构建
*
* @param body 返回体
* @param codeEnum 返回状态码
* @return Result<T>
*/
public static <T> Result<T> build(T body, ResultCodeEnum codeEnum) {
Result<T> result = build(body);
result.setCode(codeEnum.getCode());
result.setMessage(codeEnum.getMessage());
return result;
}
/**
* * 自定义返回体
*
* @param body 返回体
* @param code 返回状态码
* @param message 返回消息
* @return Result<T>
*/
public static <T> Result<T> build(T body, Integer code, String message) {
Result<T> result = build(body);
result.setCode(code);
result.setMessage(message);
result.setData(null);
return result;
}
/**
* * 操作成功
*
* @return Result<T>
*/
public static <T> Result<T> success() {
return success(null, ResultCodeEnum.SUCCESS);
}
/**
* * 操作成功
*
* @param data baseCategory1List
*/
public static <T> Result<T> success(T data) {
return build(data, ResultCodeEnum.SUCCESS);
}
/**
* * 操作成功-状态码
*
* @param codeEnum 状态码
*/
public static <T> Result<T> success(ResultCodeEnum codeEnum) {
return success(null, codeEnum);
}
/**
* * 操作成功-自定义返回数据和状态码
*
* @param data 返回体
* @param codeEnum 状态码
*/
public static <T> Result<T> success(T data, ResultCodeEnum codeEnum) {
return build(data, codeEnum);
}
/**
* * 操作失败-自定义返回数据和状态码
*
* @param data 返回体
* @param message 错误信息
*/
public static <T> Result<T> success(T data, String message) {
return build(data, 200, message);
}
/**
* * 操作失败-自定义返回数据和状态码
*
* @param data 返回体
* @param code 状态码
* @param message 错误信息
*/
public static <T> Result<T> success(T data, Integer code, String message) {
return build(data, code, message);
}
/**
* * 操作失败
*/
public static <T> Result<T> error() {
return Result.build(null);
}
/**
* * 操作失败-自定义返回数据
*
* @param data 返回体
*/
public static <T> Result<T> error(T data) {
return build(data, ResultCodeEnum.FAIL);
}
/**
* * 操作失败-状态码
*
* @param codeEnum 状态码
*/
public static <T> Result<T> error(ResultCodeEnum codeEnum) {
return build(null, codeEnum);
}
/**
* * 操作失败-自定义返回数据和状态码
*
* @param data 返回体
* @param codeEnum 状态码
*/
public static <T> Result<T> error(T data, ResultCodeEnum codeEnum) {
return build(data, codeEnum);
}
/**
* * 操作失败-自定义返回数据和状态码
*
* @param data 返回体
* @param code 状态码
* @param message 错误信息
*/
public static <T> Result<T> error(T data, Integer code, String message) {
return build(data, code, message);
}
/**
* * 操作失败-自定义返回数据和状态码
*
* @param data 返回体
* @param message 错误信息
*/
public static <T> Result<T> error(T data, String message) {
return build(null, 500, message);
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -68,6 +68,10 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>

View File

@ -18,9 +18,15 @@
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.springdoc</groupId> -->
<!-- <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> -->
<!-- <version>2.8.6</version> -->
<!-- </dependency> -->
<!-- <dependency> -->
<!-- <groupId>io.swagger</groupId> -->
<!-- <artifactId>swagger-annotations</artifactId> -->
<!-- <version>1.6.14</version> -->
<!-- </dependency> -->
</dependencies>
</project>

View File

@ -3,12 +3,16 @@ package cn.bunny.service.controller;
import cn.bunny.model.order.bean.Order;
import cn.bunny.service.config.OrderProperties;
import cn.bunny.service.service.OrderService;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/order")
@RequiredArgsConstructor
@ -25,18 +29,28 @@ public class OrderController {
private final OrderProperties orderProperties;
@Operation(summary = "创建订单")
@SentinelResource(value = "order", blockHandler = "createBlockHandler")
@GetMapping("create")
public Order createOrder(Long userId, Long productId) {
return orderService.createOrder(productId, userId);
}
public Order createBlockHandler(Long userId, Long productId, BlockException exception) {
Order order = new Order();
order.setUserId(userId);
order.setAddress("xxx");
order.setNickName(exception.getMessage());
order.setProductList(List.of());
return order;
}
@Operation(summary = "读取配置")
@GetMapping("config")
public String config() {
String timeout = orderProperties.getTimeout();
String autoConfirm = orderProperties.getAutoConfirm();
String dbUrl = orderProperties.getDbUrl();
return "timeout" + timeout + "\nautoConfirm" + autoConfirm + "\norder.db-url" + dbUrl;
}
}

View File

@ -0,0 +1,26 @@
package cn.bunny.service.exception;
import cn.bunny.model.common.result.Result;
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.PrintWriter;
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception {
PrintWriter writer = httpServletResponse.getWriter();
httpServletResponse.setContentType("application/json;charset=utf-8");
Result<String> result = new Result<>(500, "限制访问", null);
String json = objectMapper.writeValueAsString(result);
writer.write(json);
}
}

View File

@ -16,6 +16,14 @@ spring:
read-timeout: 5000 # 最多等待对方 5s
request-interceptors:
- cn.bunny.service.interceptor.XTokenRequestInterceptor
sentinel:
enabled: true
eager: true # 提前加载
transport:
dashboard: 192.168.3.150:8858
filter:
enabled: true
# web-context-unify: false # 关闭统一上下文
feign:
sentinel:
enabled: true
enabled: true

View File

@ -10,7 +10,7 @@ spring:
- feign
cloud:
nacos:
server-addr: 192.168.95.135:8848
server-addr: 192.168.3.150:8848
config:
import-check:
enabled: false

View File

@ -2,4 +2,4 @@ server:
port: 8001
nacos:
server-addr: 192.168.95.135:8848
server-addr: 192.168.3.150:8848

View File

@ -12,3 +12,11 @@ spring:
config:
import-check:
enabled: false
sentinel:
enabled: true
eager: true # 提前加载
transport:
dashboard: 192.168.3.150:8858
filter:
enabled: true

View File

@ -26,7 +26,7 @@ sudo systemctl daemon-reload && sudo systemctl restart docker
#### MySQL配置问题
| **特性** | `**my.cnf**` | `**conf.d**` **目录** |
| **特性** | my.cnf | conf.d **目录** |
| ------------ | :--------------------------- | :------------------------: |
| **文件类型** | 单个文件 | 目录,包含多个 `.cnf` 文件 |
| **配置方式** | 集中式配置 | 分布式配置 |