背景:
1.公司需要开发统一审计中台,基本功能是收集其他各系统的操作日志,基本原理是提供SDK嵌入至其他业务系统,通过AOP注解截取方法入参出参上传至收集接口。
难点:
1.SDK通过AOP拦截方法,日志上传通过RPC调用会产生一次网络IO,势必会增加被注解修饰的方法的执行时间。而且网络环境波动,审计平台宕机等各种不稳定因素,会对被嵌入的系统产生致命影响。所以SDK必须要做异步日志上传,先在内存中缓存日志,然后由定时线程池取缓存中的日志上传。
2.因为是并发插入,故内存缓存使用的数据结构必须是线程安全的,目前jdk中的所有线程安全的数据结构除了ConcurrentHashMap(ConcurrentHashMap不能并发读取,会导致日志上传速度太慢故不采用)进行了性能优化,其他都是加锁串行,这样会导致所有被AOP拦截的方法全部串行化,方法执行时间会大大增加,这对业务系统的影响同样是致命的,所以需要自行设计一个支持高性能并发插入读取的线程安全的数据结构。
设计思想:
1.插入采用类似ConcurrentHashMap的node数组机制降低加锁串行概率,提高插入速度。
2.采用双缓冲技术思想使插入和读取数据分离,使读写互不影响。
3.对读取线程做hash随机读取数据,以支持并发读取。
最终代码如下:
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author yangjq
* 双缓冲多队列并发读写,无序,不遵从先进先出原则
*/
public class ConcurrentDoubleBufferQueue {
/**
* 预估最大并发数
*/
private int predict;
/**
* 每个队列容量
*/
private int capacity;
/**
* 翻转间隔-毫秒
*/
private long turnTime;
/**
* 读空时翻转阈值
*/
private final int turnNum;
/**
* 默认预估最大并发数-200
*/
private static final int DEFAULT_PREDICT = 200;
/**
* 默认每个队列容量-2000000
*/
private static final int DEFAULT_CAPACITY = 2000000;
/**
* 默认翻转间隔-1秒
*/
private static final long DEFAULT_TURNTIME = 1000;
/**
* 默认读空时翻转阈值-100个
*/
private static final int DEFAULT_TURNNUM = 100;
private volatile ArrayList<Integer> dataIndexs;
private final ReentrantLock dataLock = new ReentrantLock();
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition takeCondition = takeLock.newCondition();
private volatile LinkedBlockingQueue<Object>[] writeArray;
private volatile LinkedBlockingQueue<Object>[] readArray;
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
public ConcurrentDoubleBufferQueue() {
this(DEFAULT_PREDICT, DEFAULT_CAPACITY, DEFAULT_TURNTIME, DEFAULT_TURNNUM);
}
/**
* @param predict 预估最大并发数
* @param capacity 每个队列容量
* @param turnTime 翻转间隔-毫秒
* @param turnNum 读空时翻转阈值
*/
public ConcurrentDoubleBufferQueue(int predict, int capacity, long turnTime, int turnNum) {
this.predict = predict;
this.capacity = capacity;
this.turnTime = turnTime;
this.turnNum = turnNum;
dataIndexs = new ArrayList<>();
writeArray = new LinkedBlockingQueue[predict];
for (int i = 0; i