亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747


MySQL無鎖化WAL系統(tǒng)那些事兒

 

概述

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

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

日志并行化

日志并行化的思路也很簡(jiǎn)單:將寫日志拆分為兩個(gè)過程:

  1. 從內(nèi)存log buffer中為日志預(yù)留空間

2. 將日志內(nèi)容拷貝至1預(yù)留的空間

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

而在步驟1中也可以使用原子變量來代替代價(jià)較高鎖實(shí)行預(yù)留,在mysql 8實(shí)現(xiàn)中,其實(shí)就兩行代碼:

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;    ...}

可以看到,只需要一個(gè)原子變量log.sn記錄當(dāng)前分配的位置信息,下次分配時(shí)更新該log.sn即可,非常簡(jiǎn)潔優(yōu)雅。

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

Log Buffer空洞問題

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

MySQL無鎖化WAL系統(tǒng)那些事兒

 

上圖中,3個(gè)線程分別分配了對(duì)應(yīng)的redo buffer,線程1和3已經(jīng)完成了wal日志內(nèi)容的拷貝,而線程2則還在拷貝中,此時(shí)寫入線程最多只能將thread-1的redo log寫入日志文件。 為此,MySQL 8.0中引入了 Link_buf 。

Link_buf原理

Link_buf用于輔助表示其他數(shù)據(jù)結(jié)構(gòu)的使用情況,在Link_buf中,如果一個(gè)索引位置index處存儲(chǔ)的是非0值n,則表示Link_buf輔助標(biāo)記的那個(gè)數(shù)據(jù)結(jié)構(gòu),從index開始后面n個(gè)元素已被占用。

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是一個(gè)定長(zhǎng)數(shù)組,且保證數(shù)組的每個(gè)元素的更新是原子操作的。以環(huán)形的方式復(fù)用已經(jīng)釋放的空間。

同時(shí)Link_buf內(nèi)部維護(hù)了一個(gè)變量 m_tail 表示當(dāng)前最大可達(dá)的LSN。

Innodb日志系統(tǒng)中為L(zhǎng)og Buffer維護(hù)了兩個(gè)Link_buf類型的變量 recent_written 和 recent_closed 。示意圖如下:

MySQL無鎖化WAL系統(tǒng)那些事兒

 

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

接下來當(dāng)lsn1處的空洞被填充后,Link_buf中該處對(duì)應(yīng)的內(nèi)容就會(huì)被設(shè)置,如下:

MySQL無鎖化WAL系統(tǒng)那些事兒

 

同理,當(dāng)lsn3處的空洞也被填充后,狀態(tài)變成下面這樣:

MySQL無鎖化WAL系統(tǒng)那些事兒

 

Link_buf實(shí)現(xiàn)

初始化

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) {  // 默認(rèn)值為1MB  log.recent_written = Link_buf<lsn_t>{srv_log_recent_written_size};}?// Link_buf構(gòu)造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);  }}

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

同時(shí),創(chuàng)建完成后會(huì)將數(shù)組的每個(gè)成員初始化為0。

mtr log拷貝完成

mtr在commit時(shí)會(huì)將其運(yùn)行時(shí)產(chǎn)生的所有redo log拷貝至Innodb全局的redo log buffer,這借助了 mtr_write_log_t 對(duì)象來完成,且每次拷貝按照block為單位進(jìn)行。需要說明的是:一個(gè)mtr中可能存在多個(gè)block來存儲(chǔ)mtr運(yùn)行時(shí)產(chǎn)生的redo log,每個(gè)block拷貝完成后均觸發(fā)一次Link_buf的更新。

struct mtr_write_log_t {  bool operator()(const mtr_buf_t::block_t *block) {    ...    // 拷貝完成后觸發(fā)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) {  ...  // 更新本次寫入的內(nèi)容范圍對(duì)應(yīng)的LinkBuf內(nèi)特定的數(shù)組項(xiàng)值  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) {  // 定位本次寫入的內(nèi)容范圍所在數(shù)組項(xiàng)index  // 算法是將起始lsn(@from)對(duì)數(shù)組容量取模,即from % capacity  const auto index = slot_index(from);  auto &slot = m_links[index];  slot.store(to - from);}

在這里會(huì)找到start_lsn對(duì)應(yīng)的slot,并在該slot內(nèi)設(shè)置值為end_lsn - start_lsn,記錄該位置處已寫入的內(nèi)容數(shù)量。

log_advance_ready_for_write_lsn

Innodb將redo log buffer內(nèi)容寫入日志文件時(shí)需要保證不能存在空洞,即在寫入前需要獲得當(dāng)前最大的無空洞lsn。這同樣依賴LinkBuf。在后臺(tái)寫日志線程 log_writer 的 log_advance_ready_for_write_lsn 函數(shù)中完成。

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);  }}

這里的關(guān)鍵在于函數(shù) Link_buf::advance_tail_until ,即推進(jìn)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;  }}

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

MySQL無鎖化WAL系統(tǒng)那些事兒

 

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

分享到:
標(biāo)簽:MySQL 無鎖化
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定