guava ratelimiter限流(2)

【订阅专栏合集,关注公众号,作者所有付费文章都能看(持续更新)】

  本篇是《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拦截接口限流。

代码下载地址:https://github.com/bigbirditedu/learn-ratelimit

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程猿薇茑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值