最近在做消息发送压测, 发现BlockingQueue会在堆积200w数据时卡死服务.
前情提要:
用BlockingQueue是为了做限速发送, 对方的接口每个账号只能每秒50进行发送, 超过会报错. 我们这里每个用户开放的200于是有差速就做了限速队列.
之前压测没发现这个问题, 是因为多个账号同时发消费也快. 没顶到单个队列200w. 这次用同一个账号放开限速压极限堆积量发现了这个问题.
之前用的BlockingQueue的put和take多线程生产和消费. 为了确定是不是某个特定版本java8导致的问题, 测了几台服务器上不同java8的微分版本都相同的问题, 被逼无奈自己写限速队列了.
用了线程安全的ConcurrentLinkedQueue, 自己加锁. 目前没设置队列上限, 后面看情况加个队列数量上限控制. 超过数量直接阻塞等待消费.
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/**
* 双队列先进先出redis速率控制器
*/
@Slf4j
@Data
@Component
public class LRURedisQueue<T> {
/**
* 令牌桶redisKey
*/
private static final String RateLimiterKey = "RateLimiter:%s";
/**
* 初始化标识
*/
private boolean isInit = false;
/**
* 发送速度 每秒speed
*/
private int speed = 50;
/**
* 线程数量
*/
private int threadCount = 32;
/**
* 最大值
*/
private int maxSize = 1024 * 1024 * 10;
/**
* 数据列表1
*/
private ConcurrentLinkedQueue<Package<T>> list = null;
/**
* 线程
*/
private final List<Thread> threadList = new ArrayList<>();
/**
* 锁条件
*/
private Condition notEmpty;
/**
* 锁
*/
private ReentrantLock lock;
/**
* 执行方法
*/
private Function<T, Void> func;
/**
* redis限速令牌生成器
*/
private RRateLimiter rRateLimiter;
/**
* 增加速度
*/
public void addSpeed(){
if(rRateLimiter == null){
log.info("队列未初始化");
return;
}
speed += speed * 0.1;
rRateLimiter.setRate(RateType.OVERALL, speed, 1, RateIntervalUnit.SECONDS);
}
/**
* 减慢速度
*/
public void subSpeed(){
if(rRateLimiter == null){
log.info("队列未初始化");
return;
}
speed *= 0.9;
rRateLimiter.setRate(RateType.OVERALL, speed, 1, RateIntervalUnit.SECONDS);
}
/**
* 推入队列
*/
public void push(T t, int count) {
log.info("push");
if (!ObjectUtil.isEmpty(t)) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
list.offer(Package.<T>builder().item(t).count(count).build());
notEmpty.signal();
log.info("队列数量: {}", list.size());
} catch (Exception e) {
log.info("推入队列失败: {}, {}", e.getMessage(), e);
} finally {
lock.unlock();
}
}
}
/**
* 初始化
*/
public void init(Function<T, Void> func, RedissonClient redis, String key, int concurrent) throws Exception {
log.info("初始化线程队列");
if (func == null) {
throw new Exception("线程调用方法为空");
}
lock = new ReentrantLock();
notEmpty = lock.newCondition();
this.func = func;
// 先销毁旧的线程
stop();
// 获取令牌桶
speed = concurrent;
rRateLimiter = redis.getRateLimiter(String.format(RateLimiterKey, key));
rRateLimiter.setRate(RateType.OVERALL, speed, 1, RateIntervalUnit.SECONDS);
// 清空列表
if (list != null) {
list.clear();
}
// 创建新列表
list = new ConcurrentLinkedQueue<>();
// 创建新线程
for (int i = 0; i < threadCount; i++) {
int finalI = i;
Thread thread = new Thread(()->{
while (true) {
try {
// 加锁(可中断)
lock.lockInterruptibly();
log.info("pop" + finalI + "等待消费1");
Package<T> pack;
while ((pack = list.poll()) == null) {
log.info("等待获取" + finalI);
notEmpty.await();
}
// 解锁
lock.unlock();
rRateLimiter.acquire(1);
if (pack.count > 1) {
rRateLimiter.acquire(pack.count - 1);
}
func.apply(pack.item);
log.info("pop" + finalI + "消费完成");
} catch (Exception e) {
log.error("线程队列线程报错: {}, {}", e.getMessage(), e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
});
thread.start();
threadList.add(thread);
}
this.isInit = true;
}
/**
* 销毁所有线程
*/
public void stop() {
for (Thread thread : threadList) {
try {
if (thread != null) {
thread.stop();
}
} catch (Exception e) {
log.info("主动关闭线程报错: {}, {}", e.getMessage(), e);
}
}
}
}
/**
* 封装类
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
class Package<T> {
T item;
int count;
}