【劃重點】MySQL技術內幕:InnoDB存儲引擎

說明

本文絕大部分內容來源《MySQL技術內幕:InnoDB存儲引擎》一書,部分圖片來源網絡。#我是搬運工#

InnoDB 體系結構

後臺線程

InnoDB存儲引擎是多線程模型,其後臺有多個不同的後臺線程,負責處理不同的任務。

Master Thread

Master Thread 主要負責將緩存池中的數據異步刷新到磁盤,保證數據的一致性,包括髒頁的刷新、合併插入緩衝(INSERT BUFFER)、UNDO頁的回收。

IO Thread

IO Thread 主要負責 Async IO 請求的回調處理,包含 write、read、insert buffer 和 log IO thread。

Purge Thread

Purge Thread 負責回收已經使用並分配的 undo 頁,減輕 Master Thread 的工作。

Page Cleaner Thread

Page Cleaner Thread 作用是將之前版本中髒頁的刷新操作都放入到單獨的線程中來完成,減輕 Master Thread 的工作及對於用戶查詢線程的阻塞。

內存

緩衝區

一塊內存區域,通過內存的速度來彌補磁盤速度較慢對數據庫性能的影響;讀取數據時,首先將從磁盤讀到的數據存放在緩衝池中,下一次讀取直接從緩衝池中取。更新數據時,先更顯緩衝池的數據,然後通過後臺線程定期將有過更新的緩衝數據刷新到磁盤。從而減少磁盤IO的讀寫。

clipboard.png

LRU List、Free List 和 Flush List

數據庫緩衝池通過 LRU (Last Recent Used) 算法管理,LRU List 用來管理已經讀取的頁,數據庫啓動時,LRU List 爲空列表,沒有任何的頁。此時頁都存放在 Free List 中,當需要從緩衝池中分頁時,首先從 Free List 中查找是否有可用的空閒頁,若有空閒頁則將該頁從 Free 列表中刪除並能夠放入到 LRU List 中,否則淘汰 LRU List 中末尾的頁。在 LRU List 中的頁被修改後,稱該頁爲髒頁(dirty page)。髒頁存儲於 Flush List,表示緩衝池中的頁與磁盤頁不一致,等待被調度刷新。髒頁同時存在於 Flush List 與 LRU List 中。

重做日誌緩衝 redo buffer cache

InnoDB 將重做日誌首先寫入 redo buffer cache,之後通過一定頻率寫入到重做日誌(redo logo)中。redo buffer cache 不需要設置太大,重做日誌緩衝在一下情況下被刷入到重做日誌文件中:
(1) Master Thread 每一秒將重做日誌緩衝刷到重做日誌文件
(2) 每個事務提交時會將重做日誌緩衝刷新到重做日誌文件
(3) 當重做日誌緩衝池剩餘空間小於50%時,重做日誌緩衝刷新到重做日誌

額外的內存池

InnoDB 對內存的管理是通過一種稱爲內存堆的方式進行的,對一些數據結構進行內存分配時,需要從額外的內存池中申請,當該區域不夠時,會從緩衝池中進行申請。

InnoDB 關鍵特性

插入緩衝(insert buffer)

Insert Buffer

對於【非聚集索引】的更新或插入操作,不是直接插入到索引頁中,而是先判斷插入的非聚集索引頁是否在緩衝池中,若在,則直接插入,否則先放入到一個 Insert Buffer 中。再以一定頻率和情況進行 Insert Buffer 和輔助索引頁子節點的merge操作,合併插入操作,提高非聚集索引的插入性能。

Change Buffer

Insert Buffer 的升級,InnoDb 1.0.x 版本開始引入,同樣適用對象爲非唯一的輔助索引。可以對 DML 操作進行緩衝:insert、delete、update。

兩次寫(double write)

double write 帶給 InnoDB 存儲引擎的是數據頁的可靠性。

當數據庫發生宕機時,可能InnoDB存儲引擎正在寫入某個頁到列表中,而這個頁只寫了一部分,,比如16KB的頁,只寫了4KB,之後發生宕機,此時次出現【部分寫失效】(頁斷裂)的情況,InnoDB 通過 double write 解決出現這種情況時造成的數據丟失並且無法恢復的問題。
double write 工作流程:髒頁刷新時,先拷貝至內存的 double write buffer,從緩衝區分兩次接入磁盤共享表空間紅,順序寫,緩衝區中的髒頁數據寫入實際的各個表空間,離散寫。

頁斷裂數據恢復流程:通過頁的 checksum,校驗 double write 在磁盤中的數據塊,通過 double write 緩衝區數據來修復。

clipboard.png

自適應哈希索引(Adaptive Hash Index)

InnoDB 會監控對錶上各索引頁的查詢。如果觀察到建議哈希索引可以帶來速度的提升,則建立哈希索引,稱之爲自適應哈希索引(AHI)。AHI 是通過緩衝池的 B+ 樹構造來的,因此建立的速度非常快,而且不需要對整張表構建哈希索引,InnoDB 會根據訪問頻率和模式來自動創建自適應哈希索引,無需人爲設置干預。

自適應哈希索引只適用於等值查詢,比如 where smsId = 'XXXXXX',不支持範圍查找。

異步 IO(Asynchronous IO)

InnoDB 採用異步IO(AIO)的方式來處理磁盤操作,進而提高對磁盤的操作性能。InnoDB 存儲引擎中,read ahead 方式的讀取是通過 AIO 完成,髒頁的刷新,即磁盤的寫入操作也是由 AIO 完成。

刷新臨接頁(Flush Neighbor Page)

當刷新一個髒頁到磁盤時,InnoDB 會檢測該頁所在區的所有頁,如果是髒頁,則一起進行刷新。通過 AIO 合併多個 IO 寫入,減少磁盤的 IO,但是可能造成將不怎麼髒的頁的磁盤寫入,對於 SSD 磁盤,本身有着較高的 IOPS,則建議關閉該特性,InnoDB 1.2.x 版本提供參數 innodb_flush_neighbors,設置爲 0 可關閉該特性。而對於普通磁盤,建議開啓。

Checkpoint 技術

Checkpoint 技術的目的是解決以下問題:

  • 縮短數據庫的恢復時間
  • 緩衝池不夠用時,刷新髒頁
  • 重做日誌不可用時,刷新髒頁

Checkpoint 類型

  • Sharp Checkpoint:發生在數據庫關閉時,將所有髒頁刷新到磁盤。
  • Fuzzy Chckpoint:數據庫運行時使用該方式進行頁的刷新,刷新部分髒頁進磁盤。

InnoDB 中可能發生的 Fuzzy Checkpoint

Master Thread Checkpoint

Master Thread 以每秒或每十秒的速度從緩衝池的髒頁列表中刷新一定比例的頁到磁盤,異步進行,用戶查詢線程不會阻塞。

FLUSH_LRU_LIST Checkpoint

InnoDB 存儲引擎需保證差不多 100 個空閒頁可用,空閒也不足時,InnoDB 會將 LRU 列表尾端的頁移除,如果尾端頁存在髒頁,則需要進行 Checkpoint。

Async/Sync Flush Checkpoint

重做日誌不可用時進行,強制將一些頁刷新回磁盤,從髒頁列表中選取。根據不同的狀態使用不同的刷新方式(同步或異步)。

Dirty Page too much Checkpoint

髒頁數量太多,比如佔據緩衝池比例大於 75% 時,強制進行刷新,比例可調。

MySQL 文件

參數文件

告訴 MySQL 實例啓動時在哪裏可以找到數據庫文件,並且指定某些初始化參數,這些參數定義了某種內存結構的大小等設置。在默認情況下,MySQL 實例會按照一定的順序在指定的位置進行讀取,通過以下命令可以尋找:

mysql --help | grep my.cnf

MySQL 數據庫中的參數分類

  • 動態參數:MySQL 運行期間中可以進行實時修改
  • 靜態參數:MySQL 運行期間不可修改,只讀

日誌文件

記錄了影響 MySQL 數據庫的各種類型活動,常見的日誌文件有:

錯誤日誌

對 MySQL 的啓動、運行、關閉過程進行記錄,可根據錯誤日誌定位問題。不僅記錄錯誤信息,同時也記錄一些告警信息或正確的信息。

# 查看日誌文件存儲路徑
mysql> show variables like 'log_error';
+---------------+--------------------------------+
| Variable_name | Value                          |
+---------------+--------------------------------+
| log_error     | /data/mysql_data/data/r002.err |
+---------------+--------------------------------+

慢查詢日誌

幫助查找存在問題的 SQL 語句,記錄執行時間超過某個時間長度的 SQL 語句。

# 查詢記錄執行時間長度(秒)
mysql> show variables like 'long_query_time' \g;
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+
# 慢查詢記錄開關
mysql> show variables like 'log_slow_queries' \g;
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| log_slow_queries | ON    |
+------------------+-------+

慢查詢日誌文件可通過 mysqldumpslow 解析結果並查看。

查詢日誌

查詢日誌記錄了所有對 MySQL 數據庫請求的信息,無論這些請求是否得到了正確的執行。默認文件名爲:主機名.log。

二進制日誌

二進制日誌(binary log)記錄了對 MySQL 數據庫執行更改的所有操作,不包含只讀操作。二進制文件主要有以下幾種作用:

  • 恢復:某些數據的恢復需要二進制日誌,例如,在一個數據庫全備文件恢復後,用戶可以通過二進制日誌進行 point-in-time 的恢復。
  • 複製:通過複製和執行二進制日誌使一臺遠程的 MySQL 數據庫與另一臺 MySQL 數據庫進行實時同步。
  • 審計:用戶可以通過二進制日誌中的信息來進行審計,判斷是否有對數據庫進行注入攻擊。

套接字文件

在 UNIX 系統下本地連接 MySQL 可以採用 UNIX 域套接字方式。

# 查看套接字文件地址
mysql> show variables like 'socket';
+---------------+-----------------------+
| Variable_name | Value                 |
+---------------+-----------------------+
| socket        | /var/mysql/mysql.sock |
+---------------+-----------------------+

pid 文件

在 MySQL 實例啓動時,生成的進程ID會被寫入到一個文件中,即 pid 文件。

# 查看 pid 文件
mysql> show variables like 'pid_file';
+---------------+--------------------------------+
| Variable_name | Value                          |
+---------------+--------------------------------+
| pid_file      | /data/mysql_data/data/r002.pid |
+---------------+--------------------------------+

表結構定義文件

MySQL 數據的存儲是根據表進行的,每個表都有與之對應的文件,是以 frm 爲後綴名的文件,記錄了表的表結構定義。

InnoDB 存儲引擎文件

InnoDB 存儲引擎獨有的文件,與InnoDB 存儲引擎密切相關,包括表空間文件、重做日誌文件。

表空間文件

InnoDB 採用將存儲的數據按表空間進行存放的設計。默認配置下有一個初始化大小爲 10MB 的 ibdata1 文件,可自動增長。可以通過參數 innodb_data_file_path 對其進行設置。若設置了參數 innodb_file_per_table,則用戶可以將每個基於 InnoDB 存儲引擎的表產生一個獨立表空間。獨立表空間命名規則:表名.ibd

# 查看是否開啓獨立表空間存儲
mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON   |
+-----------------------+-------+

需要注意的是,這些單獨的表空間文件僅存儲該表的數據、索引和插入緩衝 BITMAP 等信息,其餘信息還是存放在默認表空間中。下圖顯示了 InnoDB 存儲引擎對於文件的存儲方式:

clipboard.png

重做日誌文件

默認情況下,InnoDB 存儲引擎的數據目錄下會有兩個名爲 ib_logfile0 和 ib_logfile1 的文件,即 InnoDB 存儲引擎的重做日誌文件(redo log file),記錄了對於 InnoDB 存儲引擎的事務日誌。

當實例或介質存儲失敗時,例如由於主機斷電導致實例失敗,InnoDB 存儲引擎會使用重做日誌恢復到斷電前的時刻,一次來保證數據的完整性。InnoDB 存儲引擎會逐個循環寫日誌文件,當前寫的日誌文件被寫滿後,切到下一個日誌文件,當下一個日誌文件也被寫滿後,循環寫前一個日誌文件。日誌文件數量及大小可配置(innodb_log_files_in_group、innodb_log_file_size)。

關於重做日誌文件的大小設置:
(1) 不能設置太大,如果設置得很大,在恢復時可能需要很長的時間
(2) 不能設置過小,可能會導致一個事務的日誌需要多次切換重做日誌文件,也會導致頻繁的發生 async checkpoint,導致性能抖動。

寫入重做日誌文件的操作不是直接寫,而是先寫入一個重做日誌緩衝(redo log buffer)中,然後按照一定的順序寫入日誌文件:

clipboard.png

MySQL 二進制文件

MySQL 二進制文件記錄 MySQL 數據庫執行的更新操作。包含二進制日誌文件和二進制索引文件。

mysql-bin.index
mysql-bin.000001
mysql-bin.000002
mysql-bin.XXXXXX

mysql-bin.000001 即爲二進制日誌文件,日誌文件超過一定大小(根據 max_binlog_size 確定)時生成新的文件,後綴名 +1。binlog 相關的參數有如下:

max_binlog_size    # 單個 binlog 日誌文件的最大值
binlog_cache_size  # 事務提交時的二進制日誌寫入的緩衝區大小
sync_binlog           # 表示每寫入多少次緩衝區就同步至磁盤
binlog-do-db       # 表示需要寫入哪些庫的日誌
binlog-ignore-db   # 表示忽略寫入哪些庫的日誌
bin_log_format     # 表示二進制日誌的記錄格式,包含 STATEMENT、ROW、MIXED

二進制日誌記錄格式

STATEMENT:MySQL 5.1 之前的存儲格式,5.1 版本以後可選格式,記錄日誌的邏輯 SQL 語句。
ROW:二進制日誌不再是簡單的 SQL 語句,而是記錄錶行的更改情況,包含一行數據更改前與更改後列的內容。
MIXED:默認採用 STATEMENT 格式進行二進制日誌文件的記錄,但是在一些情況下回使用 ROW 格式,如以下情況:
1)表的存儲引擎爲 NDB
2)使用了 UUID()、USER()、CURRENT_USER()、FOUND_ROWS()、ROW_COUNT() 等不確定函數
3)使用了 INSERT DELAY 語句
4)使用了用戶定義函數(UDF)
5)使用了臨時表

STATEMENT 與 ROW 模式對比

  • 通常情況下 STATEMENT 因爲只記錄邏輯 SQL 語句,相關 ROW 模式下日誌存儲大小較小,特別是批量更新情況下,ROW 模式的日誌文件遠遠大於 STATEMENT 模式下的日誌文件,在做日誌複製時,由於要傳輸 binlog 文件的內容,STATEMENT 模式的傳輸要優於 ROW 模式
  • 如果更新的 SQL 語句中存在不確定的函數調用等情況,用 STATEMENT 模式記錄的 SQL 語句做同步會導致數據不一致,因此使用場景有所侷限。

二進制文件(binlog)與重做日誌文件(redo log)

  • 二進制日誌記錄所有與 MySQL 數據庫有關的日誌記錄,包括 InnoDB、MyISAM以及其他存儲引擎的日誌。而 InnoDB 存儲引擎的重做日誌只記錄 InnoDB 存儲引擎本身的事務日誌。
  • 記錄的內容不同,二進制日誌文件記錄的是關於一個事務的具體操作內容,即該日誌的邏輯日誌。而重做日誌文件記錄的是關於每個頁(Page )的更改的物理情況。
  • 寫入時間不同,二進制日誌文件僅在事務提交後進行寫入,即只寫磁盤一次,不論事務有多大。而在事務進行的過程中,卻不斷有重做日誌條目被寫入到重做日誌文件中。

MySQL 分區表

InnoDB 邏輯存儲結構

clipboard.png

從 InnoDB 存儲引擎的邏輯存儲結構看,所有數據都被邏輯的存放在表空間(tablespace),表空間又分爲段(segment)、區(extend)、頁(page)組成。而表的行數據則存儲在頁中,一個頁存儲多個行。

分區

MySQL 數據庫在 5.1 版本時添加了對分區的支持。分區功能並不是在存儲引擎層完成的,因此不僅 InnoDB 存儲引擎支持分區,MyISAM、NDB 等都支持。也並不是所有存儲引擎都支持分區,如 CSV、MERGE、FEDORATED 等就不支持。

分區的作用

對一部分 SQL 語句性能帶來明顯的提高,但是分區主要用於數據庫高可用性的管理。

分區類型

RANGE 分區

行數據基於屬於一個給定連續區間的列值被放入分區。

mysql> create table test_range (id int) ENGINE = INNODB PARTITION BY RANGE (id)(
    -> PARTITION P0 VALUES LESS THAN (10),
    -> PARTITION P1 VALUES LESS THAN (20));
Query OK, 0 rows affected (0.03 sec)

創建分區後,存儲文件的獨立表空間將根據分區存儲,如下圖所示:

-rw-rw----  1 _mysql  _mysql    96K Jan 28 17:05 test_range#P#P0.ibd
-rw-rw----  1 _mysql  _mysql    96K Jan 28 17:05 test_range#P#P1.ibd
-rw-rw----  1 _mysql  _mysql   8.4K Jan 28 17:05 test_range.frm
-rw-rw----  1 _mysql  _mysql    28B Jan 28 17:05 test_range.par

對錶添加數據時,會根據指定的列將數據存儲到對應的分區存儲文件中,如列對應的值不在對應範圍內,將寫入失敗:

mysql> insert into test_range values (30);
ERROR 1526 (HY000): Table has no partition for value 30

LIST 分區

LIST 分區與 RANGE 分區非常相似,只是分區列的值是離散的,而非連續的。如下:

mysql> create table test_list (a INT, b INT) ENGINE = INNODB PARTITION BY LIST (b)(
    -> PARTITION P0 VALUES IN (1, 3 ,5, 7, 9),
    -> PARTITION P1 VALUES IN (0, 2, 4, 6, 8));
Query OK, 0 rows affected (0.03 sec)

同樣的,添加數據時,對應的列必須在指定的範圍內,否則將寫入失敗:

mysql> insert into test_list (a, b) values (1, 11);
ERROR 1526 (HY000): Table has no partition for value 11

HASH 分區

HASH 分區的目的是將數據均勻的分佈到預先定義的各個分區中,保證各分區的數據量大致一樣。用於需要對將要進行哈希分區的列值指定一個列值或表達式,以及指定被分區的表將要被分割成的分區數量:

mysql> create table test_hash (a INT, b DATETIME) ENGINE = INNODB
    -> PARTITION BY HASH (YEAR(b))
    -> PARTITIONS 4;
Query OK, 0 rows affected (0.03 sec)

KEY 分區

KEY 分區與 HASH 分區相似,HASH 分區使用用戶定義的函數進行分區,KEY 分區使用 MySQL 數據庫提供的函數進行分區。

mysql> create table test_key (a INT, b DATETIME) ENGINE = INNODB
    -> PARTITION BY KEY (b)
    -> PARTITIONS 4;
Query OK, 0 rows affected (0.04 sec)

COLUMNS 分區

以上四種區分方式均存在一樣的分區條件:數據必須是整型(INT)的,如果不是整型,需要將對應的值轉化爲整型,如 YEAR(),TO_DAYS() 等函數。MySQL 5.5 版本開始支持 COLUMNS 分區,可視爲 RANGE 分區和 LIST 分區的一種進化。

可以直接使用非整形的數據進行分區,如所有的整型類型 SMALLINT、BIGINT,日期類型 DATE、DATETIME,字符串類型 CHAR、VARCHAR,相應的 FLOAT、DECIMAL、BLOB、TEXT、TIMESTAMP 不予支持。

分區和分區性能

數據庫的應用分爲兩類:OLTP(在線事務處理)和 OLAP(在線分析處理)。

對於 OLAP 的應用,分區的確可以很好地提高查詢性能。體現在掃描表時不需要掃描全表,掃描單個分區時效率最高;同時也依賴於應用的處理以及分區方式,如不合理的分區,將帶來巨大的性能下降。比如對主鍵進行 HASH 分區,查詢的時候通過非主鍵字段匹配查詢,則同樣是全量數據掃描,但是由於分區的數量較多,會大量增加系統的 IO 調用。

對於 OLTP 的應用,分區並不會明顯的降低 B+ 樹索引高度,一般的 B+ 樹需要 2~3 次的磁盤 IO,分區並不能明顯提升寫入速度。但是設計不好的分區會帶來嚴重的性能問題。

MySQL 索引

InnoDB 存儲引擎支持以下幾種常見索引:

  • B+ 樹索引
  • 哈希索引
  • 全文索引

B+ 樹

B+ 樹的概念在此不做介紹,B+ 樹的操作演示地址:https://www.cs.usfca.edu/~gal...

B+ 樹索引

MySQL 並不是通過 B+ 樹索引直接找到數據行,而是找到數據行所在的頁,將頁加載到內存,最後查找到行數據。一個數據頁包含多行數據。

B+ 樹索引包含數據頁與索引頁。數據頁存放完整的行數據,索引頁存放鍵值以及指向數據頁的偏移量,而非完整的行記錄。

B+ 樹索引分類

聚集索引(Clustered Index)

InnoDB 存儲引擎是索引組織表,即表中數據按照主鍵順序存放,聚集索引就是按照主鍵構造一顆 B+ 樹,同時葉子節點存放表的行記錄數據,也將聚集索引的葉子節點稱爲數據頁。簡而言之,數據是索引的一部分。

MySQL 通過索引構造數據,所以一張數據表中只能有一個聚集索引。

輔助索引(Secondary Index)

也成爲非聚集索引,葉子節點並不包含數據行的全部數據。葉子節點中的索引行中包含一個書籤,該書籤就是相應行數據的聚集索引鍵,因此通過非聚集索引查找行數據需要經過兩級索引才能查找到具體的數據內容。比如,非主鍵索引查找行數據,先通過非主鍵索引查找到主鍵,再通過主鍵查找行數據。

哈希索引

MySQL 中的 HASH 索引爲自適應的,無需人工干擾,MySQL 內部會針對查詢業務自動創建 HASH 索引,以提高業務的查詢效率。

HASH 索引僅適用的等值匹配查詢,對於範圍查找無能爲力。

全文索引

InnoDB 1.2.x 版本開始,InnoDB 存儲引擎開始支持全文索引,通過倒排索引來實現。因爲業務極少使用 MySQL 的全文索引,通常如果需要做全文搜索,可選擇 Elasticsearch。

Cardinality 值

Cardinality 值對列創建索引的選擇性提供了較好的參考,Cardinality 爲一個預估值,非準確值,某一個列的 Cardinality 值代表該列在整張表中不重複值的數量。

忽略業務因素以及數據類型,表中某個列是否適合創建索引,體現在該列所有的值是否相對分散,重複數據越少,相對來說越適合添加索引。因此,當某個列 Cardinality值/錶行數 約接近 1,代表重複數據越少,爲該列建索引的選擇性便越高。

InnoDB 存儲引擎對於 Cardinality 的更新是非實時的,並且獲取到的值爲預估值,通過採樣統計來獲取該值。通常具體業務只需關心該值是否接近於表的行數,以判斷某個列是否適合創建索引。

MySQL 鎖

Innodb 存儲引擎鎖類型

行級鎖
共享鎖(S Lock):允許事務讀一行數據
排他鎖(X Lock):允許事務刪除或更新一行數據
表級鎖
意向共享鎖(IS Lock):事務想要獲得一張表中某幾行的共享鎖
意向排他鎖(IS Lock):事務想要獲得一張表中某幾行的排他鎖

InnoDB 存儲引擎中鎖的兼容性:

IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

如下圖所示,當 InnoDB 需要做細粒度加鎖時,比如對某一行加 X 鎖,需要先對該行所在的表、頁加 IX 鎖。若其中任何一個部分導致等待,那麼該操作需要等待粗粒度鎖的完成。

clipboard.png

一致性非鎖定讀:Consistent Nonlocking Read

InnoDB 存儲引擎通過行多版本控制的方式來讀取當前執行時間數據庫中行的數據。如果讀取的行正在執行 UPDATE 或 DELETE 操作,這時讀取操作不會因此等待行上鎖的釋放。相應的,InnoDB 存儲引擎會去讀取行的一個快照數據。

非鎖定讀機制極大的提高了數據庫的併發性,在 InnoDB 存儲引擎的默認設置下,讀取不會佔用和等待表上的鎖。快照數據即當前行記錄的歷史版本,每行記錄可能有多個版本,由此帶來的併發控制,稱之爲多版本併發控制(Multi Version Concurrency Control,MVCC)。

一致性鎖定讀:Consistent Locking Read

在某些情況下,用戶需要顯示的對數據庫讀取操作進行加鎖以保證數據邏輯的一致性。這要求數據庫支持加鎖語句,即使對於 SELECT 的支付操作。InnoDB 存儲引擎對於 SELECT 語句支持兩種一致性的鎖定讀操作:

# 對讀取的行記錄添加一個 X 鎖
SELECT ... FOR UPDATE;

# 對讀取的行記錄添加一個 S 鎖
SELECT ... LOCK IN SHARE MODE;

鎖的算法

1)Record Lock:單個行記錄上的鎖

# 鎖定 id = 5 的行記錄
SELECT ... FROM ... WHERE id = 5 FOR UPDATE;

2)Gap Lock:間隙鎖,鎖定一個範圍,單不包含記錄本身

# 鎖定 id < 5 的所有記錄
SELECT ... FROM ... WHERE id < 5 FOR UPDATE;

3)Next-Key Lock:Gap Lock + Record Lock,鎖定一個範圍,並且鎖定記錄本身

# 鎖定 id <= 5 的所有記錄
SELECT ... FROM ... WHERE id <= 5 FOR UPDATE;

幻像問題:Phantom Problem

Phantom Problem 是指同一事務下,連續執行兩次同樣的 SQL 語句可能導致不同的結果,第二次的 SQL 語句可能會返回之前不存在的行。比如:

# 表 t 中存在 id 爲 1,2,3,4 四條數據,事務隔離級別爲  READ-COMMITTED
BEGIN;
SELECT * FROM t WHERE id > 2;    # 此時查詢結果有id爲 3,4 的記錄
# 此時其他線程增加一條數據 id = 5
SELECT * FROM t WHERE id > 2;    # 此時查詢結果有id爲 3,4,5 的記錄

上述查詢結果,在一個事務中出現同一個查詢返回不同的結果,違反了事務的隔離性,即當前事務能夠看到其他事務的結果。

InnoDB 存儲引擎默認的事務隔離級別是 REPEATABLE READ,在該事務隔離級別下采用 Next Key Locking 的方式來加鎖解決。同時應用也可以通過 InnoDB 存儲引擎的 Next Key Locking 機制在應用層面實現唯一性的檢查。例如:

SELECT * FROM t WHERE col = xxx LOCK IN SHARE MODE;

鎖問題

髒讀

髒讀指一個事務可以讀到另一個事務中未提交的修改數據,違反了數據庫的隔離性。

髒讀發生的條件是事務隔離級別是 READ UNCOMMITTED,目前大部分數據庫都至少設置成 READ COMMITTED。

不可重複讀

不可重複讀指在一個事務內多次讀取同一數據集合,出現了不同的數據結果。

不可重複讀發生在事務隔離級別爲 READ COMMITTED,事務 A 讀取一個結果集,事務 B 同樣讀取到該結果集並對其進行修改,提交事務,事務 A 再次讀取結果集時,兩次結果不一致。一般情況下,不可重複的問題是可接受的,因爲讀取的是已經提交的數據,本身不會帶來很大問題。

InnoDB 存儲引擎的隔離級別爲 READ REPEATABLE 時,採用 Next Key Lock 算法,避免了不可重複讀的現象。

丟失更新

一個事務的更新操作結果被另一個事務的更新操作結果所覆蓋,從而導致數據的不一致。

數據庫層面可以阻止丟失更新問題的發生,但是應用中存在一個邏輯意義的丟失更新問題。例如,多個線程同時讀取到某條數據,之後均對數據進行修改再更新庫,此時會出現最後一個線程的更新結果覆蓋了先執行的更新結果。應用層面可以通過對查詢的數據進行加鎖,如前文提到的一致性鎖定讀方式,對需要更新的數據進行加鎖,其他線程即會出現阻塞串行等待。

死鎖

死鎖是指兩個或兩個以上的事務在執行過程中,因搶奪鎖資源而造成的互相等待的現象。

解決死鎖的方式:
1)超時
當一個等待時間超過設置的某一閾值時,對該事務進行回滾,InnoDB 中通過參數 innodb_lock_wait_timeout 設置超時時間。

超時處理機制簡單,但不判斷事務所佔權重,比如一個事務更新的行非常多,回滾也需要佔用更多的時間,同時與該事務搶佔資源的事務可能僅更新少量數據,回滾該事務應當更合理。

2) wait-for graph(等待圖)死鎖檢測
主動檢測死鎖,判斷事務之間的等待狀態是否存在閉環。若檢測到存在死鎖的情況,InnoDB 存儲引擎選擇回滾 undo 量最小的事務。

鎖升級

鎖升級指將當前鎖的粒度降低。比如數據庫把一個表的 1000 個行鎖升級爲一個頁鎖,或者將頁鎖升級爲表鎖。從而避免鎖的開銷。

InnoDB 存儲引擎根據頁進行加鎖,並採用位圖方式, 開銷由頁的量決定,因此 InnoDB 引擎不會產生鎖升級的問題。

MySQL 事務

事務的實現

InndoDB 是事務的存儲引擎,其通過 Forece Log at Commit 機制實現事務的持久性,即當事務提交時,必須先將該事務的所有日誌寫入到重做日誌文件進行持久化,待事務的 COMMIT 操作完成纔算完成。這裏的日誌是指重做日誌,由兩部分組成,即 redo log 和 undo log。

事務的 ACID 特性實現:隔離性,通過鎖實現;原子性、一致性、持久性,通過數據庫的 redo log 和 undo log 實現。

redo log 與 undo log

redo 和 undo 的作用都可以視爲是一種恢復操作,redo 恢復提交事務修改的頁操作,而 undo 回滾行記錄到某個特定的版本,用來幫助事務回滾及 MVCC 的功能。因此兩者記錄的內容不同,redo 通常是物理日誌,記錄的是頁的物理修改操作。redo log 基本上都是順序寫的,undo 是邏輯日誌,根據每行記錄進行記錄。undo log 是需要進行隨機讀寫的。

redo

重做日誌用來實現事務的持久性,即事務 ACID 的 D。其由兩部分組成:一是內存中的重做日誌緩衝(redo log buffer),其是容易丟失的;二是重做日誌文件(redo log file),其是持久的。

爲了確保每次日誌都寫入重做日誌文件,在每次將重做日誌緩衝寫入重做日誌文件後,InnoDB 存儲引擎都需要調用一次 fsync 操作。因此磁盤的性能決定了事務提交的性能,也就是數據庫的性能。

參數 innodb_flush_log_at_trx_commit 用來控制重做日誌刷新到磁盤的策略,其參數含義如下:

1 -> 表示事務提交時必須調用一次 fsync
0 -> 表示事務提交是不進行寫入重做日誌操作,這個操作盡在 master thread 中完成,而 master thread 中每 1 秒進行一次重做日誌文件的 fsync 操作
2 -> 表示事務提交時將重做日誌寫入重做日誌文件,但僅寫入文件系統的緩存中,不進行 fsync 操作。

MySQL 默認參數爲 1,保證最高的數據可靠性,爲 0 或 2 時可以提供更好的事務性能,但是存在數據庫宕機時數據丟失風險。

undo

重做日誌記錄了事務的行爲,可以很好的通過其對頁進行“重做”操作。但是事務有時還需要進行回滾操作,這時就需要 undo。在對數據庫進行修改時,InnoDB 存儲引擎不但會產生 redo,還會產生一定量的 undo,這樣如果執行的事務或語句由於某種原因失敗了,又或者是用戶主動 ROLLBACK 請求回滾,就可以利用 undo 進行數據回滾到修改之前的樣子。

purge

delete 和 update 操作並不直接刪除原有的數據,執行 delete 語句時,受影響的數據被標記爲邏輯刪除,真正刪除這行記錄的操作在 purge 操作中完成。

purge 用於最終完成 delete 和 update 操作。這樣設計是因爲 InnoDB 存儲引擎支持 MVCC,所以記錄不能再事務提交時立即進行處理。而是否可以刪除該條記錄通過 purge 來判斷,若該行記錄已不再被任務其他事務引用,那麼就可以進行真正的 delete 操作。

group commit

若事務爲非只讀事務,則每次事務提交時需要進行一次 fsync 操作,以保證重做日誌都已經寫入磁盤。爲了提高磁盤 fsync 的效率,當前數據庫提供了 group commit 的操作,即一次 fsync 可以刷新確保多個事務日誌被寫入文件。對於 InnoDB 存儲引擎來說,事務提交時會進行兩個階段的操作:
1)修改內存中事務對應的信息,並且將日誌寫入重做日誌緩衝
2)調用 fsync 確保日誌都從重做日誌緩衝寫入磁盤

步驟 2)相對步驟 1)是一個較慢的過程,當有事務進行步驟 2)時,其他事務可以進行步驟 1)的操作,正在提交的事務完成提交操作後,再次進行步驟 2)時,可以將多個事務的重做日誌通過一次 fsync 刷新到磁盤,這樣就大大的減少了磁盤的壓力,從而提高了數據庫的整體性能。

MySQL 備份與恢復

備份分類

根據不同的類型來劃分:

  • Hot Backup(熱備)
  • Cold Backup(冷備)
  • Warm Backup(溫備)

按照備份後文件的內容劃分:

  • 邏輯備份
  • 裸文件備份

按照備份數據庫的內容劃分:

  • 完全備份
  • 增量備份
  • 日誌備份

冷備

對於 InnoDB 存儲引擎的冷備,只需要備份 MySQL 數據庫的 frm 文件,共享表空間文件,獨立表空間文件(*.ibd),重做日誌文件。另外建議定期備份 MySQL 數據庫的配置文件 my.cnf,有利於恢復的操作。

冷備的優點:

  • 備份簡單,只需要複製相關文件即可
  • 備份文件易於在不同操作系統,不同 MySQL 版本上進行恢復
  • 恢復相當簡單,只需要把文件恢復到指定位置即可
  • 回覆速度快,不需要執行任何 SQL 語句,不需要重建索引

冷備的缺點:

  • InnoDB 存儲引擎冷備的文件通常比邏輯文件大很多
  • 冷備也不總是可以輕易的跨平臺

邏輯備份

mysqldump

用來完成轉存數據庫的備份及不同數據庫之間的移植,如從 MySQL 低版本數據庫升級到 MySQL 高版本數據庫,又或者從 MySQL 移植到 Oracle,SQL Server 等。

mysqldump [arguments] > file_name
mysqldump --all-databases > dump.sql
mysqldump --databases db1 db2 db3 > dump.sql

SELECT ... INTO OUTFILE

邏輯備份方法,更準確的說是導出一張表中的數據。

SELECT * INTO OUTFILE '/home/yw/a.txt' FROM test;

邏輯備份的恢復

SOURCE

mysqldump 的恢復操作簡單,僅需執行導出的 SQL 語句即可。

source /home/yw/dump.sql

LOAD DATA INFILE

恢復通過 SELECT INTO OUTFILE 導出的數據

LOAD DATA INTO TABLE test IGNORE 1 LINES INFILE '/home/yw/a.txt'

二進制日誌備份與恢復

二進制日誌非常關鍵,用戶可以通過它完成 point-in-time 的恢復工作,MySQL 的 replication 同樣需要二進制日誌,在默認情況下並不開啓二進制日誌,要使用二進制日誌必須啓用它。InnoDB 存儲引擎推薦的二進制日誌的服務器配置如下:

[mysqld]
log-bin = mysql-bin
sync_binlog = 1
innodb_support_xa = 1

在備份二進制日誌文件前,可以通過 FLUSH LOGS 命令生成一個新的二進制日誌文件,然後備份之前的二進制日誌。

恢復二進制日誌也非常簡單,通過 mysqlbinlog 即可:

mysqlbinlog [options] log_file
mysqlbinlog binlog.0000001 | mysql -uroot -p test

也可以先將二進制文件導出到一個文件,然後通過 source 進行導入:

shell > mysqlbinlog binlog.0000001 > /home/yw/binlog.sql
...
mysql > source /home/yw/binlog.sql

複製的工作原理

複製(replication)是 MySQL 數據庫提供的一種高可用高性能的解決方案,replication 的工作原理分爲以下 3 個步驟:

  • 主服務器把數據更改記錄到二進制日誌(binlog)中
  • 從服務器把主服務器的二進制日誌複製到自己的中繼日誌(relay log)中
  • 從服務器重做中繼日誌中的日誌,把更改用到自己的數據庫上,以達到最終一致性

複製的工作原理如下圖所示:

clipboard.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章