【订阅专栏合集,关注公众号,作者所有付费文章都能看(持续更新)】
本篇是《Guava RateLimiter互联网限流实战(上)》的姊妹篇,主要介紹算法java实现。包括计数器法、滑动窗口计数法、漏斗桶算法、令牌桶算法。
Guava ratelimiter工程概览
基于redis的简单计数法
新建springboot工程并引入依赖
<properties>
<java.version>1.8</java.version>
<spring.version>2.3.1.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
</dependencies>
配置application.properties
server.port=8888
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=20
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=1000
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=2000
编写RedisCountLimit
基于redis的incr机制
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
/**
* 计数法限流
*/
@Component
public class RedisCountLimit {
public static final String KEY = "ratelimit_";
public static final int LIMIT = 10;
@Autowired
StringRedisTemplate redisTemplate;
public boolean triggerLimit(String reqPath) {
String redisKey = KEY + reqPath;
Long count = redisTemplate.opsForValue().increment(redisKey, 1);
System.out.println(LocalTime.now() + " " + reqPath + " " + count);
if (count != null && count == 1) {
redisTemplate.expire(redisKey, 60, TimeUnit.SECONDS);
}
//防止出现并发操作未设置超时时间的场景,这样key就是永不过期,存在风险
if (redisTemplate.getExpire(redisKey, TimeUnit.SECONDS) == -1) {
redisTemplate.expire(redisKey, 60, TimeUnit.SECONDS);
}
if (count > LIMIT) {
System.out.println(LocalTime.now() + " " + reqPath + " count is:" + count + ",触发限流");
return true;
}
return false;
}
}
Controller层集成
import com.bigbird.ratelimit.rediscount.RedisCountLimit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
/**
* 基于redis的计数器限流demo
*/
@RestController
public class RedisCountLimitController {
@Autowired
RedisCountLimit redisCountLimit;
@RequestMapping("/rediscount")
public String redisCount(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = redisCountLimit.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
@RequestMapping("/rediscount2")
public String redisCount2(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = redisCountLimit.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
}
运行测试
启动springboot工程,确保redis已运行,浏览器访问,f5多刷新几次
- http://localhost:8888/rediscount
- http://localhost:8888/rediscount2
基于redis的滑动窗口计数法
编写RedisSlidingCountLimit
通过redis的zset数据结构
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.UUID;
/**
* 滑动窗口计数法限流
*/
@Component
public class RedisSlidingCountLimit {
public static final String KEY = "slidelimit_";
public static final int LIMIT = 10;
//限流时间间隔(秒)
public static final int PERIOD = 60;
@Autowired
StringRedisTemplate redisTemplate;
public boolean triggerLimit(String reqPath) {
String redisKey = KEY + reqPath;
if (redisTemplate.hasKey(redisKey)) {
Integer count = redisTemplate.opsForZSet().rangeByScore(redisKey, System.currentTimeMillis() - PERIOD * 1000, System.currentTimeMillis()).size();
System.out.println(count);
if (count != null && count > LIMIT) {
System.out.println(LocalTime.now() + " " + reqPath + " count is:" + count + ",触发限流");
return true;
}
}
long currentTime = System.currentTimeMillis();
redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), currentTime);
// 清除旧的访问数据,比如period=60s时,标识清除60s以前的记录
redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, System.currentTimeMillis() - PERIOD * 1000);
return false;
}
}
Controller层集成
import com.bigbird.ratelimit.rediscount.RedisSlidingCountLimit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
/**
* 基于Redis的滑动窗口计数器限流demo
*/
@RestController
public class RedisSlidingCountLimitController {
@Autowired
RedisSlidingCountLimit redisSlidingCountLimit;
@RequestMapping("/slidecount")
public String redisCount(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = redisSlidingCountLimit.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
@RequestMapping("/slidecount2")
public String redisCount2(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = redisSlidingCountLimit.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
}
运行测试
启动springboot工程,确保redis已运行,访问
- http://localhost:8888/slidecount
- http://localhost:8888/slidecount2
漏斗桶算法
编写LeakyBucket
import java.time.LocalTime;
/**
* 漏斗桶算法限流
*/
public class LeakyBucket {
/**
* 每秒处理数量(出水速率)
*/
private int rate;
/**
* 桶容量
*/
private int capacity;
/**
* 当前水量
*/
private int water;
/**
* 最后刷新时间
*/
private long refreshTime;
public LeakyBucket(int rate, int capacity) {
this.capacity = capacity;
this.rate = rate;
}
private void refreshWater() {
long now = System.currentTimeMillis();
water = (int) Math.max(0, water - (now - refreshTime) / 1000 * rate);
refreshTime = now;
}
public synchronized boolean triggerLimit(String reqPath) {
refreshWater();
if (water < capacity) {
water++;
System.out.println(LocalTime.now() + " " + reqPath + " current capacity is:" + (capacity - water) + ",water is:" + water + ",请求成功");
return false;
} else {
System.out.println(LocalTime.now() + " " + reqPath + " current capacity is:" + (capacity - water) + ",water is:" + water + ",触发限流");
return true;
}
}
}
Controller层集成
import com.bigbird.ratelimit.leakybucket.LeakyBucket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
/**
* 漏斗桶算法限流demo
*/
@RestController
public class LeakyBucketLimitController {
LeakyBucket bucket1 = new LeakyBucket(2, 10);
LeakyBucket bucket2 = new LeakyBucket(2, 20);
@RequestMapping("/leakyBucket1")
public String leakyBucket1(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = bucket1.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
@RequestMapping("/leakyBucket2")
public String leakyBucket2(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = bucket2.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
}
运行测试
启动springboot工程,浏览器访问下列地址,连续f5多刷新测试
- http://localhost:8888/leakyBucket1
- http://localhost:8888/leakyBucket2
令牌桶算法(RateLimiter)
基于Guava RateLimiter实现
引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
编写TokenBucket
import com.google.common.util.concurrent.RateLimiter;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
/**
* 令牌桶算法限流
*/
public class TokenBucket {
/**
* qps,即每秒处理数量
*/
private int rate;
private RateLimiter rateLimiter;
public TokenBucket(int rate) {
this.rate = rate;
this.rateLimiter = RateLimiter.create(rate);
//在实际业务开发中,一般一种限流场景下的个体对应一个RateLimiter实例
//比如对客户端IP限流,会创建一个 static volatile 的 Map <Ip,RateLimiter>保存各个IP的限流器
//比如对url限流,会创建一个 static volatile 的 Map <Url,RateLimiter>保存各个Url的限流器
//比如对商户限流,会创建一个 static volatile 的 Map <MerchantId,RateLimiter>保存各个商户的限流器
//static volatile 修饰的变量可以保证全局可见、统一配置,实时修改限流配置后立即生效
}
public boolean triggerLimit(String reqPath) {
boolean acquireRes = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (acquireRes) {
System.out.println(LocalTime.now() + " " + reqPath + ",请求成功");
return false;
} else {
System.out.println(LocalTime.now() + " " + reqPath + ",触发限流");
return true;
}
}
}
关于volatile关键字参考《volatile关键字解析与实践》
Controller层集成
import com.bigbird.ratelimit.tokenbucket.TokenBucket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
/**
* 令牌桶限流算法demo
*/
@RestController
public class TokenBucketLimitController {
/**
* 每秒钟限速1
*/
TokenBucket bucket1 = new TokenBucket(1);
/**
* 每秒钟限速2
*/
TokenBucket bucket2 = new TokenBucket(2);
@RequestMapping("/tokenBucket1")
public String leakyBucket1(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = bucket1.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
@RequestMapping("/tokenBucket2")
public String leakyBucket2(HttpServletRequest request) {
String servletPath = request.getServletPath();
boolean triggerLimit = bucket2.triggerLimit(servletPath);
if (triggerLimit) {
return LocalDateTime.now() + " " + servletPath + " 系统忙,稍后再试";
} else {
return LocalDateTime.now() + " " + servletPath + "请求成功";
}
}
}
运行测试
启动springboot工程,浏览器访问下列地址,连续f5多刷新测试
- http://localhost:8888/tokenBucket1
- http://localhost:8888/tokenBucket2
在实际业务开发中,一般一种限流场景下的个体对应一个RateLimiter实例
比如对客户端IP限流,会创建一个 static volatile 的 Map <Ip,RateLimiter>保存各个IP的限流器
比如对接口url限流,会创建一个 static volatile 的 Map <Url,RateLimiter>保存各个Url的限流器
比如对商户限流,会创建一个 static volatile 的 Map <MerchantId,RateLimiter>保存各个商户的限流器
static volatile 修饰的变量可以保证全局统一配置,实时修改限流配置后立即生效
自定义注解、aop封装限流
上述实现方式简单粗暴,实际应用中可以封装自定义注解,并通过aop实现controller层接口自动限流拦截。废话不多说,上代码。下面的案例基于RateLimiter令牌桶。其它算法读者可以参考此例自行封装。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtRateLimiter {
double permitsPerSecond();
long timeout();
}
编写aop切面
import com.bigbird.ratelimit.annotation.ExtRateLimiter;
import com.google.common.util.concurrent.RateLimiter;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.time.LocalTime;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 封装基于RateLimiter的限流注解
*/
@Component
@Aspect
public class RateLimiterAop {
/**
* 保存接口路径和限流器的对应关系
*/
private ConcurrentHashMap<String, RateLimiter> rateLimiters = new ConcurrentHashMap();
@Pointcut("execution(public * com.bigbird.ratelimit.controller.*.*(..))")
public void rateLimiterAop() {
}
/**
* 使用环绕通知拦截所有Controller请求
*
* @param proceedingJoinPoint
* @return
*/
@Around("rateLimiterAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
if (method == null) {
return null;
}
ExtRateLimiter extRateLimiter = method.getDeclaredAnnotation(ExtRateLimiter.class);
if (extRateLimiter == null) {
return proceedingJoinPoint.proceed();
}
double permitsPerSecond = extRateLimiter.permitsPerSecond();
long timeout = extRateLimiter.timeout();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String requestURI = requestAttributes.getRequest().getRequestURI();
RateLimiter rateLimiter = rateLimiters.get(requestURI);
if (rateLimiter == null) {
rateLimiter = RateLimiter.create(permitsPerSecond);
RateLimiter rateLimiterPrevious = rateLimiters.putIfAbsent(requestURI, rateLimiter);
if (rateLimiterPrevious != null) {
rateLimiter = rateLimiterPrevious;
}
}
boolean tryAcquire = rateLimiter.tryAcquire(timeout, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
System.out.println(LocalTime.now() + " " + requestURI + " 触发限流");
doFallback();
return null;
}
System.out.println(LocalTime.now() + " " + requestURI + " 请求成功");
return proceedingJoinPoint.proceed();
}
private void doFallback() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = requestAttributes.getResponse();
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.println("系统忙,请稍后再试!");
} catch (IOException e) {
e.printStackTrace();
} finally {
writer.close();
}
}
}
Controller层集成
对要限流的接口加ExtRateLimiter 注解设置
import com.bigbird.ratelimit.annotation.ExtRateLimiter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalTime;
/**
* 自定义注解标识接口进行限流
*/
@RestController
public class ExtRateLimiterController {
@RequestMapping("/extRate1")
@ExtRateLimiter(permitsPerSecond = 0.5, timeout = 500)
public String extRate1(HttpServletRequest request) {
return LocalTime.now() + " " + request.getRequestURI() + "请求成功";
}
@RequestMapping("/extRate2")
@ExtRateLimiter(permitsPerSecond = 2, timeout = 500)
public String extRate2(HttpServletRequest request) {
return LocalTime.now() + " " + request.getRequestURI() + "请求成功";
}
}
运行测试
启动springboot工程,浏览器访问下列地址,连续f5多刷新测试
- http://localhost:8888/extRate1
- http://localhost:8888/extRate2
小结
本文通俗易懂地介绍了互联网限流相关的概念与算法,并且附以Java代码实现。包括计数器法、滑动窗口计数法、漏斗桶算法、令牌桶算法。最后封装了一个自定义限流注解以及aop拦截接口限流。