实现方案

1.基于guava 限流实现(单机版)

guava 为谷歌开源的一个比较实用的组件,利用这个组件可以帮助开发人员完成常规的限流操作,接下来看具体的实现步骤

1.1 依赖引入

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>

1.2 自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.zhen.studytotal.limitRequest.annotation;

import java.lang.annotation.*;

/**
* Description: 自定义限流注解-基于gvava
* User: zhen0w0
* Date: 2025/3/14
* Time: 00:32
*/
@Documented
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface GuavaLimitRateAnnotation {

//限制类型
String limitType();

//每秒五个请求
double limitCount() default 5d;
}

1.3 guava工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.zhen.studytotal.limitRequest.aspect;

import com.google.common.util.concurrent.RateLimiter;

import java.util.HashMap;
import java.util.Map;

/**
* @description 创建RateLimiter
* @author tcm
* @version 1.0.0
* @date 2021/10/12 17:55
**/
public class RateLimiterHelper {

private RateLimiterHelper(){}

//根据mapkey,对不同注解类型限流
private static Map<String, RateLimiter> rateMap = new HashMap<>();

public static RateLimiter getRateLimiter(String limitType, double limitCount ){
RateLimiter rateLimiter = rateMap.get(limitType);
if(rateLimiter == null){
rateLimiter = RateLimiter.create(limitCount);
rateMap.put(limitType,rateLimiter);
}
return rateLimiter;
}

}

1.4 AOP切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.zhen.studytotal.limitRequest.aspect;


import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.util.JSONPObject;
import com.google.common.util.concurrent.RateLimiter;
import com.zhen.studytotal.limitRequest.annotation.GuavaLimitRateAnnotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;


import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Objects;

/**
* Description: 接口限流切面类
* User: zhen0w0
* Date: 2025/3/15
* Time: 02:20
*/
@Aspect
@Component
public class GuavaLimitRateAspect {

private static Logger logger = LoggerFactory.getLogger(GuavaLimitRateAspect.class);

@Before("execution(@com.zhen.studytotal.limitRequest.annotation.GuavaLimitRateAnnotation * *(..))")
public void limit(JoinPoint joinPoint){
//1.获取当前方法
Method currentMethod = getCurrentMethod(joinPoint);
if(Objects.isNull(currentMethod)){
return;
}
//2.获取注解参数
String limitType = currentMethod.getAnnotation(GuavaLimitRateAnnotation.class).limitType();
double limitCount = currentMethod.getAnnotation(GuavaLimitRateAnnotation.class).limitCount();
//3.使用guava令牌桶算法获取令牌,获取不到先等待
RateLimiter rateLimiter = RateLimiterHelper.getRateLimiter(limitType, limitCount);
boolean b = rateLimiter.tryAcquire();
if (b){
logger.info("获取令牌成功");
} else {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
//这里使用的json第三方依赖为(fastjson2)
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", false);
jsonObject.put("message", "请求频繁,限流中···");
try {
output(response, jsonObject.toJSONString());
}catch (Exception e){
logger.error("限流异常:{}", e);
}

}

}

private void output(HttpServletResponse response, String jsonString) throws IOException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
outputStream.write(jsonString.getBytes("UTF-8"));
}catch (IOException e){
e.printStackTrace();
}finally {
assert outputStream != null;
outputStream.flush();
outputStream.close();
}
}

private Method getCurrentMethod(JoinPoint joinPoint) {
Method[] methods = joinPoint.getTarget().getClass().getMethods();
Method target = null;
for (Method method : methods) {
if (method.getName().equals(joinPoint.getSignature().getName())) {
target = method;
break;
}
}
return target;
}


}

1.5 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.zhen.studytotal.limitRequest.controller;

import com.zhen.studytotal.limitRequest.annotation.GuavaLimitRateAnnotation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Description: 限流测试
* User: zhen0w0
* Date: 2025/3/16
* Time: 02:54
*/
@RestController
@RequestMapping("/test/guava")
public class TestGuavaController {

@GetMapping("/limit")
@GuavaLimitRateAnnotation(limitType = "测试guava限流", limitCount = 1)
public String test(){
return "limit";
}
}

1.6 效果

这里为了清楚看到效果,限流QPS设置为1,多刷新几次请求即可

image-20250316233852767

2. 基于 redis+lua 限流实现(分布式版)

redis是线程安全的,天然具有线程安全的特性,支持原子性操作,限流服务不仅需要承接超高QPS,还要保证限流逻辑的执行层面具备线程安全的特性,利用Redis这些特性做限流,既能保证线程安全,也能保证性能

image-20250317000152044

结合流程图可以得出以下实现思路:

1.编写 lua 脚本,指定入参的限流规则,比如对特定的接口限流时,可以根据某个或几个参数进行判定,调用该接口的请求,在一定的时间窗口内监控请求次数;
2.既然是限流,最好能够通用,可将限流规则应用到任何接口上,那么最合适的方式就是通过自定义注解形式切入;
3.提供一个配置类,被 spring 的容器管理,redisTemplate 中提供了 DefaultRedisScript这个 bean;
4.提供一个能动态解析接口参数的类,根据接口参数进行规则匹配后触发限流;

2.1 依赖

YML文件记得添加redis相关配置

1
2
3
4
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.zhen.studytotal.limitRequest.annotation;

import com.zhen.studytotal.limitRequest.enums.LimitTypeEnum;
import org.apache.commons.lang3.StringUtils;

import java.lang.annotation.*;

/**
* Description: redis限流注解
* User: zhen0w0
* Date: 2025/3/17
* Time: 00:11
*/
@Documented
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RedisLimitAnnotation {

/**
* redis-key
*/
String key() default "";

/**
* redis-key前缀
*/
String prefix() default "";

/**
* 限流时间内限流次数
*/
int count();

/**
* 限流时间-单位秒
*/
int time();

/**
* 限流类型
*/
LimitTypeEnum limitType() default LimitTypeEnum.INTERFACE;
}

2.3 限流类型枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.zhen.studytotal.limitRequest.enums;

import lombok.Getter;

/**
* Description:
* User: zhen0w0
* Date: 2025/3/17
* Time: 00:35
*/
@Getter
public enum LimitTypeEnum {

// 默认限流策略,针对某一个接口进行限流
INTERFACE
,

// 根据IP地址进行限流
IP
,

// 自定义的Key
CUSTOMER
;
}

2.4 IP工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.zhen.studytotal.limitRequest.utils;

import javax.servlet.http.HttpServletRequest;
import java.net.HttpCookie;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
* Description: iputils
* User: zhen0w0
* Date: 2025/3/17
* Time: 23:08
*/
public class IPUtils {

public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}

// 本机访问
if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){
// 根据网卡取本机配置的IP
InetAddress inet;
try {
inet = InetAddress.getLocalHost();
ip = inet.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
//"***.***.***.***".length() = 15
if (null != ip && ip.length() > 15) {
if (ip.indexOf(",") > 15) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}

}

2.5 自定义 lua 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 定义变量:redis中key值、规定的时间段内访问次数、redis中过期时间、当前访问次数

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then
return 0
end
-- 没有超阈值,将当前访问数量+1,
current = redis.call("INCRBY", key, "1")
if tonumber(current) == 1 then
-- 设置过期时间
redis.call("expire", key, count)
end
return current

2.6 Redis配置类

设置执行lua脚本,这里注意lua脚本返回类型,我这里原来采用number类型接收返回,报错

io.lettuce.core.output.ValueOutput does not support set(long),改用Long类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.zhen.studytotal.limitRequest.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;

/**
* Description: redis配置
* User: zhen0w0
* Date: 2025/3/17
* Time: 00:47
*/
@Configuration
public class RedisConfiguration {

@Bean
public DefaultRedisScript<Long> redisLuaScript(){
DefaultRedisScript<Long> numberDefaultRedisScript = new DefaultRedisScript<>();
numberDefaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua\\limit.lua")));
//设置lua脚本返回值类型,需要和lua脚本的返回值类型一致
numberDefaultRedisScript.setResultType(Long.class);
return numberDefaultRedisScript;
}

@Bean("redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate= new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//设置value的序列化方式为json
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置key的序列化方式为string
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

2.7 Redis切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.zhen.studytotal.limitRequest.aspect;

import com.zhen.studytotal.limitRequest.annotation.RedisLimitAnnotation;
import com.zhen.studytotal.limitRequest.enums.LimitTypeEnum;
import com.zhen.studytotal.limitRequest.utils.IPUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Objects;

/**
* Description: reids限流切面
* User: zhen0w0
* Date: 2025/3/17
* Time: 01:05
*/
@Aspect
@Component
public class RedisLimitAspect {
public static final Logger logger = LoggerFactory.getLogger(RedisLimitAspect.class);

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private DefaultRedisScript<Long> redisLuaScript;

@Pointcut("@annotation(com.zhen.studytotal.limitRequest.annotation.RedisLimitAnnotation)")
public void redisLimit(){}

@Around("redisLimit()")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLimitAnnotation annotation = method.getAnnotation(RedisLimitAnnotation.class);
if (Objects.isNull(annotation)){
return joinPoint.proceed();
}

String key = getKeyByLimitType(annotation,signature);
//调用lua脚本,获取返回结果,这里即为请求次数
Long number = redisTemplate.execute(redisLuaScript, Collections.singletonList(key), annotation.count(), annotation.time());
logger.info("限流时时间内访问次数:{}",number);
if (number != null && number.intValue() != 0 && number.intValue() <= annotation.count()){
logger.info("限流时时间内访问次数:{}",number);
return joinPoint.proceed();
}
throw new RuntimeException("访问次数超过限制,限流中···");
}

/**
* 三种redis-key
* 1.自定义key:prefix + : + key
* 2.接口限流:prefix + : + 接口全类名
* 3.根据IP限流:prefix + : + IP地址 + - + 接口全类名
* @param annotation
* @param signature
* @return
*/
private String getKeyByLimitType(RedisLimitAnnotation annotation, MethodSignature signature) {
String key = "";
LimitTypeEnum limitTypeEnum = annotation.limitType();
String prefix = annotation.prefix();
if (StringUtils.isNotBlank(prefix)){
key += prefix + ":";
}

if (LimitTypeEnum.CUSTOMER == limitTypeEnum) {
String tempKey = annotation.key();
if (StringUtils.isBlank(tempKey)) {
throw new RuntimeException("redis限流->自定义类型,key不能为空");
}
return key + tempKey;
}

Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
String classFullName = targetClass.getName() + "-" + method.getName();
if (LimitTypeEnum.INTERFACE == limitTypeEnum) {
return key + classFullName;
}
//IP
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ipAddr = IPUtils.getIpAddr(request);
return key + ipAddr + "-" + classFullName;
}


}

2.8 测试

1
2
3
4
5
@GetMapping("/redis")
@RedisLimitAnnotation(key = "limitByRedis",time = 5,count = 5,limitType = LimitTypeEnum.IP)
public String testRedis(){
return "REDIS";
}

image-20250318235255409

3.基于 sentinel 限流实现(分布式版)

sentinel 通常是需要结合 springcloud-alibaba 框架一起实用的,而且与框架集成之后,可以配合控制台一起使用达到更好的效果,实际上,sentinel 官方也提供了相对原生的 SDK 可供使用,接下来就以这种方式进行整合

3.1 依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.0</version>
</dependency>

3.2 自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zhen.studytotal.limitRequest.annotation;

import java.lang.annotation.*;

/**
* Description:
* User: zhen0w0
* Date: 2025/3/19
* Time: 00:11
*/
@Documented
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface SentinelLimitRateAnnotation {

//限制类型
String resourceName();

//每秒允许请求数量
int limitCount() default 5;
}

3.3 AOP切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.zhen.studytotal.limitRequest.aspect;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.zhen.studytotal.limitRequest.annotation.SentinelLimitRateAnnotation;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.checkerframework.checker.units.qual.A;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* Description:
* User: zhen0w0
* Date: 2025/3/19
* Time: 00:20
*/
@Aspect
@Component
public class SentinelLimitRateAspect {

@Pointcut(value = "@annotation(com.zhen.studytotal.limitRequest.annotation.SentinelLimitRateAnnotation)")
public void rateLimit(){

}

@Around(value = "rateLimit()")
public Object around(ProceedingJoinPoint joinPoint){
// 1.获取当前方法
Method currentMethod = getCurrentMethod(joinPoint);
if (Objects.isNull(currentMethod)) {
return null;
}
// 2.从方法注解定义上获取限流的类型
String resourceName = currentMethod.getAnnotation(SentinelLimitRateAnnotation.class).resourceName();
if(StringUtils.isEmpty(resourceName)){
throw new RuntimeException("资源名称为空");
}
int limitCount = currentMethod.getAnnotation(SentinelLimitRateAnnotation.class).limitCount();
// 3.初始化规则
initFlowRule(resourceName,limitCount);
Entry entry = null;
Object result = null;
try {
entry = SphU.entry(resourceName);
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级,在此处进行相应的处理操作
System.out.println("blocked");
return "被限流了";
} catch (Exception e) {
Tracer.traceEntry(e, entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return result;
}

private static void initFlowRule(String resourceName,int limitCount) {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//设置受保护的资源
rule.setResource(resourceName);
//设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源阈值
rule.setCount(limitCount);
rules.add(rule);
//加载配置好的规则
FlowRuleManager.loadRules(rules);
}

private Method getCurrentMethod(JoinPoint joinPoint) {
Method[] methods = joinPoint.getTarget().getClass().getMethods();
Method target = null;
for (Method method : methods) {
if (method.getName().equals(joinPoint.getSignature().getName())) {
target = method;
break;
}
}
return target;
}
}

3.4 测试

1
2
3
4
5
@GetMapping("/sentinel")
@SentinelLimitRateAnnotation(resourceName = "测试sentinel限流", limitCount = 1)
public String testSentinel(){
return "Sentinel";
}

image-20250319003246529

4.参考

https://blog.csdn.net/truelove12358/article/details/127751003