原文地址:www.xilidou.com/2018/01/22/…
在高併發系統中,我們經常遇到這樣的需求:系統產生大量的請求,但是這些請求實時性要求不高。我們就可以將這些請求合併,達到一定數量我們統一提交。最大化的利用系統性IO,提升系統的吞吐效能。
所以請求合併框架需要考慮以下兩個需求:
- 當請求收集到一定數量時提交資料
- 一段時間後如果請求沒有達到指定的數量也進行提交
我們就聊聊一如何實現這樣一個需求。
閱讀這篇文章你將會了解到:
- ScheduledThreadPoolExecutor
- 阻塞佇列
- 執行緒安全的引數
- LockSuppor的使用
設計思路和實現
我們就聊一聊實現這個東西的具體思路是什麼。希望大家能夠學習到分析問題,設計模組的一些套路。
- 底層使用什麼資料結構來持有需要合併的請求?
- 既然我們的系統是在高併發的環境下使用,那我們肯定不能使用,普通的
ArrayList
來持有。我們可以使用阻塞佇列來持有需要合併的請求。 - 我們的資料結構需要提供一個 add() 的方法給外部,用於提交資料。當外部add資料以後,需要檢查佇列裡面的資料的個數是否達到我們限額?達到數量提交資料,不達到繼續等待。
- 資料結構還需要提供一個timeOut()的方法,外部有一個計時器定時呼叫這個timeOut方法,如果方法被呼叫,則直接向遠端提交資料。
- 條件滿足的時候執行緒執行提交動作,條件不滿足的時候執行緒應當暫停,等待佇列達到提交資料的條件。所以我們可以考慮使用
LockSuppor.park()
和LockSuppor.unpark
來暫停和啟用操作執行緒。
- 既然我們的系統是在高併發的環境下使用,那我們肯定不能使用,普通的
經過上面的分析,我們就有了這樣一個資料結構:
private static class FlushThread<Item> implements Runnable{
private final String name;
//佇列大小
private final int bufferSize;
//操作間隔
private int flushInterval;
//上一次提交的時間。
private volatile long lastFlushTime;
private volatile Thread writer;
//持有資料的阻塞佇列
private final BlockingQueue<Item> queue;
//達成條件後具體執行的方法
private final Processor<Item> processor;
//建構函式
public FlushThread(String name, int bufferSize, int flushInterval,int queueSize,Processor<Item> processor) {
this.name = name;
this.bufferSize = bufferSize;
this.flushInterval = flushInterval;
this.lastFlushTime = System.currentTimeMillis();
this.processor = processor;
this.queue = new ArrayBlockingQueue<>(queueSize);
}
//外部提交資料的方法
public boolean add(Item item){
boolean result = queue.offer(item);
flushOnDemand();
return result;
}
//提供給外部的超時方法
public void timeOut(){
//超過兩次提交超過時間間隔
if(System.currentTimeMillis() - lastFlushTime >= flushInterval){
start();
}
}
//解除執行緒的阻塞
private void start(){
LockSupport.unpark(writer);
}
//當前的資料是否大於提交的條件
private void flushOnDemand(){
if(queue.size() >= bufferSize){
start();
}
}
//執行提交資料的方法
public void flush(){
lastFlushTime = System.currentTimeMillis();
List<Item> temp = new ArrayList<>(bufferSize);
int size = queue.drainTo(temp,bufferSize);
if(size > 0){
try {
processor.process(temp);
}catch (Throwable e){
log.error("process error",e);
}
}
}
//根據資料的尺寸和時間間隔判斷是否提交
private boolean canFlush(){
return queue.size() > bufferSize || System.currentTimeMillis() - lastFlushTime > flushInterval;
}
@Override
public void run() {
writer = Thread.currentThread();
writer.setName(name);
while (!writer.isInterrupted()){
while (!canFlush()){
//如果執行緒沒有被打斷,且不達到執行的條件,則阻塞執行緒
LockSupport.park(this);
}
flush();
}
}
}
複製程式碼
- 如何實現定時提交呢?
通常我們遇到定時相關的需求,首先想到的應該是使用 ScheduledThreadPoolExecutor
定時來呼叫FlushThread 的 timeOut 方法,如果你想到的是 Thread.sleep()
...那需要再努力學習,多看原始碼了。
- 怎樣進一步的提升系統的吞吐量?
我們使用的FlushThread
實現了 Runnable
所以我們可以考慮使用執行緒池來持有多個FlushThread
。
所以我們就有這樣的程式碼:
public class Flusher<Item> {
private final FlushThread<Item>[] flushThreads;
private AtomicInteger index;
//防止多個執行緒同時執行。增加一個隨機數間隔
private static final Random r = new Random();
private static final int delta = 50;
private static ScheduledExecutorService TIMER = new ScheduledThreadPoolExecutor(1);
private static ExecutorService POOL = Executors.newCachedThreadPool();
public Flusher(String name,int bufferSiz,int flushInterval,int queueSize,int threads,Processor<Item> processor) {
this.flushThreads = new FlushThread[threads];
if(threads > 1){
index = new AtomicInteger();
}
for (int i = 0; i < threads; i++) {
final FlushThread<Item> flushThread = new FlushThread<Item>(name+ "-" + i,bufferSiz,flushInterval,queueSize,processor);
flushThreads[i] = flushThread;
POOL.submit(flushThread);
//定時呼叫 timeOut()方法。
TIMER.scheduleAtFixedRate(flushThread::timeOut, r.nextInt(delta), flushInterval, TimeUnit.MILLISECONDS);
}
}
// 對 index 取模,保證多執行緒都能被add
public boolean add(Item item){
int len = flushThreads.length;
if(len == 1){
return flushThreads[0].add(item);
}
int mod = index.incrementAndGet() % len;
return flushThreads[mod].add(item);
}
//上文已經描述
private static class FlushThread<Item> implements Runnable{
...省略
}
}
複製程式碼
- 面向介面程式設計,提升系統擴充套件性:
public interface Processor<T> {
void process(List<T> list);
}
複製程式碼
使用
我們寫個測試方法測試一下:
//實現 Processor 將 String 全部輸出
public class PrintOutProcessor implements Processor<String>{
@Override
public void process(List<String> list) {
System.out.println("start flush");
list.forEach(System.out::println);
System.out.println("end flush");
}
}
複製程式碼
public class Test {
public static void main(String[] args) throws InterruptedException {
Flusher<String> stringFlusher = new Flusher<>("test",5,1000,30,1,new PrintOutProcessor());
int index = 1;
while (true){
stringFlusher.add(String.valueOf(index++));
Thread.sleep(1000);
}
}
}
複製程式碼
執行的結果:
start flush
1
2
3
end flush
start flush
4
5
6
7
end flush
複製程式碼
我們發現並沒有達到5個數字就觸發了flush。因為觸發了超時提交,雖然還沒有達到規定的5 個資料,但還是執行了 flush。
如果我們去除 Thread.sleep(1000);
再看看結果:
start flush
1
2
3
4
5
end flush
start flush
6
7
8
9
10
end flush
複製程式碼
每5個數一次提交。完美。。。。
總結
一個比較生動的例子給大家講解了一些多執行緒的具體運用。學習多執行緒應該多思考多動手,才會有比較好的效果。希望這篇文章大家讀完以後有所收穫,歡迎交流。
github地址:github.com/diaozxin007…
徒手擼框架系列文章地址: