public class Buffer extends OutputStream {
private final byte[] scratch = new byte[1];
public synchronized void write(int v) throws IOException {
scratch[0] = (byte)v;
write(scratch, 0, 1);
}
/**Attempt to write a sequence of bytes to the collection buffer. This method will block if the spill thread is running and it cannot write. */
public synchronized void write(byte b[], int off, int len) throws IOException {
boolean buffull = false; // 緩衝區連續寫是否寫滿標誌
boolean wrap = false; // 緩衝區非連續情況下有足夠寫空間標誌
spillLock.lock();
try {
do {
// sufficient buffer space? 是否有足夠的緩衝區
// 連續的空間, 在還沒開始寫入len個字節到kvbuffer時, 要判斷如果寫入len個字節後, 會不會使緩衝區滿, 如果滿需要spill
// 1. bufstart=bufend<bufindex 正常寫入緩衝區, 由於還沒有開始spill, bufstart=bufend. 連續區間, 所以bufstart=bufend<bufindex
// 2. bufstart < bufend < bufindex 正在溢寫(bufstart<bufend=bufindex)的同時又有數據寫入緩衝區(bufstart<bufend<bufindex)
if (bufstart <= bufend && bufend <= bufindex) { // bufindex不會在bufend前面. 即bufindex不會寫完緩衝區後,再繞到bufend前面
buffull = bufindex + len > bufvoid; // 一有數據寫入kvbuffer,bufindex就後移. 判斷再寫入len個字節後,是否到達緩衝區尾部
wrap = (bufvoid - bufindex) + bufstart > len; // 緩衝區中非連續性區間包括兩部分, buindex剩餘的和bufstart之前的
} else { // 非連續的空間(中間部分爲即將寫入的數據): 基本條件爲bufindex < bufstart.
// bufindex <= bufstart <= bufend 寫入緩衝區尾部後不夠, 需要寫到緩衝區頭部bufindex<bufstart=bufend
// bufend <= bufindex <= bufstart 溢寫bufend=bufindex<bufstart,同時又有數據寫入bufend<bufindex<bufstart
wrap = false; // 非連續情況下總設置爲false,一旦緩衝區即將滿就必須溢寫. 這樣在複製到kvbuffer時,保證不會執行if(buffull)的邏輯
buffull = bufindex + len > bufstart; // bufindex現在已經重頭開始了,再寫入len,不能超過bufstart.如果超過則說明緩衝區寫滿了
}
if (kvstart == kvend) { // spill thread not running 溢寫線程還沒運行. 只要kvstart!=kvend,纔開始將緩衝區數據寫入磁盤
if (kvend != kvindex) { // we have records we can spill 還沒溢寫時, 沒有賦值kvend=kvindex. 數組kvoffsets中有記錄
// bufindex>bufend, 連續的空間, 判斷是否需要溢寫. [bufend,bufindex)之間的爲已經寫入的數據,判斷是否超過80%的限制
final boolean bufsoftlimit = (bufindex > bufend) ? bufindex - bufend > softBufferLimit
// 非連續空間分成兩段,bufvoid-bufend後面一段, bufindex爲前面一段. 兩者之和即爲已寫入的數據bufvoid-bufend+bufindex>limit
: bufend - bufindex < bufvoid - softBufferLimit;
if (bufsoftlimit || (buffull && !wrap)) {
LOG.info("Spilling map output: buffer full= " + bufsoftlimit);
startSpill(); // 緩衝區中已使用的內存利用率超過80%, 或者在寫入之前緩衝區已經滿了, 則立即spill
}
} else if (buffull && !wrap) { // kvoffsets數組中沒有記錄, 但是kvbuffer空間還是不夠當前寫入的數據
// We have no buffered records, and this record is too large to write into kvbuffer. We must spill it directly from collect
final int size = ((bufend <= bufindex) ? bufindex - bufend : (bufvoid - bufend) + bufindex) + len; // 已使用+即將寫入的len
bufstart = bufend = bufindex = bufmark = 0;
kvstart = kvend = kvindex = 0;
bufvoid = kvbuffer.length;
throw new MapBufferTooSmallException(size + " bytes");
}
}
if (buffull && !wrap) { // 如果空間不足同時spill在運行, 等待spillDone
try {
while (kvstart != kvend) {
reporter.progress();
spillDone.await();
}
} catch (InterruptedException e) {throw (IOException)new IOException("Buffer interrupted while waiting for the writer").initCause(e);}
}
} while (buffull && !wrap);
} finally {
spillLock.unlock();
}
// here, we know that we have sufficient space to write 緩衝區有足夠的空間用來寫入數據
// 前面判斷buffull有兩種情況即連續性和非連續性. 非連續性一定不會執行下面的if語句.
// 因爲非連續性設置wrap=false,如果緩衝區滿的話, 一定會執行spill溢寫.
// 否則如果非連續性執行下面的buffull邏輯. gaplen=bufvoid-bufindex就有問題.
if (buffull) {
final int gaplen = bufvoid - bufindex; // 寫到緩衝區尾部
System.arraycopy(b, off, kvbuffer, bufindex, gaplen);
len -= gaplen; // 剩餘部分len=len-gaplen會寫到緩衝區頭部
off += gaplen; // 要寫入數據的起始位置也要相應變化
bufindex = 0; // 從緩衝區頭部開始寫
}
System.arraycopy(b, off, kvbuffer, bufindex, len);
bufindex += len; // 寫入len個字節, 緩衝區的bufindex就後移len個字節
}
}
BlockBuffer.reset
protected synchronized void reset() throws IOException {
// spillLock unnecessary; If spill wraps, then
// bufindex < bufstart < bufend so contention is impossible
// a stale value for bufstart does not affect correctness, since
// we can only get false negatives that force the more conservative path
// 寫入key時,發生跨界現象. 即寫入某個key時,緩衝區尾部剩餘空間不足以容納整個key值,因此需要將key值分開存儲,其中一部分存到緩衝區末尾,->key上半部分①
// 另一部分存到緩衝區頭部->key下半部分②. 由於key是排序的關鍵字,需要保證連續性. 因此需要將跨界的key值重新存儲到緩衝區的頭部位置.
// 發生跨界的key在緩衝區末尾的長度=bufvoid-bufmark. 其中bufmark爲最後(上一次)寫入的一個完整的key/value的結束位置
int headbytelen = bufvoid - bufmark;
// 將尾部key插入到頭部之後, bufvoid要設置爲bufmark. 那麼bufvoid開始,長度爲headbytelen的就是key的尾部部分了
bufvoid = bufmark;
// 緩衝區前半段有足夠的空間容納整個key值. 即將尾部的key插入到頭部後, 不會使得這一個key超過bufstart
if (bufindex + headbytelen < bufstart) {
// bufindex爲當前緩衝區的位置. 不管寫入key或者value, bufindex都表示下一個可寫的初始位置
// [0, bufindex]在調整之前的頭部① -> [headbytelen, headbytelen+bufindex] 即將原先頭部後移headbytelen用來準備第二次拷貝
System.arraycopy(kvbuffer, 0, kvbuffer, headbytelen, bufindex);
// [bufvoid, bufvoid+headbytelen]調整之前的尾部② -> [0, headbytelen] 將尾部②插入到原先的頭部①
System.arraycopy(kvbuffer, bufvoid, kvbuffer, 0, headbytelen);
bufindex += headbytelen; // bufindex也要跟隨移動到原先頭部的下一個位置
} else { // 緩衝區前半段沒有足夠的空間容納整個key值. 在將key值移動到緩衝區開始位置時觸發一次spill操作
byte[] keytmp = new byte[bufindex]; // bufindex爲在未調整之前的緩衝區頭部
System.arraycopy(kvbuffer, 0, keytmp, 0, bufindex); // 將原先的頭部②複製到臨時緩衝區中
bufindex = 0; // 重置緩衝區
out.write(kvbuffer, bufmark, headbytelen); // 首先將[bufmark, bufmark+headbytelen]即尾部①寫入輸出流
out.write(keytmp);// 然後將緩衝區頭部,即key的下半部分也寫入輸出流
}
}