SpringBoot 2.x 优雅解决分布式限流
阅读本文大概需要 9 分钟。
来自:blog.csdn.net/johnf_nash/article/details/89791808
一、常用的限流算法
1.计数器方式(传统计数器缺点:临界问题 可能违背定义固定速率原则)
2.令牌桶方式
3、漏桶算法
令牌桶里面装载的是令牌,然后让令牌去关联到数据发送,常规漏桶里面装载的是数据,令牌桶允许用户的正常的持续突发量(Bc),就是一次就将桶里的令牌全部用尽的方式来支持续突发,而常规的漏桶则不允许用户任何突发行。
二、限流实现
基于 redis 的分布式限流
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>21.0version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
属性配置
application.properites
资源文件中添加 redis 相关的配置项spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=battcn
Limit 注解
package com.johnfnash.learn.springboot.ratelimiter.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 限流
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
/**
* 资源的名称
* @return
*/
String name() default "";
/**
* 资源的key
*
* @return
*/
String key() default "";
/**
* Key的prefix
*
* @return
*/
String prefix() default "";
/**
* 给定的时间段
* 单位秒
*
* @return
*/
int period();
/**
* 最多的访问限制次数
*
* @return
*/
int count();
/**
* 类型
*
* @return
*/
LimitType limitType() default LimitType.CUSTOMER;
}
package com.johnfnash.learn.springboot.ratelimiter.annotation;
// 限制的类型
public enum LimitType {
/**
* 自定义key
*/
CUSTOMER,
/**
* 根据请求者IP
*/
IP;
}
RedisTemplate
package com.johnfnash.learn.springboot.ratelimiter;
import java.io.Serializable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisLimiterHelper {
@Bean
public RedisTemplatelimitRedisTemplate(LettuceConnectionFactory factory) {
RedisTemplatetemplate = new RedisTemplate ();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(factory);
return template;
}
}
Limit 拦截器(AOP)
package com.johnfnash.learn.springboot.ratelimiter.aop;
import java.io.Serializable;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.google.common.collect.ImmutableList;
import com.johnfnash.learn.springboot.ratelimiter.annotation.Limit;
import com.johnfnash.learn.springboot.ratelimiter.annotation.LimitType;
@Aspect
@Configuration
public class LimitInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);;
private final String REDIS_SCRIPT = buildLuaScript();
@Autowired
private RedisTemplateredisTemplate;
@Around("execution(public * *(..)) && @annotation(com.johnfnash.learn.springboot.ratelimiter.annotation.Limit)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limitAnno = method.getAnnotation(Limit.class);
LimitType limitType = limitAnno.limitType();
String name = limitAnno.name();
String key = null;
int limitPeriod = limitAnno.period();
int limitCount = limitAnno.count();
switch (limitType) {
case IP:
key = getIpAddress();
break;
case CUSTOMER:
// TODO 如果此处想根据表达式或者一些规则生成 请看 一起来学Spring Boot | 第二十三篇:轻松搞定重复提交(分布式锁)
key = limitAnno.key();
break;
default:
break;
}
ImmutableListkeys = ImmutableList.of(StringUtils.join(limitAnno.prefix(), key));
try {
RedisScriptredisScript = new DefaultRedisScript (REDIS_SCRIPT, Number.class);
Number count = redisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
logger.info("Access try count is {} for name={} and key = {}", count, name, key);
if(count != null && count.intValue() <= limitCount) {
return pjp.proceed();
} else {
throw new RuntimeException("You have been dragged into the blacklist");
}
} catch (Throwable e) {
if (e instanceof RuntimeException) {
throw new RuntimeException(e.getLocalizedMessage());
}
throw new RuntimeException("server exception");
}
}
/**
* 限流 脚本
*
* @return lua脚本
*/
private String buildLuaScript() {
StringBuilder lua = new StringBuilder();
lua.append("local c")
.append("\nc = redis.call('get', KEYS[1])")
// 调用不超过最大值,则直接返回
.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then")
.append("\nreturn c;")
.append("\nend")
// 执行计算器自加
.append("\nc = redis.call('incr', KEYS[1])")
.append("\nif tonumber(c) == 1 then")
// 从第一次调用开始限流,设置对应键值的过期
.append("\nredis.call('expire', KEYS[1], ARGV[2])")
.append("\nend")
.append("\nreturn c;");
return lua.toString();
}
private static final String UNKNOWN = "unknown";
public String getIpAddress() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
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.getRemoteAddr();
}
return ip;
}
}
控制层
@Limit()
注解,如下代码会在 Redis 中生成过期时间为 100s 的 key = test
的记录,特意定义了一个 AtomicInteger 用作测试…package com.johnfnash.learn.springboot.ratelimiter.controller;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.johnfnash.learn.springboot.ratelimiter.annotation.Limit;
@RestController
public class LimiterController {
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();
@Limit(key = "test", period = 100, count = 10)
// 意味著 100S 内最多允許訪問10次
@GetMapping("/test")
public int testLimiter() {
return ATOMIC_INTEGER.incrementAndGet();
}
}
测试
总结
https://github.com/battcn/spring-boot2-learning/tree/master/chapter27
/**
* 限流 脚本(处理临界时间大量请求的情况)
*
* @return lua脚本
*/
private String buildLuaScript2() {
StringBuilder lua = new StringBuilder();
lua.append("local listLen, time")
.append("\nlistLen = redis.call('LLEN', KEYS[1])")
// 不超过最大值,则直接写入时间
.append("\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then")
.append("\nlocal a = redis.call('TIME');")
.append("\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])")
.append("\nelse")
// 取出现存的最早的那个时间,和当前时间比较,看是小于时间间隔
.append("\ntime = redis.call('LINDEX', KEYS[1], -1)")
.append("\nlocal a = redis.call('TIME');")
.append("\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then")
// 访问频率超过了限制,返回0表示失败
.append("\nreturn 0;")
.append("\nelse")
.append("\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])")
.append("\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)")
.append("\nend")
.append("\nend")
.append("\nreturn 1;");
return lua.toString();
}
if(count != null && count.intValue() == 1) {
return pjp.proceed();
} else {
throw new RuntimeException("You have been dragged into the blacklist");
}
buildLuaScript2()
中的lua脚本,报错Write commands not allowed after non deterministic commands.
https://yq.aliyun.com/articles/195914
redis.replicate_commands()
来放开限制。redis.replicate_commands();
”,错误得以解决。推荐阅读:
字节工程师薪资排世界第五,中位数 43 万美元,2021 全球程序员收入报告出炉!
内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级......等技术栈!
⬇戳阅读原文领取! 朕已阅
评论