【SpringBoot框架篇】10.API接口限流实战

文章目录

  • 简介
  • 限流算法
    • 漏桶算法
    • 令牌桶算法
  • 基于guava的RateLimiter实现
    • 常用方法
    • main函数版本
    • API接口限流实战
      • 引入依赖
      • 自定义注解
      • 自定义切面类
      • web接口
      • 压测
  • 基于Semaphore控制并发数
    • 常用方法
    • main函数版本
    • API接口限流实战
      • 引入依赖
      • 自定义注解
      • 自定义切面类
      • web接口
      • 初始化限流的许可证数量
      • 压测
  • 项目配套代码

简介

对接口限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

限流算法

常用的限流算法由:漏桶算法和令牌桶算法。

漏桶算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
【SpringBoot框架篇】10.API接口限流实战_第1张图片

令牌桶算法

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等

【SpringBoot框架篇】10.API接口限流实战_第2张图片

基于guava的RateLimiter实现

RateLimiter控制的是访问速率,RateLimiter是令牌桶算法的一种实现方式

常用方法

方法 描述
create(int permits) 创建每秒发放permits个令牌的桶
acquire() 不带参数表示获取一个令牌.如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0
acquire(int permits ) 获取permits 个令牌,.如果没有获取完令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0
tryAcquire() 尝试获取一个令牌,立即返回(非阻塞)
tryAcquire(int permits) 尝试获取permits 个令牌,立即返回(非阻塞)
tryAcquire(long timeout, TimeUnit unit) 尝试获取1个令牌,带超时时间
tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取permits个令牌,带超时时间

获取令牌方法源码如下

   @CanIgnoreReturnValue
    public double acquire() {
        return this.acquire(1);
    }

    @CanIgnoreReturnValue
    public double acquire(int permits) {
        long microsToWait = this.reserve(permits);
        this.stopwatch.sleepMicrosUninterruptibly(microsToWait);//会进行线程休眠
        return 1.0D * (double)microsToWait / (double)TimeUnit.SECONDS.toMicros(1L);
    }
    
    public boolean tryAcquire() {
        return this.tryAcquire(1, 0L, TimeUnit.MICROSECONDS);
    }

    public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
        long timeoutMicros = Math.max(unit.toMicros(timeout), 0L);
        checkPermits(permits);
        long microsToWait;
        synchronized(this.mutex()) {
            long nowMicros = this.stopwatch.readMicros();
            //无参的tryAcquire方法默认的超时时间设置是0,如果在这里没有立即获取到令牌,会直接返回获取令牌失败
            if (!this.canAcquire(nowMicros, timeoutMicros)) {
                return false;
            }

            microsToWait = this.reserveAndGetWaitLength(permits, nowMicros);
        }

        this.stopwatch.sleepMicrosUninterruptibly(microsToWait);
        return true;
    }

main函数版本

    public static void main(String[] args) {
        SimpleDateFormat sdf=new SimpleDateFormat("HH:mm:ss");
        long begin = System.currentTimeMillis();
        // 每秒允许发放1个令牌
        double permits=1.0;
        RateLimiter limiter = RateLimiter.create(permits);
        for (int i = 1; i <= 10; i++) {
            // 获取i个令牌, 当i超过permits会被阻塞
            double waitTime = limiter.acquire(i);
            System.out.println("curTime=" + sdf.format(new Date()) + " call index:" + i + " waitTime:" + waitTime);
        }
        long end =  System.currentTimeMillis();
        System.out.println("begin time:" + sdf.format(new Date(begin))+",end time:"+sdf.format(new Date(end))+",Total task time:"+(end-begin));
    }

测试结果如下
当i等于1的时候,直接获取到了令牌,当i大于1的时候会随着i的增长,获取令牌的等待时间也在增长
【SpringBoot框架篇】10.API接口限流实战_第3张图片

API接口限流实战

通关aop实现对接口的限流

引入依赖

		 
        
            com.google.guava
            guava
            25.1-jre
        
        
        
            org.aspectj
            aspectjweaver
            1.9.4
        

自定义注解

该注解主要用于AOP功能的切入,不需要属性

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
}

自定义切面类

首先通过RateLimiter.create(5.0);创建一个限流器,参数代表每秒生成5个令牌,通过limiter.acquire()来获取令牌,当然也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,如果超timeout为0或则调用无参的tryAcquire(),则代表非阻塞,获取不到立即返回,支持阻塞或可超时的令牌消费。

@Component
@Scope
@Aspect
public class RateLimitAspect {
    //每秒只发出3个令牌,内部采用令牌捅算法实现
    private static RateLimiter rateLimiter = RateLimiter.create(3.0);

    /**
     * 业务层切点
     */
    @Pointcut("@annotation(com.ljm.boot.ratelimit.limit.RateLimit)")
    public void ServiceAspect() { }

    @Around("ServiceAspect()")
    public Object around(ProceedingJoinPoint joinPoint) {
        Object obj = null;
        try {
            //tryAcquire()是非阻塞, rateLimiter.acquire()是阻塞的
            if (rateLimiter.tryAcquire()) {
                obj = joinPoint.proceed();
            } else {
                //拒绝了请求(服务降级)
                obj = "The system is busy, please visit after a while";
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return obj;
    }
}

web接口

@RestController
public class LimitTestController {

    @RateLimit
    @RequestMapping("/ratelimit")
    public String ratelimit() throws Exception{
     	//假设业务处理了1秒
        TimeUnit.SECONDS.sleep(1);
        return "success";
    }
}

压测

    public static void main(String[] args) throws Exception {
        ///设置线程池最大执行20个线程并发执行任务
        int threadSize = 20;
        //AtomicInteger通过CAS操作能保证统计数量的原子性
        AtomicInteger successCount = new AtomicInteger(0);
        CountDownLatch downLatch = new CountDownLatch(20);
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadSize);
        for (int i = 0; i < threadSize; i++) {
            fixedThreadPool.submit(() -> {
                RestTemplate restTemplate = new RestTemplate();
                String str = restTemplate.getForObject("http://localhost:8010/ratelimit", String.class);
                if ("success".equals(str)) {
                    successCount.incrementAndGet();
                }
                System.out.println(str);
                downLatch.countDown();
            });
        }
        //等待所有线程都执行完任务
        downLatch.await();
        fixedThreadPool.shutdown();
        System.out.println("总共有" + successCount.get() + "个线程获得到了令牌!");
    }

可以看到大部分请求直接被拒绝了,只有4个线程获取到了令牌
【SpringBoot框架篇】10.API接口限流实战_第4张图片

基于Semaphore控制并发数

Semaphore(信号量),是用来控制同时访问特定资源的线程数量,它通过计数来协调各个线程,以保证合理的使用公共资源。我的理解是:信号量控制着一个线程池中并发线程的数量。就好像我们去一家饭店吃饭,这家饭店最多可以同时供应50人,如果饭店中已经坐满50人,这时新来的客人就必须等待,直到有客人离开他们才可以进入,并且总的数量不可以超过50人。这里饭店就好比线程池,饭店里的服务人员和厨师就好比共享的资源,每个客人都相当于一个线程, semaphore就记录着里面的人数,要根据semaphore的数量来决定是否让新的客人进入。为了得到一个资源,每个线程都要先获取permit,以确保当前可以访问。

常用方法

方法 描述
acquire() 从许可集中请求获取一个许可,此时当前线程开始阻塞,直到获得一个可用许可,或者当前线程被中断。
acquire(int permits) 从许可集中请求获取指定个数(permits)的许可,此时当前线程开始阻塞,直到获得指定数据(permits)可用许可,或者当前线程被中断。
release() 释放一个许可,将其返回给许可集。
release(int permits) 释放指定个数(permits)许可,将其返回给许可集。
tryAcquire() 尝试获取一个可用许可,如果此时有一个可用的许可,则立即返回true,同时许可集中许可个数减一;如果此时许可集中无可用许可,则立即返回false。
tryAcquire(int permits) 尝试获取指定个数(permits)可用许可,如果此时有指定个数(permits)可用的许可,则立即返回true,同时许可集中许可个数减指定个数(permits);如果此时许可集中许可个数不足指定个数(permits),则立即返回false。
tryAcquire(long timeout, TimeUnit unit) 在给定的等待时间内,尝试获取一个可用许可,如果此时有一个可用的许可,则立即返回true,同时许可集中许可个数减一;如果此时许可集中无可用许可,当前线程阻塞,直至其它某些线程调用此Semaphore的release()方法并且当前线程是下一个被分配许可的线程,或者其它某些线程中断当前线程或者已超出指定的等待时间
tryAcquire(int permits, long timeout, TimeUnit unit) 在给定的等待时间内,尝试获取指定个数(permits)可用许可,如果此时有指定个数(permits)可用的许可,则立即返回true,同时许可集中许可个数减指定个数(permits);如果此时许可集中许可个数不足指定个数(permits),当前线程阻塞,直至其它某些线程调用此Semaphore的release()方法并且当前线程是下一个被分配许可的线程并且许可个数满足指定个数,或者其它某些线程中断当前线程,或者已超出指定的等待时间。

main函数版本

下面代码设置了20个线程并发执行任务,但是通过Semaphore 设置只允许5个并发的执行

public class SemaphoreTest {

    private final static Semaphore permit = new Semaphore(5);

    public static void main(String[] args) {
        //设置线程池最大执行20个线程并发执行任务
        int threadSize = 20;
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadSize);
        for (int i = 0; i < threadSize; i++) {
            fixedThreadPool.submit(() -> {
                try {
                    //获取令牌
                    permit.acquire();
                    Thread.sleep(1L);
                    //业务逻辑处理
                    System.out.println("处理任务的线程是" + Thread.currentThread().getId() + ",当前时间是" + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放令牌
                    permit.release();
                }
            });
        }
    }
}

执行结果
通过下图可以看到,每毫秒只有5个线程在执行任务
【SpringBoot框架篇】10.API接口限流实战_第5张图片

API接口限流实战

引入依赖

     
        
            org.aspectj
            aspectjweaver
            1.9.4
        
  public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
        public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

自定义注解

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SemaphoreLimit {

    String limitKey() default ""; //限流的方法名

    int value()  default 0;  //发放的许可证数量

}

自定义切面类

@Component
@Scope
@Aspect
public class SemaphoreLimitAspect {
    
    /**
     * 存储限流量和方法,必须是static且线程安全,保证所有线程进入都唯一
     */
    public static Map<String, Semaphore> semaphoreMap = new ConcurrentHashMap<>();

    /**
     * 业务层切点
     */
    @Pointcut("@annotation(com.ljm.boot.ratelimit.limit.SemaphoreLimit)")
    public void ServiceAspect() {

    }

    @Around("ServiceAspect()")
    public Object around(ProceedingJoinPoint joinPoint) {
        //获取目标对象
        Class<?> clz = joinPoint.getTarget().getClass();
        //获取增强方法信息
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        String limitKey = getLimitKey(clz, name);
        Semaphore semaphore = semaphoreMap.get(limitKey);
        //立即获取许可证,非阻塞
        boolean flag = semaphore.tryAcquire();
        Object obj = null;
        try {
            //拿到许可证则执行任务
            if (flag) {
                obj = joinPoint.proceed();
            } else {
                //拒绝了请求(服务降级)
                obj = "limitKey:"+limitKey+", The system is busy, please visit after a while";
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            if (flag) semaphore.release(); //拿到许可证后释放通行证
        }
        return obj;
    }

    /**
     * 获取拦截方法配置的限流key,没有返回null
     */
    private String getLimitKey(Class<?> clz, String methodName) {
        for (Method method : clz.getDeclaredMethods()) {
            //找出目标方法
            if (method.getName().equals(methodName)) {
                //判断是否是限流方法
                if (method.isAnnotationPresent(SemaphoreLimit.class)) {
                    return method.getAnnotation(SemaphoreLimit.class).limitKey();
                }
            }
        }
        return null;
    }
}

web接口

@RestController
public class LimitTestController {
	
	 /**
     * 设置limitKey=SemaphoreKey,并且许可证只有3个
     */
    @SemaphoreLimit(limitKey ="SemaphoreKey", value =3)
    @RequestMapping("/SemaphoreLimit")
    public String SemaphoreLimit() throws Exception{
        //假设业务处理了1秒
        TimeUnit.SECONDS.sleep(1);
        return "success";
    }
}

初始化限流的许可证数量

@Component
public class InitSemaphoreLimit implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, Object> beanMap = applicationContext.getBeansWithAnnotation(RestController.class);
        beanMap.forEach((k, v) -> {
            Class<?> controllerClass = v.getClass();
            System.out.println(controllerClass.toString());
            System.out.println(controllerClass.getSuperclass().toString());
            //获取所有声明的方法
            Method[] allMethods = controllerClass.getSuperclass().getDeclaredMethods();
            for (Method method : allMethods) {
                System.out.println(method.getName());
                //判断方法是否使用了限流注解
                if (method.isAnnotationPresent(SemaphoreLimit.class)) {
                    //获取配置的限流量,实际值可以动态获取,配置key,根据key从配置文件获取
                    int value = method.getAnnotation(SemaphoreLimit.class).value();
                    String key = method.getAnnotation(SemaphoreLimit.class).limitKey();
                    System.out.println("limitKey:" +key+",许可证数是"+value);
                    //key作为key.value为具体限流量,传递到切面的map中
                    SemaphoreLimitAspect.semaphoreMap.put(key, new Semaphore(value));
                }
            }
        });
    }
}

压测

public class SemaphoreWebTest {

    public static void main(String[] args) throws Exception {
        //设置线程池最大执行20个线程并发执行任务
        int threadSize=20;
        AtomicInteger successCount=new AtomicInteger(0);
        CountDownLatch downLatch=new CountDownLatch(20);
        ExecutorService fixedThreadPool= Executors.newFixedThreadPool(threadSize);
        for (int i = 0; i < threadSize; i++) {
            fixedThreadPool.submit(()->{
                RestTemplate restTemplate=new RestTemplate();
                String str= restTemplate.getForObject("http://localhost:8010/SemaphoreLimit",String.class);
                if("success".equals(str)){
                    successCount.incrementAndGet();
                }
                System.out.println(str);
                downLatch.countDown();
            });
        }
        downLatch.await();
        fixedThreadPool.shutdown();
        System.out.println("总共有"+successCount.get()+"个线程获得到了许可证!");
    }
}

测试结果如下图
因为我们在调用web接口时候线程休眠了1秒,所以20个线程并发处理任务的时候,只有3个获取到个许可证,
和我们预期的结果一致.
【SpringBoot框架篇】10.API接口限流实战_第6张图片

项目配套代码

github地址
要是觉得我写的对你有点帮助的话,麻烦在github上帮我点 Star

【SpringBoot框架篇】其它文章如下,后续会继续更新。

  • 1.搭建第一个springboot项目
  • 2.Thymeleaf模板引擎实战
  • 3.优化代码,让代码更简洁高效
  • 4.集成jta-atomikos实现分布式事务
  • 5.分布式锁的实现方式
  • 6.docker部署,并挂载配置文件到宿主机上面
  • 7.项目发布到生产环境
  • 8.搭建自己的spring-boot-starter
  • 9.dobbo入门实战
  • 10.API接口限流实战
  • 11.Spring Data Jpa实战
  • 12.使用druid的monitor工具查看sql执行性能
  • 13.使用springboot admin对springboot应用进行监控
  • 14.mybatis-plus实战

你可能感兴趣的:(springBoot)