失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > java redis令牌桶_接口限流令牌桶算法Redis分布式限流

java redis令牌桶_接口限流令牌桶算法Redis分布式限流

时间:2023-10-31 02:15:54

相关推荐

java redis令牌桶_接口限流令牌桶算法Redis分布式限流

工作中对外提供的API 接口设计很多时候要考虑限流,如果不考虑,可能会造成系统的连锁反应,轻者响应缓慢,重者系统宕机。而常用的限流算法有令牌桶算法和漏桶算法,本篇介绍令牌桶算法

令牌桶算法

image.png

原理如上图,系统以恒定速率不断产生令牌,令牌桶有最大容量,超过最大容量则丢弃,同时用户请求接口,如果此时令牌桶中有令牌则能访问获取数据,否则直接拒绝用户请求

java代码实现

/**

* 线程池每0.5s发送随机数量的请求,每次请求计算当前的令牌数量,请求令牌数量超出当前令牌数量,则产生限流

*/

@Slf4j

public class TokensLimiter {

private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

// 最后一次令牌发放时间

public long timeStamp = System.currentTimeMillis();

// 桶的容量

private int capacity = 7;

// 令牌生成速度5/s

private int rate = 5;

// 当前令牌数量

private int tokens;

public void acquire() {

//令牌生成速度 = 5/1s 此次时间-上次生成时间=中间耗费时间

scheduledExecutorService.scheduleWithFixedDelay(() -> {

long now = System.currentTimeMillis();

long tokensCal = tokens + (now - timeStamp) * rate/1000;

int tokenCalInt = (int)tokensCal;

// 当前令牌数

tokens = Math.min(capacity,tokenCalInt);

//每隔0.5秒发送随机数量的请求

int permits = (int) (Math.random() * 9) + 1;

log.info("请求令牌数:" + permits + ",当前令牌数:" + tokens);

timeStamp = now;

if (tokens < permits) {

// 若不到令牌,则拒绝

log.info("限流了");

} else {

// 还有令牌,领取令牌

tokens -= permits;

log.info("剩余令牌=" + tokens);

}

}, 1000, 500, TimeUnit.MILLISECONDS);

//1秒以后开始执行第一次任务,第一次执行完每隔500ms执行下次任务

}

public static void main(String[] args) {

TokensLimiter tokensLimiter = new TokensLimiter();

tokensLimiter.acquire();

}

}

输出结果:

image.png

Guava rateLimiter实现

public abstract class AbstractInterceptor extends HandlerInterceptorAdapter {

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

ResponseEnum result;

try{

result = preFilter(request);

}catch (Exception e){

result = ResponseEnum.SERVER_ERROR;

}

if(ResponseEnum.OK == result){

return true;

}

//未申请到被限流

rateLimitResponse(result,response);

return false;

}

private void rateLimitResponse(ResponseEnum result, HttpServletResponse response){

R r = R.error(500,result.getMsg());

try {

response.getWriter().write(JSON.toJSONString(r));

} catch (IOException e) {

e.printStackTrace();

}

}

//自己声明的抽象方法,交给子类实现

protected abstract ResponseEnum preFilter(HttpServletRequest request);

}

@Slf4j

@Component

public class RateLimitInterceptor extends AbstractInterceptor {

/**

* 单机全局限流器,QPS为1

*/

@SuppressWarnings("UnstableApiUsage")

private static final RateLimiter RATE_LIMITER = RateLimiter.create(1);

@Override

protected ResponseEnum preFilter(HttpServletRequest request) {

if(!RATE_LIMITER.tryAcquire()){

log.info("限流了..");

return ResponseEnum.RATE_LIMIT;

}

log.info("请求成功");

return ResponseEnum.OK;

}

}

@Getter

public enum ResponseEnum {

OK("成功"),RATE_LIMIT("访问次数受限"),SERVER_ERROR("服务器错误"),QUERY_FAIL("查询失败");

private String msg;

ResponseEnum(String msg) {

this.msg = msg;

}

}

执行结果

image.png

Redis rateLimiter

分布式环境下解决方案

需要限流的接口使用该注解

/**

* 1(时间)分钟(单位)允许某个ip请求的最大次数(max)

*

* 如每隔2分钟,单IP限定访问次数不能超过10次

*/

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface RedisRateLimiter {

/**

* 默认根据IP拦截

*/

LimitType limitType() default LimitType.IP;

enum LimitType{

GENERAL,IP,USERID;

}

/**

* 限制时间长度

*/

long timeLimitLength() default 1;

/**

* 限制时间长度的单位

*/

TimeUnit timeLimitLengthUnit() default TimeUnit.SECONDS;

/**

* 允许时间内最大访问数

*/

long max() default 1;

/**

* redis存储的key

*/

String storeKey() default "";

}

redis配置文件

@Configuration

public class RedisConfig {

@Bean

@SuppressWarnings("unchecked")

public RedisScript limitRedisScript() {

DefaultRedisScript redisScript = new DefaultRedisScript<>();

redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/redis/limit.lua")));

redisScript.setResultType(Long.class);

return redisScript;

}

}

切面

@Slf4j

@Component

@Aspect

@RequiredArgsConstructor(onConstructor_ = @Autowired)

public class RedisRateLimitAspect {

private final static String REDIS_RATE_LIMIT_KEY_PREFIX="limit:";

private final StringRedisTemplate stringRedisTemplate;

private final RedisScript limitRedisScript;

@Pointcut("@annotation(com.jerrysong.jwt.annotations.RedisRateLimiter)")

public void rateLimit() {}

@Before("rateLimit()")

public void pointCut(JoinPoint joinPoint){

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

Method method = signature.getMethod();

// 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解

RedisRateLimiter redisRateLimit = AnnotationUtils.findAnnotation(method, RedisRateLimiter.class);

if(redisRateLimit!=null){

//获取存储key名称

String key = redisRateLimit.storeKey();

//获取时间限制

long timeLimitLength = redisRateLimit.timeLimitLength();

//获取时间限制单位

TimeUnit timeLimitLengthUnit = redisRateLimit.timeLimitLengthUnit();

//时间单位最大访问数目

long max = redisRateLimit.max();

if(StringUtils.isBlank(key)){

key = method.getDeclaringClass().getSimpleName()+"."+method.getName();

}

HttpServletRequest request

= ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

key = key+":"+ IpUtil.getIpAddr(request);

//追加统一限流前缀

key =REDIS_RATE_LIMIT_KEY_PREFIX +key;

long now = System.currentTimeMillis();

//将2分钟转化为毫秒时间戳,以获得2分钟前时间

long limitTimeLengthMills =timeLimitLengthUnit.toMillis(timeLimitLength);

//应该移除的分值区间

long removeScore = now-limitTimeLengthMills;

Long r = stringRedisTemplate.execute(

limitRedisScript,

Lists.newArrayList(key),

"" + now,

"" + limitTimeLengthMills, //设置key的保存时间,该key在2分钟的允许时间内做zadd操作

"" + removeScore, //移除当前时间2分钟前过期的score

"" + max);

if(r!=null){

if(r==0){

log.error("【{}】在 "+timeLimitLength+formatTimeUnit(timeLimitLengthUnit)+" 内已达到访问上限,当前接口上限 {}", key, max);

throw new RuntimeException("手速太快了,慢点儿吧~");

}else{

log.info("【{}】在 "+timeLimitLength+formatTimeUnit(timeLimitLengthUnit)+" 内访问 {} 次", key, r);

}

}

}

}

private String formatTimeUnit(TimeUnit timeUnit){

if(timeUnit==TimeUnit.MINUTES){

return "分钟";

}else if(timeUnit==TimeUnit.SECONDS){

return "秒";

}else if(timeUnit==TimeUnit.HOURS){

return "小时";

}

return "illegal timeUnit args";

}

}

LUA脚本

local key = KEYS[1]

local now = tonumber(ARGV[1])

local limitTimeLengthMills = tonumber(ARGV[2])

local removeScore = tonumber(ARGV[3])

local max = tonumber(ARGV[4])

redis.call('ZREMRANGEBYSCORE',key,0,removeScore)

local current = tonumber(redis.call('ZCARD',key))

local next = current+1

if next>max then

return 0

else

redis.call('ZADD',key,now,now)

redis.call('PEXPIRE',key,limitTimeLengthMills)

return next

end

控制层

@Slf4j

@RestController

public class RedisLimitController {

@TokenNoCheck

@RedisRateLimiter

@GetMapping("/test1")

public R test1() {

log.info("【test1】被执行了。。。。。");

return R.ok("成功访问到api [1]~");

}

@TokenNoCheck

@RedisRateLimiter(max = 1,limitType = RedisRateLimiter.LimitType.IP,timeLimitLength = 1,timeLimitLengthUnit = TimeUnit.SECONDS)

@GetMapping("/test2")

public R test2() {

log.info("【test2】被执行了。。。。。");

return R.ok("成功访问到api [2]~");

}

@TokenNoCheck

@RedisRateLimiter(max = 5,limitType = RedisRateLimiter.LimitType.IP,timeLimitLength = 1,timeLimitLengthUnit = TimeUnit.MINUTES)

@GetMapping("/test3")

public R test3() {

log.info("【test3】被执行了。。。。。");

return R.ok("成功访问到api [3]~");

}

}

image.png

如果觉得《java redis令牌桶_接口限流令牌桶算法Redis分布式限流》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。