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