java 限流
题记
在高并发的分布式系统中,我们都需要考虑接口并发量突增时造成的严重后果,后端服务的高压力严重甚至会导致系统宕机。为避免这种问题,我们都会为接口添加限流、降级、熔断等能力,从而使接口更为健壮。
限流算法
漏桶(leaky bucket)、令牌桶(Token Bucket)、计数器算法是经典的三种限流算法。最常见的是漏桶和令牌桶算法算法。
漏桶算法
漏桶算法(Leaky Bucket)的主要目的是控制资源获取的速率,平滑突发的流量。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(资源池)溢出,那么资源会被丢弃。 并且通过控制获取资源的速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
如图所示,不管接口请求流量多大,都只能以固定的速率获取资源;当接口请求速率>获取资源速率时,就会被限流处理;
所以漏桶算法的核心是:控制资源的访问速度;但是对于很多应用场景来说,除了要求能够限制资源的平均获取速率外,还要求允许某种程度的突发流量。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
令牌桶算法
令牌桶算法(Token Bucket),不再控制资源的访问速度,而是通过控制可用资源的数量进行更高效的控制。令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。
RateLimiter
Google 开源工具包 Guava 提供了限流工具类 RateLimiter,该类是基于令牌桶算法实现的流量限制,使用十分方便。
RateLimiter有两种限流模式,一种为稳定模式 (SmoothBursty: 令牌生成速度恒定),一种为渐进模式 (SmoothWarmingUp: 令牌生成速度缓慢提升直到维持在一个稳定值)
public class RateLimiterDemo {
private static RateLimiter limiter = RateLimiter.create(5);
public static void exec() {
//acquire()方法——从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求
if (limiter.tryAcquire()) {
System.out.println("限流通过");
//doSomething();
}
}
}
限流实现
预设场景:服务器端提供一个API供第三方平台调用, 要针对每个平台app限制调用速率;
使用ratelimiter实现单机限流
1.在配置文件中配置每个平台的限流速率,key:rate
# 平台限流参数配置(appid:rate)
rateLimit: "{\"10391\":\"1.0\",\"10392\":\"2.0\"}"
2.代码中要定义一个缓存, 缓存key: 令牌桶
//每个appkey的限流速率
@Value("${rateLimit}")
private String limitMap;
// 根据key分不同的令牌桶, 不需要自动清理
private static LoadingCache<String, RateLimiter> caches = CacheBuilder.newBuilder()
.maximumSize(100)
//.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, RateLimiter>() {
@Override
public RateLimiter load(String key) throws Exception {
Map<String, String> map = JSONObject.parseObject(limitMap, Map.class);
String rate = map.get(key);
// 新的key初始化 (限流每秒两个令牌响应)
return RateLimiter.create(Double.valueOf(rate));
}
});
3.业务中调用
private void login(String key) throws ExecutionException {
RateLimiter limiter = caches.get(key);
if (limiter.tryAcquire()) {
System.out.println("成功,进入业务处理");
} else {
System.out.println("失败,限流处理");
//doSomething();
}
}
使用redis+lua实现分布式限流
相同的预设场景:服务器端提供一个API供第三方平台调用, 要针对每个平台app限制调用速率;
但是服务端为分布式系统,则单机限流已经不能达成目的,需要有中间件来存储统一的限流信息;本文中采用数据库来实现分布式限流功能;具体代码实现如下:
配置redis
maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis 配置文件
@Configuration
public class RedisLimiterHelper {
/**
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
redis配置参数
redis:
cluster:
nodes:
- ip:port
password: xxx
timeout: 3000
lettuce:
pool:
max-active: 500
max-idle: 30
max-wait: 3000
# 平台限流参数配置(appid:rate)
rateLimit: "{\"10391\":\"1\",\"10392\":\"2\"}"
自定义限流注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
/**
* 名字
*/
String name() default "";
/**
* key
*/
String key() default "";
/**
* Key的前缀
*/
String prefix() default "";
/**
* 给定的时间范围 单位(秒)
*/
int period() default 1;
/**
* 一定时间内最多访问次数
*/
int count() default 0;
/**
* 限流的类型(用户自定义key 或者 请求ip)
*/
String limitType() default "CUSTOMER";
}
添加拦截器
@Slf4j
@Aspect
@Configuration
public class LimitInterceptor {
@Value("${rateLimit}")
private String limitMap;
private static final String UNKNOWN = "unknown";
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Resource
private HttpServletRequest request;
@Autowired
public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
/**
* 切面
*
* @param pjp
* @return java.lang.Object
*/
@Around("@annotation(com.limiter.java.demo.aop.Limit)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
String limitType = limitAnnotation.limitType();
String name = limitAnnotation.name();
String key;
/**
* 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
*/
switch (limitType) {
case "IP":
key = getIpAddress();
break;
case "CUSTOMER":
key = getAppKey();
break;
default:
key = StringUtils.upperCase(method.getName());
}
int limitPeriod = limitAnnotation.period();
//int limitCount = limitAnnotation.count();
int limitCount = getRate(key);
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
try {
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
if (count != null && count.intValue() <= limitCount) {
log.info("Access try count is {} for name={} and key = {}", count, name, key);
return pjp.proceed();
} else {
//限流处理
//告警
//doSomething();
log.error("系统繁忙,限流处理中,请稍后再试");
return "系统繁忙,限流处理中,请稍后再试";
//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");
}
}
/**
* 编写 redis Lua 限流脚本
*/
public String buildLuaScript() {
StringBuilder lua = new StringBuilder();
lua.append("local c");
lua.append("\nc = redis.call('get',KEYS[1])");
// 调用不超过最大值,则直接返回
lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
lua.append("\nreturn c;");
lua.append("\nend");
// 执行计算器自加
lua.append("\nc = redis.call('incr',KEYS[1])");
lua.append("\nif tonumber(c) == 1 then");
// 从第一次调用开始限流,设置对应键值的过期
lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
lua.append("\nend");
lua.append("\nreturn c;");
return lua.toString();
}
/**
* 获取id地址
*/
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;
}
/**
* 从请求参数中获取appKey
*
* @return
*/
public String getAppKey() {
JSONObject reqObject = JSONObject.parseObject(request.getParameter("content"));
if (!reqObject.isEmpty()) {
return reqObject.getString("key");
}
return null;
}
/**
* 根据平台key值获取对应的限流速率
*
* @return
*/
public int getRate(String key) {
Map<String, String> map = JSONObject.parseObject(limitMap, Map.class);
return Integer.parseInt(map.get(key));
}
}
使用拦截器进行接口限流
@RestController
public class TestController {
@Limit
@RequestMapping("/limiter")
public String testLimiter(@RequestParam("content") String content) {
return "成功,通过限流";
}
}
项目demo
GitHub地址:https://github.com/helloEveryoneByChenglong/java_limiter
GitBee地址:https://gitee.com/chenglonghyGitee/java_limiter.git