日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747


MySQL無鎖化WAL系統那些事兒

 

概述

數據庫系統一般采用WAL(write ahead log)技術來實現原子性和持久性,MySQL也不例外。WAL中記錄事務的更新內容,通過WAL將隨機的臟頁寫入變成順序的日志刷盤,可極大提升數據庫寫入性能,因此,WAL的寫入能力決定了數據庫整體性能的上限,尤其是在高并發時。

在MYSQL 8以前,寫日志被保護在一把大鎖之下,本來并行事務日志寫入被人為串行化處理。雖簡化了邏輯,但也極大限制了整體的性能表現。8.0很大的一部分工作便是將日志系統并行化。

日志并行化

日志并行化的思路也很簡單:將寫日志拆分為兩個過程:

  1. 從內存log buffer中為日志預留空間

2. 將日志內容拷貝至1預留的空間

而在這兩個步驟中,只需要步驟1保證在多并發并發預留空間時的正確性即可,確保并發線程預留的日志空間不會交叉。一旦預留成功,步驟2各并發線程可互不干擾地執行拷貝至自己的預留空間即可,這天然可并發。

而在步驟1中也可以使用原子變量來代替代價較高鎖實行預留,在mysql 8實現中,其實就兩行代碼:

Log_handle log_buffer_reserve(log_t &log, size_t len) {    ...    const sn_t start_sn = log.sn.fetch_add(len);    const sn_t end_sn = start_sn + len;    ...}

可以看到,只需要一個原子變量log.sn記錄當前分配的位置信息,下次分配時更新該log.sn即可,非常簡潔優雅。

8.0中引入的并行日志系統雖然很美好,但是也會帶來一些小麻煩,我們下面會詳細描述其引入的日志空洞問題并闡述其解決方案。

Log Buffer空洞問題

Mysql 8.0中使用了無鎖預分配的方式可以使MTR并行地將WAL日志寫入到Log Buffer,提升性能。但這樣勢必會帶來Redo Log Buffer的空洞問題,如下:

MySQL無鎖化WAL系統那些事兒

 

上圖中,3個線程分別分配了對應的redo buffer,線程1和3已經完成了wal日志內容的拷貝,而線程2則還在拷貝中,此時寫入線程最多只能將thread-1的redo log寫入日志文件。 為此,MySQL 8.0中引入了 Link_buf 。

Link_buf原理

Link_buf用于輔助表示其他數據結構的使用情況,在Link_buf中,如果一個索引位置index處存儲的是非0值n,則表示Link_buf輔助標記的那個數據結構,從index開始后面n個元素已被占用。

template <typename Position = uint64_t>class Link_buf { private:  ...  size_t m_capacity;  std::atomic<Distance> *m_links;  alignas(INNOBASE_CACHE_LINE_SIZE) std::atomic<Position> m_tail;};

Link_buf是一個定長數組,且保證數組的每個元素的更新是原子操作的。以環形的方式復用已經釋放的空間。

同時Link_buf內部維護了一個變量 m_tail 表示當前最大可達的LSN。

Innodb日志系統中為Log Buffer維護了兩個Link_buf類型的變量 recent_written 和 recent_closed 。示意圖如下:

MySQL無鎖化WAL系統那些事兒

 

上圖中,共有兩處日志空洞,起始的LSN為lsn1與lsn3,均有4個字節。而lsn2處的redo log已經寫入,共3個字節。在 recent_written 中,lsn1開始處的4個atomic均是0,lsn3同樣如此,而lsn2處開始的存儲的則是3,0,0表示從該位置起的3個字節已經成功寫入了redo日志。

接下來當lsn1處的空洞被填充后,Link_buf中該處對應的內容就會被設置,如下:

MySQL無鎖化WAL系統那些事兒

 

同理,當lsn3處的空洞也被填充后,狀態變成下面這樣:

MySQL無鎖化WAL系統那些事兒

 

Link_buf實現

初始化

bool log_sys_init(...){    ...    log_allocate_recent_written(log);    ...}?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) {  // 默認值為1MB  log.recent_written = Link_buf<lsn_t>{srv_log_recent_written_size};}?// Link_buf構造template <typename Position>Link_buf<Position>::Link_buf(size_t capacity)    : m_capacity(capacity), m_tail(0){  ...  m_links = UT_NEW_ARRAY_NOKEY(std::atomic<Distance>, capacity);  for (size_t i = 0; i < capacity; ++i) {    m_links[i].store(0);  }}

從構造函數中可以看到,LinkBuf內核心成員是一維數組,數組的成員類型是原子類型的Distance(uint64_t),數組成員個數則由創建者決定,如Innodb中為recent_written創建的LinkBuf的數組成員個數為1MB,而為recent_closed創建的LinkBuf的數組成員個數為2MB。

同時,創建完成后會將數組的每個成員初始化為0。

mtr log拷貝完成

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 {  bool operator()(const mtr_buf_t::block_t *block) {    ...    // 拷貝完成后觸發LinkBuf更新    log_buffer_write_completed(*log_sys, m_handle, start_lsn, end_lsn);  }}?void log_buffer_write_completed(log_t &log, const Log_handle &handle,                                lsn_t start_lsn, lsn_t end_lsn) {  ...  // 更新本次寫入的內容范圍對應的LinkBuf內特定的數組項值  log.recent_written.add_link(start_lsn, end_lsn);}?template <typename Position>inline size_t Link_buf<Position>::slot_index(Position position) const {  return position & (m_capacity - 1);}?template <typename Position>inline void Link_buf<Position>::add_link(Position from, Position to) {  // 定位本次寫入的內容范圍所在數組項index  // 算法是將起始lsn(@from)對數組容量取模,即from % capacity  const auto index = slot_index(from);  auto &slot = m_links[index];  slot.store(to - from);}

在這里會找到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) {    ...    for (uint64_t step = 0;; ++step) {      (void)log_advance_ready_for_write_lsn(log);    }}?bool log_advance_ready_for_write_lsn(log_t &log) {  const lsn_t write_lsn = log.write_lsn.load();  const auto write_max_size = srv_log_write_max_size;?  auto stop_condition = [&](lsn_t prev_lsn, lsn_t next_lsn) {    return (next_lsn - write_lsn >= write_max_size);  };  const lsn_t previous_lsn = log_buffer_ready_for_write_lsn(log);?  if (log.recent_written.advance_tail_until(stop_condition)) {    const lsn_t previous_lsn = log_buffer_ready_for_write_lsn(log);    return (true);  } else {    return (false);  }}

這里的關鍵在于函數 Link_buf::advance_tail_until ,即推進Link_buf::m_tail。

bool Link_buf<Position>::next_position(Position position, Position &next) {  const auto index = slot_index(position);  auto &slot = m_links[index];  const auto distance = slot.load();  next = position + distance;  return distance == 0;}?bool Link_buf<Position>::advance_tail_until(Stop_condition stop_condition) {  auto position = m_tail.load();  while (true) {    Position next;    bool stop = next_position(position, next);    if (stop || stop_condition(position, next)) {      break;    }    /* 回收slot */    claim_position(position);    position = next;  }  if (position > m_tail.load()) {    m_tail.store(position);    return true;  } else {    return false;  }}

這里的原理也比較簡單,可以用下面的圖來表示:

MySQL無鎖化WAL系統那些事兒

 

簡單來說,就是從上次尾部位置(m_tail)開始,順序遍歷數組,如果該項不為0,則推進m_tail,否則意味著出現了空洞,就不能再往下推進了。

分享到:
標簽:MySQL 無鎖化
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定