使用令牌桶算法解决调用第三方接口限流问题

文章介绍了如何使用令牌桶算法解决第三方接口限流问题。在单体系统中,通过自定义TokenLimiter类,结合ArrayBlockingQueue和线程池实现限流。在分布式系统中,利用Redis作为共享存储,通过RedisLimiter组件协调多个服务的限流。代码示例展示了如何控制每秒并发数,并处理获取令牌失败时的重试机制。
摘要由CSDN通过智能技术生成

我们在调用第三方接口时常常会碰到接口限流问题,为了解决这一问题,大家想出了许多方法。我这里介绍一下我的方法,第三方接口限流一般是基于令牌桶算法的,那么我们可以以彼之道还治彼身,使用令牌桶算法实现我方调用接口的限流。下面我会演示单体系统和分布式系统中使用令牌桶算法的代码。

一、单体项目

(一)、令牌桶

package com.haiwang.hotel.utils;

import lombok.extern.log4j.Log4j2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;

@Log4j2
public class TokenLimiter {


    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");

    private ScheduledFuture<?> future;

    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    /**
     * 令牌
     */
    private static final String TOKEN = "token";

    /**
     * 阻塞队列,线程安全,非公平容量有限
     */
    private ArrayBlockingQueue<String> arrayBlockingQueue;

    /**
     * 令牌桶容量
     */
    private int limit;

    /**
     * 令牌每次生成的数量
     */
    private int amount;

    /**
     * 令牌桶间隔时间——单位时间
     */
    private int period;

    public TokenLimiter(int limit, int period, int amount) {
        this.limit = limit;
        this.amount = amount;
        this.period = period;
        arrayBlockingQueue = new ArrayBlockingQueue<>(limit);
        init();
    }

    /**
     * 初始化生成令牌,按桶容量生成
     */
    private void init() {
        for (int i = 0; i < limit; i++) {
            arrayBlockingQueue.offer(TOKEN);
        }
    }
    /**
     * 关闭线程
     *
     */
    public void stop() {
        executor.shutdown();
        future.cancel(true);

    }

    /**
     * 启动添加线程
     * @param lock
     */
    public void start(Object lock) {


        this.future = this.executor.scheduleWithFixedDelay(() -> {
            synchronized (lock) {
                // 往令牌桶添加令牌
                addToken();
                lock.notifyAll();
            }
        }, this.period, period, TimeUnit.MILLISECONDS);

    }


    /**
     * 添加令牌
     */
    private void addToken() {
        for (int i = 0; i < this.amount; i++) {
            // 溢出返回false
            arrayBlockingQueue.offer(TOKEN);
            log.info("添加令牌");
        }
    }

    public boolean tryAcquire() {
        // 队首元素出队
        return arrayBlockingQueue.poll() != null;
    }

    @Override
    public String toString() {
        return "TokenLimiter{" +
                "arrayBlockingQueue=" + arrayBlockingQueue +
                ", limit=" + limit +
                ", amount=" + amount +
                ", period=" + period +
                '}';
    }
}

(二)、代码实操

线程池配置

@Configuration
public class ThreadPoolConfig {
    // 获取服务器的cpu个数
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();// 获取cpu个数
    private static final int COUR_SIZE = CPU_COUNT * 4;
    private static final int MAX_COUR_SIZE = CPU_COUNT * 8;

    // 接下来配置一个bean,配置线程池。
    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(COUR_SIZE);// 设置核心线程数
        threadPoolTaskExecutor.setMaxPoolSize(MAX_COUR_SIZE);// 配置最大线程数
        threadPoolTaskExecutor.setQueueCapacity(MAX_COUR_SIZE * 4);// 配置队列容量(这里设置成最大线程数的四倍)
        threadPoolTaskExecutor.setThreadNamePrefix("thirdParty-thread");// 给线程池设置名称
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());// 设置任务的拒绝策略
        return threadPoolTaskExecutor;
    }

}
    //规定每秒并发数为3
    @ApiOperation(value = "测试锁")
    @GetMapping("/testLock")
    public Result<?> testLock() {



        //定义好信息阻塞队列
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue(10);
        arrayBlockingQueue.add("A1");
        arrayBlockingQueue.add("A2");
        arrayBlockingQueue.add("A3");
        arrayBlockingQueue.add("A4");
        arrayBlockingQueue.add("A5");
        arrayBlockingQueue.add("A6");
        arrayBlockingQueue.add("A7");
        arrayBlockingQueue.add("A8");
        arrayBlockingQueue.add("A9");
        arrayBlockingQueue.add("A10");



        //定义好令牌桶
        TokenLimiter limiter = new TokenLimiter(3, 1000, 3);
        // 生产令牌
        limiter.start(LOCK);
        int size = arrayBlockingQueue.size();
        final CountDownLatch latch = new CountDownLatch(size);

        //创建线程并进行推送
        for (int i = 0; i < size; i++) {
            threadPoolTaskExecutor.execute(() -> {
                String msg = arrayBlockingQueue.poll();
                if(limiter.tryAcquire()){
                    log.info("推送信息{},当前线程名称为:{},当前时间为:{},",msg,Thread.currentThread().getName(),sdf.format(new Date()));
                    latch.countDown();
                }else {
                    boolean flag = false;
                    //重试五次,成功就推送,失败就不推送
                    for (int j = 0; j < 4; j++){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            log.error("线程中断错误");
                        }
                        if(limiter.tryAcquire()){

                            log.info("推送信息{},当前线程名称为:{},当前时间为:{}",msg,Thread.currentThread().getName(),sdf.format(new Date()));
                            latch.countDown();
                            flag = true;
                            break;
                        }
                    }
                    if(!flag){
                        log.info("线程繁忙,无法进行业务,当前线程名称为:{},推送信息{}",Thread.currentThread().getName(),msg);
                    }
                }

            });
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        limiter.stop();
        return Result.success(1);
    }

整体的代码代码逻辑是拿到信息,并将信息用不同的线程推送到第三方平台,推送前会检查是否拿到令牌,没有拿到令牌就重试5次并休眠一秒钟。下面是运行的结果,确实做到了每秒调用三次,最大程度利用了服务器和带宽资源。 

二、微服务项目

微服务项目中就需要利用redis实现令牌桶了,因为不同的服务在不同的服务器上,利用单体项目的写法无法实现调用限流。

(一)、令牌桶

package com.haiwang.hotel.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RedisLimiter {




    @Autowired
    private StringRedisTemplate  redisTemplate;


    /**
     *
     * @param point 标识符用来区别接口或者方法
     * @param limit 限制访问的次数
     * @param timeout 多长时间内限制访问的次数
     * @return
     */

    public synchronized  String acquireToken(String point,
                               Integer limit,
                               Long timeout) {


            try {
                // 令牌值
                String token = "token";
                // 无效的限流值 返回token
                if(limit<=0||timeout<=0){
                    return token;
                }
                String maxCountKey = "BUCKET:MAX_COUNT:" + point;
                String currCountKey = "BUCKET:CURR_COUNT:" + point;

                String  maxCount = redisTemplate.opsForValue().get(maxCountKey);
                String  currCount = redisTemplate.opsForValue().get(currCountKey);

                if(ObjectUtils.isEmpty(maxCount)){
                    // 初始计数为1
                    redisTemplate.opsForValue().set(currCountKey, "1", timeout, TimeUnit.MILLISECONDS);
                    // 总数
                    redisTemplate.opsForValue().set(maxCountKey, limit.toString(), timeout, TimeUnit.MILLISECONDS);
                    return token;
                } else if (ObjectUtils.isNotEmpty(maxCount)&&ObjectUtils.isNotEmpty(currCount)){
                    // 判断是否超过限制
                    if(Integer.valueOf(currCount)<Integer.valueOf(maxCount)){
                        // 计数加1
                        redisTemplate.opsForValue().set(currCountKey, String.valueOf(Integer.valueOf(currCount)+1), timeout, TimeUnit.MILLISECONDS);
                        return token;
                    }
                } else {
                    // currCount变量先失效(几乎不可能) 返回token
                    return token;
                }



            }catch (Exception e) {
                log.error("限流出错,请检查Redis运行状态:{}",e.toString());
            }

            return null;
    }





}

    //规定每秒并发数为3
    @ApiOperation(value = "测试分布式锁")
    @GetMapping("/testDistributedLock")
    public Result<?> testDistributedLock() {
        //定义信息阻塞队列
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue(10);
        arrayBlockingQueue.add("A1");
        arrayBlockingQueue.add("A2");
        arrayBlockingQueue.add("A3");
        arrayBlockingQueue.add("A4");
        arrayBlockingQueue.add("A5");
        arrayBlockingQueue.add("A6");
        arrayBlockingQueue.add("A7");
        arrayBlockingQueue.add("A8");
        arrayBlockingQueue.add("A9");
        arrayBlockingQueue.add("A10");
        int size = arrayBlockingQueue.size();
        for (int i = 0; i < size; i++) {
            threadPoolTaskExecutor.execute(() -> {
                String msg = arrayBlockingQueue.poll();
                if (StringUtils.isNotBlank(redisLimiter.acquireToken("DISTRIBUTED_LOCKS", 3, 1000L))) {
                    log.info("推送信息{},当前线程名称为:{},当前时间为:{}", msg, Thread.currentThread().getName(), sdf.format(new Date()));
                } else {
                    boolean flag = false;
                    //重试五次,成功就推送,失败就不推送
                    for (int j = 0; j < 4; j++) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            log.error("线程中断错误");
                        }
                        if (StringUtils.isNotBlank(redisLimiter.acquireToken("DISTRIBUTED_LOCKS", 3, 1000L))) {
                            log.info("推送信息{},当前线程名称为:{},当前时间为:{}", msg, Thread.currentThread().getName(), sdf.format(new Date()));
                            flag = true;
                            break;
                        }
                    }
                    if (!flag) {
                        log.info("线程繁忙,无法进行业务,当前线程名称为:{},信息{}", Thread.currentThread().getName(), msg);
                    }
                }
            });
        }

        return Result.success(1);
    }

上面代码的实现逻辑和单体项目差不多,只不过获取令牌的地方由jvm转向了redis。拿到了锁便可以执行业务代码,没拿到就等待并重试,下面是代码实现效果。

参考文章:

利用Redis进行分布式限流,实现令牌桶算法_redis实现令牌桶算法_LuciferCoder的博客-CSDN博客

分布式限流实战--redis实现令牌桶限流_redis 令牌桶_田培融的博客-CSDN博客

Java 简单实现令牌桶_java 令牌桶_Ang Ga Ga的博客-CSDN博客

synchronized 关键字,代表这个方法加锁_给方法加锁_BUG--ER的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小海海不怕困难

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

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

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

打赏作者

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

抵扣说明:

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

余额充值