資料庫系統一般採用wal(write ahead log)技術來實現原子性和永續性,mysql也不例外。wal中記錄事務的更新內容,通過wal將隨機的髒頁寫入變成順序的日誌刷盤,可極大提公升資料庫寫入效能,因此,wal的寫入能力決定了資料庫整體效能的上限,尤其是在高併發時。
在mysql 8以前,寫日誌被保護在一把大鎖之下,本來並行事務日誌寫入被人為序列化處理。雖簡化了邏輯,但也極大限制了整體的效能表現。8.0很大的一部分工作便是將日誌系統並行化。
日誌並行化的思路也很簡單:將寫日誌拆分為兩個過程:
從記憶體log buffer中為日誌預留空間
2. 將日誌內容拷貝至1預留的空間
而在這兩個步驟中,只需要步驟1保證在多併發併發預留空間時的正確性即可,確保併發執行緒預留的日誌空間不會交叉。一旦預留成功,步驟2各併發執行緒可互不干擾地執行拷貝至自己的預留空間即可,這天然可併發。
而在步驟1中也可以使用原子變數來代替代價較高鎖實行預留,在mysql 8實現中,其實就兩行**:
log_handle log_buffer_reserve(log_t &log, size_t len)
可以看到,只需要乙個原子變數log.sn記錄當前分配的位置資訊,下次分配時更新該log.sn即可,非常簡潔優雅。
8.0中引入的並行日誌系統雖然很美好,但是也會帶來一些小麻煩,我們下面會詳細描述其引入的日誌空洞問題並闡述其解決方案。
mysql 8.0中使用了無鎖預分配的方式可以使mtr並行地將wal日誌寫入到log buffer,提公升效能。但這樣勢必會帶來redo log buffer的空洞問題,如下:
上圖中,3個執行緒分別分配了對應的redo buffer,執行緒1和3已經完成了wal日誌內容的拷貝,而執行緒2則還在拷貝中,此時寫入執行緒最多只能將thread-1的redo log寫入日誌檔案。 為此,mysql 8.0中引入了link_buf。
link_buf用於輔助表示其他資料結構的使用情況,在link_buf中,如果乙個索引位置index處儲存的是非0值n,則表示link_buf輔助標記的那個資料結構,從index開始後面n個元素已被占用。
template class link_buf ;
link_buf是乙個定長陣列,且保證陣列的每個元素的更新是原子操作的。以環形的方式復用已經釋放的空間。
同時link_buf內部維護了乙個變數m_tail表示當前最大可達的lsn。
innodb日誌系統中為log buffer維護了兩個link_buf型別的變數recent_written和recent_closed。示意圖如下:
上圖中,共有兩處日誌空洞,起始的lsn為lsn1與lsn3,均有4個位元組。而lsn2處的redo log已經寫入,共3個位元組。在recent_written中,lsn1開始處的4個atomic均是0,lsn3同樣如此,而lsn2處開始的儲存的則是3,0,0表示從該位置起的3個位元組已經成功寫入了redo日誌。
接下來當lsn1處的空洞被填充後,link_buf中該處對應的內容就會被設定,如下:
同理,當lsn3處的空洞也被填充後,狀態變成下面這樣:
初始化
bool log_sys_init(...)
constexpr ulong innodb_log_recent_written_size_default = 1024 * 1024;
ulong srv_log_recent_written_size = innodb_log_recent_written_size_default;
static void log_allocate_recent_written(log_t &log) ;}
// link_buf構造
template link_buf::link_buf(size_t capacity)
: m_capacity(capacity), m_tail(0)
}
從建構函式中可以看到,linkbuf核心心成員是一維陣列,陣列的成員型別是原子型別的distance(uint64_t),陣列成員個數則由建立者決定,如innodb中為recent_written建立的linkbuf的陣列成員個數為1mb,而為recent_closed建立的linkbuf的陣列成員個數為2mb。
同時,建立完成後會將陣列的每個成員初始化為0。
mtr在commit時會將其執行時產生的所有redo log拷貝至innodb全域性的redo log buffer,這借助了mtr_write_log_t物件來完成,且每次拷貝按照block為單位進行。需要說明的是:乙個mtr中可能存在多個block來儲存mtr執行時產生的redo log,每個block拷貝完成後均觸發一次link_buf的更新。
struct mtr_write_log_t }
void log_buffer_write_completed(log_t &log, const log_handle &handle,
lsn_t start_lsn, lsn_t end_lsn)
template inline size_t link_buf::slot_index(position position) const
template inline void link_buf::add_link(position from, position to)
在這裡會找到start_lsn對應的slot,並在該slot內設定值為end_lsn - start_lsn,記錄該位置處已寫入的內容數量。
log_advance_ready_for_write_lsn
innodb將redo log buffer內容寫入日誌檔案時需要保證不能存在空洞,即在寫入前需要獲得當前最大的無空洞lsn。這同樣依賴linkbuf。在後台寫日誌執行緒log_writer的log_advance_ready_for_write_lsn函式中完成。
void log_writer(log_t *log_ptr) }
bool log_advance_ready_for_write_lsn(log_t &log) ;
const lsn_t previous_lsn = log_buffer_ready_for_write_lsn(log);
if (log.recent_written.advance_tail_until(stop_condition)) else
}
這裡的關鍵在於函式link_buf::advance_tail_until,即推進link_buf::m_tail。
bool link_buf::next_position(position position, position &next)
bool link_buf::advance_tail_until(stop_condition stop_condition)
/* **slot */
claim_position(position);
position = next;
} if (position > m_tail.load()) else
}
這裡的原理也比較簡單,可以用下面的圖來表示:
簡單來說,就是從上次尾部位置(m_tail)開始,順序遍歷陣列,如果該項不為0,則推進m_tail,否則意味著出現了空洞,就不能再往下推進了。
mySQL無鎖佇列 go 無鎖佇列
無鎖佇列適用場景 兩個執行緒之間的互動資料,乙個執行緒生產資料,另外乙個執行緒消費資料,效率高 缺點 需要使用固定分配的空間,不能動態增加 減少長度,存在空間浪費和無法擴充套件空間問題 package main import fmt reflect strings time type loopque...
無鎖環形佇列
環形一讀一寫佇列中,不需要擔心unsigned long溢位問題,因為溢位後自動回歸,相減值還會保留。示例一 注 max count 必須為 2 的指數,即 2,4,8,16.佇列尺寸 define max count 4096 define max mask 4095 max count 1 變數...
KFIFO無鎖佇列
linux核心中實現了以非常漂亮的無鎖佇列,在只有乙個讀者和乙個寫者的情況下,無需上鎖,而擁有執行緒安全的特性,使得效能相比於加鎖方式實現的佇列提公升數倍 kfifo的分析可見 這位作者講的很清楚 kifio可以實現無鎖佇列,但是為什麼可以實現呢,通過這種方式為什麼可以執行緒安全?換句話說,平時實現...