作者:钟小嘿
原文链接: https://www.cnblogs.com/zys2019/p/17385568.html
1、序言 {#toc_h2_0}
联网系统通常都要面对大并发大流量的请求,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法对外提供服务。为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。
限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。
常见的限流算法有三种: {#toc_h3_1}
1)计数器限流 {#toc_h4_2}
计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。
如:使用 AomicInteger 来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。
2)漏桶算法 {#toc_h4_3}
漏桶算法思路很简单,我们把水比作是 请求
,漏桶比作是 系统处理能力极限
,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
3)令牌桶算法 {#toc_h4_4}
令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
系统会维护一个令牌( token
)桶,以一个恒定的速度往桶里放入令牌( token
),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌( token
),当桶里没有令牌( token
)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
2、单机版 {#toc_h2_5}
对于单机版,直接使用Guava即可。
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效,实现步骤如下:
第一步:引入guava依赖包
<dependency> <groupid>com.google.guava</groupid> <artifactid>guava</artifactid> <version>30.1-jre</version> </dependency>
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
<dependency>
<groupid>com.google.guava</groupid>
<artifactid>guava</artifactid>
<version>30.1-jre</version>
</dependency>
</code>
</pre>
第二步:给接口加上限流逻辑
@Slf4j @RestController @RequestMapping("/limit") public class LimitController { /** * 限流策略 : 1秒钟2个请求 */ private final RateLimiter limiter = RateLimiter.create(2.0); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @GetMapping("/test1") public String testLimiter() { //500毫秒内,没拿到令牌,就直接进入服务降级 boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf)); return "当前排队人数较多,请稍后再试!"; } log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf)); return "请求成功"; } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {
/**
* 限流策略 : 1秒钟2个请求
*/
private final RateLimiter limiter = RateLimiter.create(2.0);
private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@GetMapping("/test1")
public String testLimiter() {
//500毫秒内,没拿到令牌,就直接进入服务降级
boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf));
return "当前排队人数较多,请稍后再试!";
}
log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf));
return "请求成功";
}
}
</code>
</pre>
第三步:体验效果
通过访问测试地址: http://127.0.0.1:8080/limit/test1,反复刷新并观察后端日志
![](http://static.51tbox.com/static/2025-01-06/col/c96f1de8ea934d2b567a2db004a4b6af/331f0e7b18094577ba1075bd3ef1a08a.jpg)
初体验后,就可以对其进行解耦了,对于这样的限流,全部写在controller中是冗余的,可以使用注解方式来优化代码:
①加入AOP依赖
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-aop</artifactid> </dependency>
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-aop</artifactid>
</dependency>
</code>
</pre>
②自定义限流注解
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Limit { /** * 资源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的访问限制次数 */ double permitsPerSecond () ; /** * 获取令牌最大等待时间 */ long timeout(); /** * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒 */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; /** * 得不到令牌的提示语 */ String msg() default "系统繁忙,请稍后再试."; }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 最多的访问限制次数
*/
double permitsPerSecond () ;
/**
* 获取令牌最大等待时间
*/
long timeout();
/**
* 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
*/
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
/**
* 得不到令牌的提示语
*/
String msg() default "系统繁忙,请稍后再试.";
}
</code>
</pre>
③使用AOP切面拦截限流注解
@Slf4j @Aspect @Component public class LimitAop { /** * 不同的接口,不同的流量控制 * map的key为 Limiter.key */ private final Map<string, ratelimiter=""> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.zxh.limit.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { //key作用:不同的接口,不同的流量控制 String key=limit.key(); RateLimiter rateLimiter = null; //验证缓存是否有命中key if (!limitMap.containsKey(key)) { // 创建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond()); } rateLimiter = limitMap.get(key); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回异常提示 if (!acquire) { log.debug("令牌桶={},获取令牌失败",key); this.responseFail(limit.msg()); return null; } } return joinPoint.proceed(); } /** * 直接向前端抛出异常 * @param msg 提示信息 */ private void responseFail(String msg) { HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter pr=response.getWriter(); pr.write(msg); } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
@Aspect
@Component
public class LimitAop {
/**
* 不同的接口,不同的流量控制
* map的key为 Limiter.key
*/
private final Map<string, ratelimiter=""> limitMap = Maps.newConcurrentMap();
@Around("@annotation(com.zxh.limit.Limit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿limit的注解
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
//key作用:不同的接口,不同的流量控制
String key=limit.key();
RateLimiter rateLimiter = null;
//验证缓存是否有命中key
if (!limitMap.containsKey(key)) {
// 创建令牌桶
rateLimiter = RateLimiter.create(limit.permitsPerSecond());
limitMap.put(key, rateLimiter);
log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
}
rateLimiter = limitMap.get(key);
// 拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
// 拿不到命令,直接返回异常提示
if (!acquire) {
log.debug("令牌桶={},获取令牌失败",key);
this.responseFail(limit.msg());
return null;
}
}
return joinPoint.proceed();
}
/**
* 直接向前端抛出异常
* @param msg 提示信息
*/
private void responseFail(String msg) {
HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter pr=response.getWriter();
pr.write(msg);
}
}
</code>
</pre>
④给需要限流的接口加上注解
@Slf4j @RestController @RequestMapping("/limit") public class LimitController { @GetMapping("/test2") @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!") public String limit2() { log.info("令牌桶limit2获取令牌成功"); return "ok"; } @GetMapping("/test3") @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!") public String limit3() { log.info("令牌桶limit3获取令牌成功"); return "ok"; } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {
@GetMapping("/test2")
@Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")
public String limit2() {
log.info("令牌桶limit2获取令牌成功");
return "ok";
}
@GetMapping("/test3")
@Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!")
public String limit3() {
log.info("令牌桶limit3获取令牌成功");
return "ok";
}
}
</code>
</pre>
⑤体验效果
通过访问测试地址: http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果
正常响应是返回"ok",触发限流时显示"系统繁忙,请稍后再试!"
3、分布式版 {#toc_h2_6}
------------------
上述单机版并不能解决分布式的问题,那么分布式限流可以采用 ` Redis + Lua ` 实现。
第一步:引入Redis依赖包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</code>
</pre>
第二步:配置Redis
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
</code>
</pre>
第三步:自定义限流注解
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface RedisLimit { /** * 资源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的访问限制次数 */ long permitsPerSecond() default 2; /** * 过期时间也可以理解为单位时间,单位秒,默认60 */ long expire() default 60; /** * 得不到令牌的提示语 */ String msg() default "系统繁忙,请稍后再试."; }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimit {
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 最多的访问限制次数
*/
long permitsPerSecond() default 2;
/**
* 过期时间也可以理解为单位时间,单位秒,默认60
*/
long expire() default 60;
/**
* 得不到令牌的提示语
*/
String msg() default "系统繁忙,请稍后再试.";
}
</code>
</pre>
第四步:创建限流异常(代码中并未体现异常拦截的代码,开发时根据时间情况进行拦截并处理即可)
public class RedisLimitException extends RuntimeException{ public RedisLimitException(String msg) { super( msg ); } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
public class RedisLimitException extends RuntimeException{
public RedisLimitException(String msg) {
super( msg );
}
}
</code>
</pre>
第五步:在resouces文件夹下创建lua文件 rateLimiter.lua
--获取KEY local key = KEYS[1] local limit = tonumber(ARGV[1]) local curentLimit = tonumber(redis.call('get', key) or "0") if curentLimit + 1 > limit then return 0 else -- 自增长 1 redis.call('INCRBY', key, 1) -- 设置过期时间 redis.call('EXPIRE', key, ARGV[2]) return curentLimit + 1 end
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
--获取KEY
local key = KEYS[1]
local limit = tonumber(ARGV\[1\])
local curentLimit = tonumber(redis.call('get', key) or "0")
if curentLimit + 1 \> limit
then return 0
else
-- 自增长 1
redis.call('INCRBY', key, 1)
-- 设置过期时间
redis.call('EXPIRE', key, ARGV\[2\])
return curentLimit + 1
end
</code>
</pre>
第六步:使用AOP切面拦截限流注解
@Slf4j @Aspect @Component public class RedisLimitAop { @Autowired private StringRedisTemplate stringRedisTemplate; private DefaultRedisScript<Long> redisScript; @PostConstruct public void init(){ redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Long.class); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua"))); } @Pointcut("@annotation(com.zxh.limit.redis.RedisLimit)") private void check() { } @Before("check()") public void before(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿到RedisLimit注解,如果存在则说明需要限流 RedisLimit redisLimit = method.getAnnotation(RedisLimit.class); if(redisLimit != null){ //获取redis的key String key = redisLimit.key(); String className = method.getDeclaringClass().getName(); String name = method.getName(); String limitKey = key + className + method.getName(); log.info(limitKey); if(StringUtils.isEmpty(key)){ throw new RedisLimitException( "key cannot be null" ); } long limit = redisLimit.permitsPerSecond(); long expire = redisLimit.expire(); List<String> keys = new ArrayList<>(); keys.add( key ); String luaScript = buildLuaScript(); RedisScript<Long> redisScript = new DefaultRedisScript<>( luaScript, Long.class ); Long count = stringRedisTemplate.execute( redisScript, keys, String.valueOf(limit), String.valueOf(expire) ); log.info( "Access try count is {} for key={}", count, key ); if (count != null && count == 0) { log.debug("令牌桶={},获取令牌失败",key); throw new RedisLimitException(redisLimit.msg()); } } } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
@Aspect
@Component
public class RedisLimitAop {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript<Long> redisScript;
@PostConstruct
public void init(){
redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
}
@Pointcut("@annotation(com.zxh.limit.redis.RedisLimit)")
private void check() {
}
@Before("check()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿到RedisLimit注解,如果存在则说明需要限流
RedisLimit redisLimit = method.getAnnotation(RedisLimit.class);
if(redisLimit != null){
//获取redis的key
String key = redisLimit.key();
String className = method.getDeclaringClass().getName();
String name = method.getName();
String limitKey = key + className + method.getName();
log.info(limitKey);
if(StringUtils.isEmpty(key)){
throw new RedisLimitException( "key cannot be null" );
}
long limit = redisLimit.permitsPerSecond();
long expire = redisLimit.expire();
List<String> keys = new ArrayList<>();
keys.add( key );
String luaScript = buildLuaScript();
RedisScript<Long> redisScript = new DefaultRedisScript<>( luaScript, Long.class );
Long count = stringRedisTemplate.execute( redisScript, keys, String.valueOf(limit), String.valueOf(expire) );
log.info( "Access try count is {} for key={}", count, key );
if (count != null && count == 0) {
log.debug("令牌桶={},获取令牌失败",key);
throw new RedisLimitException(redisLimit.msg());
}
}
}
}
</code>
</pre>
第七步:给需要限流的接口加上注解
@Slf4j @RestController @RequestMapping("/limit/redis") public class LimitRedisController { /** * 基于Redis AOP限流 */ @GetMapping("/test") @RedisLimit(key = "redis-limit:test", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!") public String test() { log.info("限流成功。。。"); return "ok"; } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
@RestController
@RequestMapping("/limit/redis")
public class LimitRedisController {
/**
* 基于Redis AOP限流
*/
@GetMapping("/test")
@RedisLimit(key = "redis-limit:test", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!")
public String test() {
log.info("限流成功。。。");
return "ok";
}
}
</code>
</pre>
第八步:体验效果
通过访问测试地址: http://127.0.0.1:8080/limit/redis/test,反复刷新并观察输出结果
正常响应是返回"ok",触发限流时显示"当前排队人数较多,请稍后再试!"(上述代码未体现异常拦截处理,这里会出现500的错误)
4、整合版 {#toc_h2_7}
-----------------
当然,每个项目都加入这个代码也是有些繁杂的,为了可以方便的进行切换两种场景,可以将其封装为starter,然后按需引入即可。
### 第1步、创建通用模块cloud-limiter-starter {#toc_h3_8}
首先在父项目下创建一个模块
![](http://static.51tbox.com/static/2025-01-06/col/c96f1de8ea934d2b567a2db004a4b6af/1dfd7d31bda54389bf399942b3aeba1d.jpg)
然后在pom文件中引入相关依赖
<dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <!--SpringFramework--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <scope>provided</scope> </dependency> </dependencies>
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!--SpringFramework-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<scope>provided</scope>
</dependency>
\</dependencies\>
</code>
</pre>
### 第2步、实现限流功能 {#toc_h3_9}
①创建限流接口
既然有两种限流机制,按照套路肯定得先创建一个限流接口,就叫 ` LimiterManager ` 吧。
public interface LimiterManager { boolean tryAccess(Limiter limiter); }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
public interface LimiterManager {
boolean tryAccess(Limiter limiter);
}
</code>
</pre>
②分别实现Redis的限流功能和Guava的限流功能,这里只给出核心代码。
Guava限流的核心实现GuavaLimiter
@Slf4j public class GuavaLimiter implements LimiterManager{ private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap(); @Override public boolean tryAccess(Limiter limiter) { RateLimiter rateLimiter = getRateLimiter(limiter); if (rateLimiter == null) { return false; } boolean access = rateLimiter.tryAcquire(1,100, TimeUnit.MILLISECONDS); log.info("{} access :{}",limiter.getKey() , access); return access; } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
public class GuavaLimiter implements LimiterManager{
private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap();
@Override
public boolean tryAccess(Limiter limiter) {
RateLimiter rateLimiter = getRateLimiter(limiter);
if (rateLimiter == null) {
return false;
}
boolean access = rateLimiter.tryAcquire(1,100, TimeUnit.MILLISECONDS);
log.info("{} access :{}",limiter.getKey() , access);
return access;
}
}
</code>
</pre>
Redis限流的核心实现RedisLimiter
@Slf4j public class RedisLimiter implements LimiterManager{ private final StringRedisTemplate stringRedisTemplate; public RedisLimiter(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryAccess(Limiter limiter) { String key = limiter.getKey(); if (StringUtils.isEmpty(key)) { throw new LimiterException( "redis limiter key cannot be null" ); } List<String> keys = new ArrayList<>(); keys.add( key ); int seconds = limiter.getSeconds(); int limitCount = limiter.getLimitNum(); String luaScript = buildLuaScript(); RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); Long count = stringRedisTemplate.execute( redisScript, keys, "" + limitCount, "" + seconds ); log.info( "Access try count is {} for key={}", count, key ); return count != null && count != 0; } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Slf4j
public class RedisLimiter implements LimiterManager{
private final StringRedisTemplate stringRedisTemplate;
public RedisLimiter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryAccess(Limiter limiter) {
String key = limiter.getKey();
if (StringUtils.isEmpty(key)) {
throw new LimiterException( "redis limiter key cannot be null" );
}
List<String> keys = new ArrayList<>();
keys.add( key );
int seconds = limiter.getSeconds();
int limitCount = limiter.getLimitNum();
String luaScript = buildLuaScript();
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long count = stringRedisTemplate.execute( redisScript, keys, "" + limitCount, "" + seconds );
log.info( "Access try count is {} for key={}", count, key );
return count != null && count != 0;
}
}
</code>
</pre>
### 第3步、创建配置类 {#toc_h3_10}
编写配置类根据配置文件注入限流实现类,当配置文件中属性 ` limit.type=local ` 时启用Guava限流机制,当 ` limit.type=redis ` 时启用Redis限流机制。
@Configuration public class LimiterConfigure { @Bean @ConditionalOnProperty(name = "limit.type",havingValue = "local") public LimiterManager guavaLimiter(){ return new GuavaLimiter(); } @Bean @ConditionalOnProperty(name = "limit.type",havingValue = "redis") public LimiterManager redisLimiter(StringRedisTemplate stringRedisTemplate){ return new RedisLimiter(stringRedisTemplate); } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Configuration
public class LimiterConfigure {
@Bean
@ConditionalOnProperty(name = "limit.type",havingValue = "local")
public LimiterManager guavaLimiter(){
return new GuavaLimiter();
}
@Bean
@ConditionalOnProperty(name = "limit.type",havingValue = "redis")
public LimiterManager redisLimiter(StringRedisTemplate stringRedisTemplate){
return new RedisLimiter(stringRedisTemplate);
}
}
</code>
</pre>
### 第4步、创建AOP {#toc_h3_11}
创建一个AOP进行切面拦截
@Aspect @EnableAspectJAutoProxy(proxyTargetClass = true) //使用CGLIB代理 @Conditional(LimitAspectCondition.class) public class LimitAspect { @Setter(onMethod_ = @Autowired) private LimiterManager limiterManager; @Pointcut("@annotation(com.zxh.limit.aop.Limit)") private void check() { } @Before("check()") public void before(JoinPoint joinPoint){ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Limit limit = method.getAnnotation(Limit.class); if(limit != null){ Limiter limiter = Limiter.builder().limitNum(limit.limitNum()) .seconds(limit.seconds()) .key(limit.key()).build(); if(!limiterManager.tryAccess(limiter)){ throw new LimiterException( "There are currently many people , please try again later!" ); } } } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true) //使用CGLIB代理
@Conditional(LimitAspectCondition.class)
public class LimitAspect {
@Setter(onMethod_ = @Autowired)
private LimiterManager limiterManager;
@Pointcut("@annotation(com.zxh.limit.aop.Limit)")
private void check() {
}
@Before("check()")
public void before(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);
if(limit != null){
Limiter limiter = Limiter.builder().limitNum(limit.limitNum())
.seconds(limit.seconds())
.key(limit.key()).build();
if(!limiterManager.tryAccess(limiter)){
throw new LimiterException( "There are currently many people , please try again later!" );
}
}
}
}
</code>
</pre>
这里使用自定义条件选择器
public class LimitAspectCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { //检查配置文件是否包含limit.type属性 return conditionContext.getEnvironment().containsProperty(ConfigConstant.LIMIT_TYPE); } }
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
public class LimitAspectCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//检查配置文件是否包含limit.type属性
return conditionContext.getEnvironment().containsProperty(ConfigConstant.LIMIT_TYPE);
}
}
</code>
</pre>
### 第5步、创建spring.factories文件,引导SpringBoot加载配置类 {#toc_h3_12}
## AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.zxh.limit.config.LimiterConfigure,\ com.zxh.limit.aop.LimitAspect
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
## AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zxh.limit.config.LimiterConfigure,\
com.zxh.limit.aop.LimitAspect
</code>
</pre>
完整目录结构如下:
![](http://static.51tbox.com/static/2025-01-06/col/c96f1de8ea934d2b567a2db004a4b6af/3baba077bc4a4faaa82baad2ea88e3f2.jpg)
### 第6步、在项目中引用限流组件 {#toc_h3_13}
①引入依赖
<dependency> <groupId>com.zxh</groupId> <artifactId>cloud-limit-starter</artifactId> </dependency>
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
<dependency>
<groupId>com.zxh</groupId>
<artifactId>cloud-limit-starter</artifactId>
</dependency>
</code>
</pre>
②在 ` application.properties ` 中设置加载的限流组件
limit.type = redis
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
limit.type = redis
</code>
</pre>
如果不配置此属性则不加载对应限流功能。
③在需要限流的接口上加上注解
@Limit(key = "Limiter:test",limitNum = 3,seconds = 1)
<svg>
<use xlink:href="#vditor-icon-copy" />
</svg>
</span>
@Limit(key = "Limiter:test",limitNum = 3,seconds = 1)
</code>
</pre>
对于SpringBoot自定义的starter是非常有用的,毕竟写代码的思想就是减少冗余,提高效率,便于维护。
参考: {#toc_h2_14}
----------------
https://www.cnblogs.com/jianzh5/p/17291540.html及其系列文章
*** ** * ** ***
标题:SpringBoot 如何进行接口限流?
作者: https://51tbox.com/
地址: /
如未加特殊说明,文章均为原创,转载必须注明出处。均采用 https://creativecommons.org/licenses/by-sa/4.0/deed.zh-Hans !
本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!文章观点不代表本网站立场,如需处理请联系首页客服。
• 网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
• 公众号转载请联系网站首页的微信号申请白名单!
*** ** * ** ***
![]()