一、 引言

做資料庫相關工作的同學都知道事務,就是一個操作序列 ,這些操作要麼都執行,要麼都不執行,它是一個不可分割的工作單位, ACID又是事務的四大特性。

那麼就會有如下疑問:

  • ACID具體代表什麼?
  • MySQL InnoDB引擎又是如何實現ACID的呢?

二、 ACID詳解

ACID,是指在可靠資料庫管理系統(DBMS)中,事務(transaction)所應該具有的四個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)

2.1 原子性(Atomicity)

一個事務必須被視為一個不可分割的最小工作單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾,對於一個事務來說,不可能只執行其中的一部分操作。實現事務的原子性,資料庫要支持回滾操作,在事務中某個操作失敗後,回滾到事務之前的狀態。大多數資料庫實現事務時,事務操作的數據是在事務相關的數據快照上,並不是真正修改實際的數據,如果有錯不提交即可。而其他一些只支持簡單事務的資料庫系統中,事務更新數據不在快照上,而是直接操作實際數據,則系統需要先預演一遍所有要執行的操作,如果失敗,事務相關的所有操作都不會被執行。

2.2 一致性(consistency)

一致性是指事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。

如轉賬的栗子: 假設用戶A和用戶B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。

事務的一致性決定了一個系統設計和實現的複雜度,因為事務可以有不同程度的一致性:

  • 強一致性: 無論更新操作實在哪一個數據副本執行,之後所有的讀操作都能獲得最新的數據。
  • 弱一致性: 提交的更新操作,不一定立即會被讀操作讀到,需要一段時間,此種情況會存在一個不一致窗口。
  • 最終一致性: 是弱一致性的特例。事務更新一份數據,最終一致性保證在沒有其他事務更新同樣的值的話,最終所有的事務都會讀到之前事務更新的最新值。如果沒有錯誤發生,不一致窗口的大小依賴於:通信延遲,系統負載等。

2.3 隔離性(Isolation)

隔離性是當多個用戶並發訪問資料庫時,比如操作同一張表,資料庫為每一個用戶開啟的事務,不能被其他事務的操作所幹擾,多個並發事務之間要相互隔離。

當前大多數資料庫都提供了不同級別的事務隔離,之後便會產生不同的問題,一般會出現的問題主要有:

  • 臟讀

臟讀是指在一個事務處理過程裏讀取了另一個未提交的事務中的數據。當一個事務A正在多次修改某個數據,而在這個事務中這多次的修改都還未提交,這時另外一個並發的事務B來訪問該數據,將會獲取到事務A中沒有提交的數據, 從而最終造成兩個事務得到的數據不一致。

如: 張三有500塊錢,現在給李四轉100塊錢,這個事務中的SQL涉及到兩個操作:1、 A賬戶減100塊錢

update account set money=money - 100 where name=』張三』;

2、 B賬戶加100塊錢

update account set money=money+100 where name=』李四』;

我們假設事務A執行轉賬,剛執行完第一條SQL語句,此時,另外一個事務B來查詢張三賬戶的錢,那麼獲取到的是400塊錢。但是事務A執行第二條語句的時候出現了問題,事務A進行了回滾,那麼事實上張三賬戶的錢應該是500塊錢,而不是400塊錢,從而出現了臟讀。

  • 不可重複讀

不可重複讀是指在對於資料庫中的某個數據,一個事務範圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。

例如事務T1在讀取某一數據,而事務T2立馬修改了這個數據並且提交事務給資料庫,事務T1再次讀取該數據就得到了不同的結果,發送了不可重複讀。

不可重複讀和臟讀的區別是,臟讀是某一事務讀取了另一個事務未提交的臟數據,而不可重複讀則是讀取了前一事務提交的數據。

  • 幻讀

幻讀,是指當事務不是獨立執行時發生的一種現象。

例如事務T1對一個表中所有的行的某個數據項做了從「1」修改為「2」的操作,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值還是為「1」並且提交給資料庫。而操作事務T1的用戶如果再查看剛剛修改的數據,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀。

幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點就臟讀不同),所不同的是不可重複讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。

  • 丟失更新

兩個事務同時讀取同一條記錄,A先修改記錄,B也修改記錄(B是不知道A修改過),B提交數據後B的修改結果覆蓋了A的修改結果。

2.4 持久性(Durability)

持久性是指一個事務一旦被提交了,那麼對資料庫中的數據的改變就是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作。

三、MySQL實現ACID的機制

3.1 原子性(Atomicity)

MySQL InnoDB引擎通過以下幾個方面實現事務的原子性:

  • 事務自動提交(Autocommit)配置
  • commit 和rollback語句
  • 從元資料庫INFORMATION_SCHEMA的 表中獲取操作數據

3.1.1 MySQL 事務自動提交(Autocommit)配置

MySQL默認操作模式就是autocommit自動提交模式。這就表示除非顯式地開始一個事務,否則每個查詢都被當做一個單獨的事務自動執行。

  • 查看autocommit模式

變數autocommit分會話系統變數與全局系統變數,所以查詢的時候,最好區別是會話系統變數還是全局系統變數。

mysql> show session variables like autocommit;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)

mysql> show global variables like autocommit;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)

Value的值為ON,表示autocommit開啟。OFF表示autocommit關閉。

  • 修改autocommit模式

方法1: 通過命令行修改

mysql> set session autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> show session variables like autocommit;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)

mysql> show global variables like autocommit;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)

mysql>

mysql> set global autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> show global variables like autocommit;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.01 sec)

上述SQL修改會話系統變數或全局系統變數,只對當前實例有效,如果MySQL服務重啟的話,這些設置就會丟失,如果要永久生效,就必須在配置文件中修改系統變數。

方法2: 修改配置文件

[mysqld]
autocommit=0

  • autocommit與顯性事務的關係

對於顯性事務start transaction或begin, 在自動提交模式關閉(關閉隱式提交)的情況下,開啟一個事務上下文。首先資料庫會隱式提交之前的還未被提交的操作,同時開啟一個新事務。具體實驗如下:

mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 15 |
+-----------------+
1 row in set (0.00 sec)
mysql> show variables like autocommit;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)

mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> delete from tester.test where name=張三;
Query OK, 1 row affected (0.00 sec)

此時重新打開一個終端,登陸MySQL開啟一個新的繪畫,此時可以查詢到會話ID為15的事務信息, 詳情如下:

mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 16 |
+-----------------+
1 row in set (0.00 sec)

mysql> SELECT a.trx_state,
-> b.event_name,
-> a.trx_started,
-> b.timer_wait / 1000000000000 timer_wait,
-> a.trx_mysql_thread_id blocking_trx_id,
-> b.sql_text
-> FROM information_schema.innodb_trx a,
-> performance_schema.events_statements_current b,
-> performance_schema.threads c
-> WHERE a.trx_mysql_thread_id = c.processlist_id
-> AND b.thread_id = c.thread_id;
+-----------+----------------------+---------------------+------------+-----------------+------------------------------------------+
| trx_state | event_name | trx_started | timer_wait | blocking_trx_id | sql_text |
+-----------+----------------------+---------------------+------------+-----------------+------------------------------------------+
| RUNNING | statement/sql/delete | 2019-02-25 15:45:00 | 0.0010 | 1 | delete from tester.test where name=張三 |
+-----------+----------------------+---------------------+------------+-----------------+------------------------------------------+
1 row in set (0.00 sec)

如果在會話15中開啟顯性事務,那麼之前掛起的事務會自動提交,然後,你再去會話16當中查詢,就發現之前的DELETE操作已經提交。

在會話15中開啟顯示事務:start transaction;

mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 15 |
+-----------------+
1 row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

在會話16中繼續查詢事務

mysql> select connection_id();
+-----------------+
| connection_id() |
+-----------------+
| 16 |
+-----------------+
1 row in set (0.00 sec)

mysql> SELECT a.trx_state,
-> b.event_name,
-> a.trx_started,
-> b.timer_wait / 1000000000000 timer_wait,
-> a.trx_mysql_thread_id blocking_trx_id,
-> b.sql_text
-> FROM information_schema.innodb_trx a,
-> performance_schema.events_statements_current b,
-> performance_schema.threads c
-> WHERE a.trx_mysql_thread_id = c.processlist_id
-> AND b.thread_id = c.thread_id;
Empty set (0.00 sec)

使用START TRANSACTION,自動提交將保持禁用狀態,直到你使用COMMIT或ROLLBACK結束事務。 自動提交模式然後恢復到之前的狀態(如果start transaction 前 autocommit = 1,則完成本次事務後 autocommit 還是 1。如果 start transaction 前 autocommit = 0,則完成本次事務後 autocommit 還是 0)

3.1.2 COMMIT

一般的MySQL語句都是直接針對資料庫表執行和編寫的。這就是所謂的隱含提交(implicit commit),即提交(寫或保存)操作是自動進行的。但是,在事務處理塊中,提交不會隱含地進行。為進行明確的提交,使用 COMMIT 語句,如下所示:

mysql> start transaction;
mysql> delete from persons where id = 200;
mysql> delete from students where person_id = 200;
mysql> commit;

當 COMMIT 或 ROLLBACK 語句執行後,事務會自動關閉

3.1.3 ROLLBACK

ROLLBACK 只能在一個事務處理內使用,即: 在執行一條 START TRANSACTION 命令之後。

mysql> begin transaction; # 開始事務
Query OK, 0 rows affected (0.00 sec)

mysql> insert into aaaa values(7);
Query OK, 1 rows affected (0.00 sec)

mysql> rollback; # 回滾
Query OK, 0 rows affected (0.00 sec)

3.1.4 從元資料庫INFORMATION_SCHEMA獲取和操作數據

元數據就是描述數據的數據,如資料庫名,表名,表大小,欄位名,欄位類型等。而MySQL中的INFORMATION_SCHEMA就是一個元資料庫,存儲著資料庫實例中的所有元數據信息。

關於INFORMATION_SCHEMA的官方定義:

INFORMATION_SCHEMA provides access to database metadata, information about the MySQL server such as the name of a database or table, the data type of a column, or access privileges

information_schema資料庫是MySQL自帶的, 確切說information_schema是MySQL的信息資料庫,保存著關於MySQL伺服器所維護的所有其他資料庫的信息。如資料庫名,資料庫的表,欄位的數據類型與訪問許可權等。

nformation_schema資料庫是MySQL系統自帶的資料庫,它提供了資料庫元數據的訪問方式。感覺information_schema就像是MySQL實例的一個百科全書,記錄了資料庫當中大部分我們需要了結的信息,比如字符集,許可權相關,資料庫實體對象信息,外檢約束,分區,壓縮表,表信息,索引信息,參數,優化,鎖和事物等等。通過information_schema我們可以窺透整個MySQL實例的運行情況,可以了結MySQL實例的基本信息,甚至優化調優,維護資料庫等,

下面對information_schema中的表進行簡單的分類:

1、 關於字符集和排序規則相關的系統表

  • CHARACTER_SETS : 保存所有Mysql可用的字符集。相當於命令:SHOW CHARACTER SET
  • COLLATIONS: 字符集對應的排序規則
  • COLLATION_CHARACTER_SET_APPLICABILITY: 顯示了哪種字符集適用於哪種排序方式。這些列相當於我們從SHOW COLLATION獲得的前兩個欄位。

2、 許可權相關的表

  • SCHEMA_PRIVILEGES:保存資料庫的許可權信息,該表是個內存表, 裡面的數據從mysql.db中載入。
  • TABLE_PRIVILEGES: 保存表的許可權信息。該表是個內存表, 裡面的數據從mysql.tables_priv中載入。
  • COLUMN_PRIVILEGES: 保存表中列的許可權信息。該表是個內存表, 裡面的數據從mysql.columns_priv中載入。
  • USER_PRIVILEGES:存儲用戶的許可權。該表是個內存表, 裡面的數據從mysql.user中載入。

3、 存儲資料庫系統實體對象的表

  • COLUMNS: 保存所有資料庫中表的列信息
  • INNODB_SYS_COLUMNS: 存放的是INNODB的元數據, 依賴於SYS_COLUMNS這個統計表而存在的。
  • ENGINES: 存儲Mysql支持的資料庫引擎類型,相當於命令SHOW ENGINES
  • EVENTS: 保存計劃事件(scheduled events)的信息,類似於定時作業
  • FILES: 這張表提供了有關在MySQL的表空間中的數據存儲的文件的信息,文件存儲的位置
  • PARAMETERS: 參數表存儲了一些存儲過程和方法的參數,以及存儲過程的返回值信息。存儲和方法在ROUTINES裡面存儲。
  • PLUGINS: MySQL的插件信息。其實SHOW PLUGINS本身就是通過這張表來獲取數據。
  • ROUTINES: 關於存儲過程和方法function的一些信息,不過這個信息是不包括用戶自定義的,只是系統的一些信息。
  • SCHEMATA: 供了實例下有多少個資料庫,以及資料庫默認的字符集
  • TABLES: 保存數據表信息。類似show tables。
  • TRIGGERS: 記錄的就是觸發器的信息,包括所有的相關的信息。包括系統的和自己用戶創建的觸發器。
  • VIEWS: 視圖的信息,包括系統的和用戶的基本視圖信息

4、 約束外鍵等相關的表

  • REFERENTIAL_CONSTRAINTS: 這個表提供的外鍵相關的信息,而且只提供外鍵相關信息
  • TABLE_CONSTRAINTS: 保存表的約束信息
  • INNODB_SYS_FOREIGN_COLS: 這個表也是存儲的INNODB關於外鍵的元數據信息和INNODB_SYS_FOREIGN 存儲的信息是一致的
  • INNODB_SYS_FOREIGN: 存儲的INNODB關於外鍵的元數據信息和SYS_FOREIGN_COLS 存儲的信息是一致的,只不過是單獨對於INNODB來說的
  • KEY_COLUMN_USAGE: 資料庫中所有有約束的列都會存下下來,也會記錄下約束的名字和類別

5、 關於資料庫管理的表

  • GLOBAL_STATUS: 保存MySQL的全局狀態。全局是相對於Session而言的,Session是指單個Mysql連接,全局可以理解為自從Mysql啟動以來,所有的連接,產生的狀態。
  • GLOBAL_VARIABLES: 保存MySQL的全局參數。

狀態(status)是隨著MySQL的運行,發生變化的。參數(variable)只有資料庫管理員主動修改,才會變化的。

  • SESSION_STATUS: 保存SESSION時的資料庫狀態,類似於GLOBAL_STATUS
  • SESSION_VARIABLES: 保存SESSION的資料庫變數,類似於GLOBAL_BARIABLES
  • PARTITIONS: MySQL分區表相關的信息,通過這張表我們可以查詢到分區的相關信息(資料庫中已分區的表,以及分區表的分區和每個分區的數據信息)
  • PROCESSLIST:show processlist其實就是從這個表拉取數據。由於是一個內存表,所以我們相當於在內存中查詢一樣,這些操作都是很快的。
  • INNODB_CMP_PER_INDEX,INNODB_CMP_PER_INDEX_RESET:這兩個表存儲的是關於壓縮INNODB信息表的時候的相關信息,有關整個表和索引信息都有
  • INNODB_CMPMEM,INNODB_CMPMEM_RESET: 這兩個表是存放關於MySQL INNODB的壓縮頁的buffer pool信息,但是要注意一點的就是,用這兩個表來收集所有信息的表的時候,是會對性能造成嚴重的影響的,所以說默認是關閉狀態的。
  • INNODB_BUFFER_POOL_STATS: 表提供有關INNODB 的buffer pool相關信息,和show engine innodb status提供的信息是相同的。也是show engine innodb status的信息來源。
  • INNODB_BUFFER_PAGE_LRU,INNODB_BUFFER_PAGE: 維護了INNODB LRU LIST的相關信息
  • INNODB_SYS_DATAFILES: 這張表就是記錄的表的文件存儲的位置和表空間的一個對應關係(INNODB)
  • INNODB_TEMP_TABLE_INFO:
  • INNODB_METRICS: 提供INNODB的各種的性能指數,收集的是MySQL的系統統計信息。這些統計信息都是可以手動配置打開還是關閉的。有以下參數都是可以控制的:innodb_monitor_enable, innodb_monitor_disable, innodb_monitor_reset, innodb_monitor_reset_all。
  • INNODB_SYS_VIRTUAL:表存儲的是INNODB表的虛擬列的信息,
  • INNODB_CMP,INNODB_CMP_RESET: 存儲的是關於壓縮INNODB信息表的時候的相關信息。

6、 關於表信息和索引信息的一些表

  • TABLES: 記錄的資料庫中表的信息,其中包括系統資料庫和用戶創建的資料庫。show table status like test1G的來源就是這個表;
  • TABLESPACES: 標註活躍表空間,這個表是不提供關於innodb的表空間信息。
  • INNODB_SYS_TABLES: 這張表依賴的是SYS_TABLES數據字典中拉取出來的。此表提供了有關表格的格式和存儲特性,包括行格式,壓縮頁面大小位級別的信息。提供的是關於INNODB的表空間信息,其實和SYS_TABLESPACES 中的INNODB信息是一致的。
  • STATISTICS: 這個表提供的是關於表的索引信息,所有索引的相關信息。
  • INNODB_SYS_INDEXES: 提供相關INNODB表的索引的相關信息,和SYS_INDEXES 這個表存儲的信息基本是一樣的,只不過後者提供的是所有存儲引擎的索引信息,前者只提供INNODB表的索引信息。
  • INNODB_SYS_TABLESTATS: 這個表非常重要,記錄的是MySQL的INNODB表信息以及MySQL優化器會預估SQL選擇合適的索引信息,其實就是MySQL資料庫的統計信息。這個表的記錄是記錄在內存當中的,是一個內存表,每次重啟後就會重新記錄,所以只能記錄從上次重啟後的資料庫統計信息。有了這個表,我們對於索引的維護就更加方便了,我們可以查詢索引的使用次數,方便清理刪除不常用的索引,提高表的更新插入等效率,節省磁碟空間。
  • INNODB_SYS_FIELDS: 這個表記錄的是INNODB的表索引欄位信息,以及欄位的排名
  • INNODB_FT_CONFIG: 這張表存的是全文索引的信息
  • INNODB_FT_DEFAULT_STOPWORD: 這個表存放的是stopword 的信息,是和全文索引匹配起來使用的,和innodb的 INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD 是相同的,這個STOPWORD必須是在創建索引之前創建,而且必須指定欄位為varchar。stopword 也就是我們所說的停止詞,全文檢索時,停止詞列表將會被讀取和檢索,在不同的字符集和排序方式下,會造成命中失敗或者找不到此數據,這取決於停止詞的不同的排序方式。我們可以使用這個功能篩選不必要欄位。
  • INNODB_FT_INDEX_TABLE: 這個表存儲的是關於INNODB表有全文索引的索引使用信息的,同樣這個表也是要設置innodb_ft_aux_table以後纔能夠使用的,一般情況下是空的
  • INNODB_FT_INDEX_CACHE: 這張表存放的是插入前的記錄信息,也是為了避免DML時候昂貴的索引重組

7、 關於MySQL優化相關的表

  • OPTIMIZER_TRACE: 提供的是優化跟蹤功能產生的信息.
  • PROFILING: SHOW PROFILE可以深入的查看伺服器執行語句的工作情況。以及也能幫助你理解執行語句消耗時間的情況。一些限制是它沒有實現的功能,不能查看和剖析其他連接的語句,以及剖析時所引起的消耗。
  • INNODB_FT_BEING_DELETED,INNODB_FT_DELETED: INNODB_FT_BEING_DELETED 這張表是INNODB_FT_DELETED的一個快照,只在OPTIMIZE TABLE 的時候才會使用。

8、 關於MySQL事物和鎖的相關的一些表

  • INNODB_LOCKS: 現在獲取的鎖,但是不含沒有獲取的鎖,而且只是針對INNODB的。
  • INNODB_LOCK_WAITS: 系統鎖等待相關信息,包含了阻塞的一行或者多行的記錄,而且還有鎖請求和被阻塞改請求的鎖信息等
  • INNODB_TRX: 包含了所有正在執行的的事物相關信息(INNODB),而且包含了事物是否被阻塞或者請求鎖。

3.2 一致性(consistency)

MySQL通過如下策略完成事務的一致性:

  • InnoDB doublewrite buffer。
  • InnoDB crash recovery。

3.2.1 InnoDB doublewrite buffer(雙寫緩衝)

3.2.1.1 什麼是 InnoDB doublewrite buffer?

[官方定義]
InnoDB uses a file flush technique called doublewrite. Before writing pages to the data files, InnoDB first writes them to a contiguous area called the doublewrite buffer. Only after the write and the flush to the doublewrite buffer have completed, does InnoDB write the pages to their proper positions in the data file. If there is an operating system, storage subsystem, or mysqld process crash in the middle of a page write, InnoDB can later find a good copy of the page from the doublewrite buffer during crash recovery.

InnoDB使用了一種叫做doublewrite的特殊文件flush技術,在把pages寫到date files之前,InnoDB先把它們寫到一個叫doublewrite buffer的連續區域內,在寫doublewrite buffer完成後,InnoDB才會把pages寫到data file的適當的位置。如果在寫page的過程中發生意外崩潰,InnoDB在稍後的恢復過程中在doublewrite buffer中找到完好的page副本用於恢復。

3.2.1.2 InnoDB doublewrite buffe解決的問題

資料庫,操作系統和磁碟讀寫的基本單位是塊,也可以稱之為(page size)block size。資料庫的塊一般為8K,16K;而OS的塊則一般為4K;IO塊則更小,linux內核要求IO block size<=OS block size。

磁碟IO除了IO block size,還有一個概念是扇區(IO sector),扇區是磁碟物理操作的基本單位,而IO 塊是磁碟操作的邏輯單位,一個IO塊對應一個或多個扇區,扇區大小一般為512個位元組。

各個塊大小的關係如下: DB block > OS block >= IO block > 磁碟 sector,而且他們之間保持了整數倍的關係。

下面小編系統各個塊的大小(MySQL為5.7,OS以Centos7):

MySQL block size

mysql> show variables like innodb_page_size;
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)

OS block

[root@ecs-prod-my57-fserp-ro ~]# getconf PAGESIZE
4096

IO block size

[root@ecs-prod-my57-fserp-ro ~]# blockdev --getbsz /dev/vdb
4096

sector size

root@ecs-prod-my57-fserp-ro ~]# fdisk -l | grep Sector
Sector size (logical/physical): 512 bytes / 512 bytes

從上面的結果可以看到DB page=4*OS page=4*IO page=32*sector size

由於任何DB page的寫入,最終都會轉為sector的寫入,如果在寫磁碟的過程中,出現異常重啟,就可能會發生一個DB頁只寫了部分sector到磁碟,進而出現頁斷裂的情況 

InnoDB的page size一般是16KB,其數據校驗也是針對這16KB來計算的,將數據寫入到磁碟是以page為單位進行操作的。操作系統寫文件是以4KB作為單位的,那麼每寫一個InnoDB的page到磁碟上,操作系統需要寫4個塊。而計算機硬體和操作系統,在極端情況下(比如斷電)往往並不能保證這一操作的原子性,16K的數據,寫入4K時,發生了系統斷電或系統崩潰,只有一部分寫是成功的,這種情況下就是partial page write(部分頁寫入)問題。這時page數據出現不一樣的情形,從而形成一個"斷裂"的page,使數據產生混亂。這個時候InnoDB對這種塊錯誤是無 能為力的.

有人會認為系統恢復後,MySQL可以根據redo log進行恢復,而MySQL在恢復的過程中是檢查page的checksum,checksum就是pgae的最後事務號,發生partial page write問題時,page已經損壞,找不到該page中的事務號,就無法恢復。

doublewrite buffer是InnoDB在tablespace上的128個頁(2個區)大小是2MB。為瞭解決 partial page write問題,當MySQL將臟數據flush到data file的時候, 先使用memcopy將臟數據複製到內存中的doublewrite buffer,之後通過doublewrite buffer再分2次,每次寫入1MB到共享表空間,然後馬上調用fsync函數,同步到磁碟上,避免緩衝帶來的問題,在這個過程中,doublewrite是順序寫,開銷並不大,在完成doublewrite寫入後,再將double write buffer寫入各表空間文件,這時是離散寫入,詳情如下: 。

所以在正常的情況下, MySQL寫數據page時,會寫兩遍到磁碟上,第一遍是寫到doublewrite buffer,第二遍是從doublewrite buffer寫到真正的數據文件中。如果發生了極端情況(斷電),InnoDB再次啟動後,發現了一個page數據已經損壞,那麼此時就可以從doublewrite buffer中進行數據恢復了。

3.2.2 InnoDB crash recovery

注: 本節內容來自網易雲社區,由網易資料庫和大數據資深專家蔣鴻翔分享。

InnoDB的數據恢復是一個很複雜的過程,在其恢復過程中,需要redo log、binlog、undo log等參與,這裡把InnoDB的恢復過程主要劃分為兩個階段,第一階段主要依賴於redo log的恢復,而第二階段,恰恰需要binlog和undo log的共同參與.

3.2.2.1 第一階段: redo log恢復

資料庫啟動後,InnoDB會通過redo log找到最近一次checkpoint的位置,然後根據checkpoint相對應的LSN開始,獲取需要重做的日誌,接著解析獲取的日誌並且保存到一個哈希表中,最後通過遍歷哈希表中的redo log信息,讀取相關頁進行恢復。

大致過程如下:

  • 打開Redo Logs和系統表空間文件(ibdataN)
  • 讀取並從中找到最大的Checkpoint LSN
  • 從最近的Checkpoint 開始往後掃描Redo Log
  • 如果能夠找到Redo Log記錄,說明還有數據頁的更改沒有刷新到數據文件上,啟動Crash Recovery,使用Redo Log來恢複數據的一致性

InnoDB的checkpoint信息保存在日誌文件中,即ib_logfile0的開始2048個位元組中,checkpoint有兩個,交替更新,checkpoint與日誌文件的關係如下圖:

checkpoint信息分別保存在ib_logfile0的512位元組和1536位元組處,每個checkpoint默認大小為512位元組,InnoDB的checkpoint主要有3部分信息組成:

  • checkpoint no

checkpoint no主要保存的是checkpoint號,因為InnoDB有兩個checkpoint(上圖中的checkpoint1和checkpoint2),通過checkpoint號來判斷哪個checkpoint最新

  • checkpoint lsn

checkpoint lsn主要記錄了產生該checkpoint是flush的LSN(Log Sequence Number),確保在該LSN前面的數據頁都已經落盤,不再需要通過redo log進行恢復。

  • checkpoint offset

checkpoint offset主要記錄了該checkpoint產生時,redo log在ib_logfile中的偏移量,通過該offset位置就可以找到需要恢復的redo log開始位置。

通過以上checkpoint的信息,我們可以簡單得到需要恢復的redo log的位置,然後通過順序掃描該redo log來讀取數據,比如我們通過checkpoint定位到開始恢復的redo log位置在ib_logfile1中的某個位置,那麼整個redo log掃描的過程可能是這樣的:

  • 從ib_logfile1的指定位置開始讀取redo log,每次讀取4 * page_size的大小,這裡我們默認頁面大小為16K,所以每次讀取64K的redo log到緩存中,redo log每條記錄(block)的大小為512位元組
  • 讀取到緩存中的redo log通過解析、驗證等一系列過程後,把redo log的內容部分保存到用於恢復的緩存recv_sys->buf,保存到恢復緩存中的每條信息主要包含兩部分:(space,offset)組成的位置信息和具體redo log的內容,我們稱之為body
  • 同時保存在恢復緩存中的redo信息會根據space,offset計算一個哈希值後保存到一個哈希表(recv_sys->addr_hash)中,相同的哈希值不同(space,offset)用鏈表存儲,相同的(space,offset)用列表保存,可能部分事務比較大,redo信息一個block不能保存,所以,每個body中可以用鏈錶鏈接多body的值
  • redo log被保存到哈希表中之後,InnoDB就可以開始進行數據恢復,只需要輪詢哈希表中的每個節點獲取redo信息,根據(space,offset)讀取指定頁面後進行日誌覆蓋。

redo log全部被解析並且apply完成,整個InnoDB recovery的第一階段也就結束了,在該階段中,所有已經被記錄到redo log但是沒有完成數據刷盤的記錄都被重新落盤。然而,InnoDB單靠redo log的恢復是不夠的,這樣還是有可能會丟失數據(或者說造成主從數據不一致),因為在事務提交過程中,寫binlog和寫redo log提交是兩個過程,寫binlog在前而redo提交在後,如果MySQL寫完binlog後,在redo提交之前發生了宕機,這樣就會出現問題:binlog中已經包含了該條記錄,而redo沒有持久化。binlog已經落盤就意味著slave上可以apply該條數據,redo沒有持久化則代表了master上該條數據並沒有落盤,也不能通過redo進行恢復。這樣就造成了主從數據的不一致,換句話說主上丟失了部分數據,那麼MySQL又是如何保證在這樣的情況下,數據還是一致的?這就需要進行第二階段恢復。

3.2.2.2 第二階段 使用binlog和undo log恢複數據

在第二階段恢復中,需要用到binlog和undo log,其實在該階段的恢復中,也被劃分成兩部分,第一部分,根據binlog獲取所有可能沒有提交事務的xid列表;第二部分,根據undo中的信息構造所有未提交事務鏈表,最後通過上面兩部分協調判斷事務是否可以提交。

[圖3-2-2-2-0]根據binlog獲取xid列表

如上圖中所示,MySQL在第二階段恢復的時候,先會去讀取最後一個binlog文件的所有event信息,然後把xid保存到一個列表中,然後進行第二部分的恢復,如下:

[圖3-2-2-2-1]基於undo構造事務鏈表

InnoDB當前版本有128個回滾段,每個回滾段中保存了undo log的位置指針,通過掃描undo日誌,我們可以構造出還未被提交的事務鏈表(存在於insert_undo_list和update_undo_lsit中的事務都是未被提交的),所以通過起始頁(0,5)下的solt信息可以定位到回滾段,然後根據回滾段下的undo的slot定位到undo頁,把所有的undo信息構建一個undo_list,然後通過undo_list再創建未提交事務鏈表trx_sys->trx_list。 基於上面兩步, 我們已經構建了xid列表和未提交事務列表,那麼在這些未提交事務列表中的事務,哪些需要被提交?哪些又該回滾?判斷條件很簡單:凡是xid在通過binlog構建的xid列表中存在的事務,都需要被提交,換句話說,所有已經記錄binlog的事務,需要被提交,而剩下那些沒有記錄binlog的事務,則需要被回滾。

3.2.2.3 InnoDB crash recovery流程圖

3.3 隔離性

Innodb引擎通過以下方式實現事務的隔離性:

  • 事務自動提交(Autocommit)配置
  • 隔離級別設置
  • 不同隔離級別和相對應的鎖機制

3.3.1 事務自動提交(Autocommit)配置

關於事務自動提交(Autocommit)配置請參閱3.1.1節

3.3.2 隔離級別設置

當資料庫上有多個事務同時執行的時候,就可能出現臟讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantom read)的問題,為瞭解決這些問題,就有了「隔離級別」的概念。

隔離性其實比想像的要複雜,簡單來說,隔離性的作用就是保證在多個事務並行執行時,相互之間不會影響;比如一個事務的修改對其他事務是不可見的,好似多個事務是串列執行的。

在SQL 92標準定義了四個隔離級別,分別包括:讀未提交(READ UNCOMMITTED)、讀已提交(READ COMMITTED)、可重複讀(REPEATABLE READ)和串列化(SERIALIZABLE)。但是很少有資料庫廠商遵循這些標準,比如Oracle資料庫就不支持READ UNCOMMITTED和REPEATABLE READ隔離級別。而MySQL支持這全部4種隔離級別。MySQL是支持REPEATABLE READ隔離級別,在這個級別下可以解決「不可重複讀」的問題,是真正滿足ACID中隔離性的要求的,但鎖粒度比READ COMMITTED隔離級別要重。在READ COMMITTED隔離級別下是無法滿足隔離性要求的,所以MySQL默認是REPEATABLE READ隔離級別。

下面對各個隔離級別進行詳解:

3.3.2.1 串列化(SERIALIZABLE)

在這個級別,它通過強制事務串列執行,避免了前面說的一系列問題。簡單來說就是對同一行記錄,「寫」會加「寫鎖」,「讀」會加「讀鎖」。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。所以可能導致大量的超時和鎖爭用的問題,實際應用中也很少在本地事務中使用SERIALIABLE隔離級別,主要應用在InnoDB存儲引擎的分散式事務中。

SERIALIZABLE事務隔離級別最嚴厲,在進行查詢時就會對相關行加上共享鎖,其他事務對加「共享鎖」的行只能進行讀操作,而不能進行寫操作。一旦有事務對行加了「獨佔鎖」,其他事務連讀都不能操作。

3.3.2.2 可重複讀(REPEATABLE READ)

可重複讀級別解決了「不可重複讀」及「幻讀」問題,就是在同一個事務執行期間前後看到的數據必須一致,MySQL默認使用這個級別。簡單說就是在同一個事務中發出同一個SELECT語句兩次或更多次,那麼產生的結果數據集總是相同的。因此,使用可重複讀隔離級別的事務可以多次檢索同一行集,並對它們執行任意操作,直到提交或回滾操作終止該事務。

基於讀寫鎖,可重複讀只能解決讀讀的並行執行,並不能使讀寫、寫寫、寫讀的並行執行(不明白就去事務原理再看一遍)。注意這是說在讀寫鎖的情況下,但是現在RDBMS都使用MVCC機制了,所以會有所不同。在RR隔離級別,針對當前讀(還有快照讀),保證對讀取到的記錄加讀鎖(行鎖),同時保證對讀取的範圍加鎖,也就是說新的滿足查詢條件的記錄不能夠插入 (由間隙鎖保證),所以解決了「不可重複讀」及「幻讀」現象。

3.3.2.3 讀已提交(READ COMMITTED)

在這個級別,能滿足前面提到的隔離性的簡單定義:一個事務開始時,只能「看見」已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的,只有事務提交之後才對於其他事務可見。這個級別有時候也叫「不可重複讀(non-repeatable read)」,因為兩次執行同樣的查詢,可能會得到不一樣的結果。為什麼會出現「不可重複讀」問題呢?從下面的分析中找答案。

RC隔離級別,針對當前讀(還有快照讀)保證對讀取到的記錄加讀鎖 (記錄鎖)。因為在RC的事務隔離級別下,除了唯一性的約束檢查和外鍵約束的檢查需要Gap Lock外,InnoDB存儲引擎不會使用Gap Lock,所以會產生「不可重複讀」問題。而不可重複讀會破壞事務隔離性要求,也就是一個事務的修改操作對其他事務可見了。

3.3.2.4 READ-UNCOMMITTED

在這個級別,只加寫鎖,讀不加鎖。那麼就會產生這三種情況:讀讀可以並行、讀寫可以並行、寫讀可以並行(只有寫寫不可以並行)。所以在這個級別,一個事務的修改,即使沒有提交,對其他事務也都是可見的。一個事務可以讀取另一個事務未提交的數據,這也被稱為「臟讀(dirty read)」,同樣破壞了事務隔離性要求,一個事務的修改對其他事務可見了。這個級別會導致很多問題,如一個事務可以讀取到另一個事務的中間狀態,且從性能上來說,READ UNCOMMITTED不會比其他的級別好太多,但卻缺乏其他級別的很多好處,除非真的有非常必要的理由,在實際應用中一般很少使用。

3.3.3 事務隔離級別和數據的對應關係

3.3.4 事務隔離的實現

理解了事務的隔離級別,我們再來看看事務隔離具體是怎麼實現的。這裡我們展開說明「可重複讀」。

在實現上,資料庫裡面會創建一個視圖(read-view),訪問的時候以視圖的邏輯結果為準。在「可重複讀」隔離級別下,這個視圖是在事務啟動時創建的,整個事務存在期間都用這個視圖,即使有其他事務修改了數據,事務中看到的數據仍然跟在啟動時看到的一樣。在「讀提交」隔離級別下,這個視圖是在每個 SQL 語句開始執行的時候創建的。這裡需要注意的是,「讀未提交」隔離級別下直接返回記錄上的最新值,沒有視圖概念;而「串列化」隔離級別下直接用加鎖的方式來避免並行訪問。

在 MySQL 中,實際上每條記錄在更新的時候都會同時記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。

假設一個值從 1 被按順序改成了 2、3、4,在回滾日誌裡面就會有類似下面的記錄。

當前值是 4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的 read-view。如圖中看到的,在視圖 A、B、C 裡面,這一個記錄的值分別是 1、2、4,同一條記錄在系統中可以存在多個版本,就是資料庫的多版本並發控制(MVCC)。對於 read-view A,要得到 1,就必須將當前值依次執行圖中所有的回滾操作得到。

同時你會發現,即使現在有另外一個事務正在將 4 改成 5,這個事務跟 read-view A、B、C 對應的事務是不會衝突的。你一定會問,回滾日誌總不能一直保留吧,什麼時候刪除呢?答案是,在不需要的時候才刪除。也就是說,系統會判斷,當沒有事務再需要用到這些回滾日誌時,回滾日誌會被刪除。 什麼時候纔不需要了呢?就是當系統裏沒有比這個回滾日誌更早的 read-view 的時候。

基於上面的說明,我們來討論一下為什麼建議你盡量不要使用長事務。

長事務意味著系統裡面會存在很老的事務視圖。由於這些事務隨時可能訪問資料庫裡面的任何數據,所以這個事務提交之前,資料庫裡面它可能用到的回滾記錄都必須保留,這就會導致大量佔用存儲空間。

在 MySQL 5.5 及以前的版本,回滾日誌是跟數據字典一起放在 ibdata 文件裏的,即使長事務最終提交,回滾段被清理,文件也不會變小,當然這個問題在 MySQL 5.7 中已經徹底解決了。我見過數據只有 20GB,而回滾段有 200GB 的庫。最終只好為了清理回滾段,重建整個庫。 除了對回滾段的影響,長事務還佔用鎖資源,也可能拖垮整個庫。

3.3.5 Innodb中的事務隔離級別和鎖的關係

注: 下面的內容來自美團技術博客<>

我們都知道事務的幾種性質,資料庫為了維護這些性質,尤其是一致性和隔離性,一般使用加鎖這種方式。同時資料庫又是個高並發的應用,同一時間會有大量的並發訪問,如果加鎖過度,會極大的降低並發處理能力。所以對於加鎖的處理,可以說就是資料庫對於事務處理的精髓所在。這裡通過分析MySQL中InnoDB引擎的加鎖機制,來拋磚引玉,讓讀者更好的理解,在事務處理中資料庫到底做了什麼。

3.3.5.1 一次封鎖or兩段鎖?

因為有大量的並發訪問,為了預防死鎖,一般應用中推薦使用一次封鎖法,就是在方法的開始階段,已經預先知道會用到哪些數據,然後全部鎖住,在方法運行之後,再全部解鎖。這種方式可以有效的避免循環死鎖,但在資料庫中卻不適用,因為在事務開始階段,資料庫並不知道會用到哪些數據。

資料庫遵循的是兩段鎖協議,將事務分成兩個階段,加鎖階段和解鎖階段(所以叫兩段鎖)

  • 加鎖階段:在該階段可以進行加鎖操作。在對任何數據進行讀操作之前要申請並獲得S鎖(共享鎖,其它事務可以繼續加共享鎖,但不能加排它鎖),在進行寫操作之前要申請並獲得X鎖(排它鎖,其它事務不能再獲得任何鎖)。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。
  • 解鎖階段:當事務釋放了一個封鎖以後,事務進入解鎖階段,在該階段只能進行解鎖操作不能再進行加鎖操作。

這種方式雖然無法避免死鎖,但是兩段鎖協議可以保證事務的並發調度是串列化(串列化很重要,尤其是在數據恢復和備份的時候)的。

3.3.5.2 MySQL中鎖的種類

MySQL中鎖的種類很多,有常見的表鎖和行鎖,也有新加入的Metadata Lock等等,表鎖是對一整張表加鎖,雖然可分為讀鎖和寫鎖,但畢竟是鎖住整張表,會導致並發能力下降,一般是做ddl處理時使用。

行鎖則是鎖住數據行,這種加鎖方法比較複雜,但是由於只鎖住有限的數據,對於其它數據不加限制,所以並發能力強,MySQL一般都是用行鎖來處理並發事務。這裡主要討論的也就是行鎖。

3.3.5.3 Read Committed(讀取提交內容)

在RC級別中,數據的讀取都是不加鎖的,但是數據的寫入、修改和刪除是需要加鎖的。效果如下

MySQL> show create table class_teacher G
Table: class_teacher
Create Table: CREATE TABLE `class_teacher` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`teacher_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_teacher_id` (`teacher_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)
MySQL> select * from class_teacher;
+----+--------------+------------+
| id | class_name | teacher_id |
+----+--------------+------------+
| 1 | 初三一班 | 1 |
| 3 | 初二一班 | 2 |
| 4 | 初二二班 | 2 |
+----+--------------+------------+

由於MySQL的InnoDB默認是使用的RR級別,所以我們先要將該session開啟成RC級別,並且設置binlog的模式

SET session transaction isolation level read committed;
SET SESSION binlog_format = ROW;(或者是MIXED)

為了防止並發過程中的修改衝突,事務A中MySQL給teacher_id=1的數據行加鎖,並一直不commit(釋放鎖),那麼事務B也就一直拿不到該行鎖,wait直到超時。

這時我們要注意到,teacher_id是有索引的,如果是沒有索引的class_name呢?update class_teacher set teacher_id=3 where class_name = 『初三一班』; 那麼MySQL會給整張表的所有數據行的加行鎖。這裡聽起來有點不可思議,但是當sql運行的過程中,MySQL並不知道哪些數據行是 class_name = 『初三一班』的(沒有索引嘛),如果一個條件無法通過索引快速過濾,存儲引擎層面就會將所有記錄加鎖後返回,再由MySQL Server層進行過濾。

但在實際使用過程當中,MySQL做了一些改進,在MySQL Server過濾條件,發現不滿足後,會調用unlock_row方法,把不滿足條件的記錄釋放鎖 (違背了二段鎖協議的約束)。這樣做,保證了最後只會持有滿足條件記錄上的鎖,但是每條記錄的加鎖操作還是不能省略的。可見即使是MySQL,為了效率也是會違反規範的。(參見《高性能MySQL》中文第三版p181)

這種情況同樣適用於MySQL的默認隔離級別RR。所以對一個數據量很大的表做批量修改的時候,如果無法使用相應的索引,MySQL Server過濾數據的的時候特別慢,就會出現雖然沒有修改某些行的數據,但是它們還是被鎖住了的現象。

3.3.5.4 Repeatable Read(可重讀)

這是MySQL中InnoDB默認的隔離級別。我們姑且分「讀」和「寫」兩個模塊來講解。

讀就是可重讀,可重讀這個概念是一事務的多個實例在並發讀取數據時,會看到同樣的數據行,有點抽象,我們來看一下效果。

RC(不可重讀)模式下的展現

事務B修改id=1的數據提交之後,事務A同樣的查詢,後一次和前一次的結果不一樣,這就是不可重讀(重新讀取產生的結果不一樣)。這就很可能帶來一些問題,那麼我們來看看在RR級別中MySQL的表現:

我們注意到,當teacher_id=1時,事務A先做了一次讀取,事務B中間修改了id=1的數據,並commit之後,事務A第二次讀到的數據和第一次完全相同。所以說它是可重讀的。那麼MySQL是怎麼做到的呢?這裡姑且賣個關子,我們往下看。

3.3.5.5 不可重複讀和幻讀的區別

很多人容易搞混不可重複讀和幻讀,確實這兩者有些相似。但不可重複讀重點在於update和delete,而幻讀的重點在於insert。

如果使用鎖機制來實現這兩種隔離級別,在可重複讀中,該sql第一次讀取到數據後,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重複讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這麼做可以有效的避免幻讀、不可重複讀、臟讀等問題,但會極大的降低資料庫的並發能力。

所以說不可重複讀和幻讀最大的區別,就在於如何通過鎖機制來解決他們產生的問題。

上文說的,是使用悲觀鎖機制來處理這兩種問題,但是MySQL、ORACLE、PostgreSQL等成熟的資料庫,出於性能考慮,都是使用了以樂觀鎖為理論基礎的MVCC(多版本並發控制)來避免這兩種問題。

3.3.5.5 悲觀鎖和樂觀鎖

  • 悲觀鎖

正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。

在悲觀鎖的情況下,為了保證事務的隔離性,就需要一致性鎖定讀。讀取數據時給加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。

  • 樂觀鎖

相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。

而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 「version」 欄位來實現。讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與資料庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於資料庫表當前版本號,則予以更新,否則認為是過期數據。

要說明的是,MVCC的實現沒有固定的規範,每個資料庫都會有不同的實現方式,這裡討論的是InnoDB的MVCC。

3.3.5.6 MVCC在MySQL的InnoDB中的實現

在InnoDB中,會在每行數據後添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(或者被刪除)。 在實際操作中,存儲的並不是時間,而是事務的版本號,每開啟一個新事務,事務的版本號就會遞增。 在可重讀Repeatable reads事務隔離級別下:

  • SELECT時,讀取創建版本號<=當前事務版本號,刪除版本號為空或>當前事務版本號。
  • INSERT時,保存當前事務版本號為行的創建版本號
  • DELETE時,保存當前事務版本號為行的刪除版本號
  • UPDATE時,插入一條新紀錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行

通過MVCC,雖然每行記錄都需要額外的存儲空間,更多的行檢查工作以及一些額外的維護工作,但可以減少鎖的使用,大多數讀操作都不用加鎖,讀數據操作很簡單,性能很好,並且也能保證只會讀取到符合標準的行,也只鎖住必要行。

我們不管從資料庫方面的教課書中學到,還是從網路上看到,大都是上文中事務的四種隔離級別這一模塊列出的意思,RR級別是可重複讀的,但無法解決幻讀,而只有在Serializable級別才能解決幻讀。於是我就加了一個事務C來展示效果。在事務C中添加了一條teacher_id=1的數據commit,RR級別中應該會有幻讀現象,事務A在查詢teacher_id=1的數據時會讀到事務C新加的數據。但是測試後發現,在MySQL中是不存在這種情況的,在事務C提交後,事務A還是不會讀到這條數據。可見在MySQL的RR級別中,是解決了幻讀的讀問題的。參見下圖

讀問題解決了,根據MVCC的定義,並發提交數據時會出現衝突,那麼衝突時如何解決呢?我們再來看看InnoDB中RR級別對於寫數據的處理

3.3.5.7 「讀」與「讀」的區別

可能有讀者會疑惑,事務的隔離級別其實都是對於讀數據的定義,但到了這裡,就被拆成了讀和寫兩個模塊來講解。這主要是因為MySQL中的讀,和事務隔離級別中的讀,是不一樣的。

我們且看,在RR級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,是不及時的數據,不是資料庫當前的數據!這在一些對於數據的時效特別敏感的業務中,就很可能出問題。

對於這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取資料庫當前版本數據的方式,叫當前讀 (current read)。很顯然,在MVCC中:

  • 快照讀:就是select

select * from table ….;

  • 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,處理的都是當前的數據,需要加鎖。

select * from table where ? lock in share mode;select * from table where ? for update;insert;update ;delete;事務的隔離級別實際上都是定義了當前讀的級別,MySQL為了減少鎖處理(包括等待其它鎖)的時間,提升並發能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些「當前讀」,就需要另外的模塊來解決了。

3.3.5.8 寫(」當前讀」)

事務的隔離級別中雖然只定義了讀數據的要求,實際上這也可以說是寫數據的要求。上文的「讀」,實際是講的快照讀;而這裡說的「寫」就是當前讀了。

為瞭解決當前讀中的幻讀問題,MySQL事務使用了Next-Key鎖。

Next-Key鎖

Next-Key鎖是行鎖和GAP(間隙鎖)的合併,行鎖上文已經介紹了,接下來說下GAP間隙鎖。

行鎖可以防止不同事務版本的數據修改提交時造成數據衝突的情況。但如何避免別的事務插入數據就成了問題。我們可以看看RR級別和RC級別的對比

  • RC級別:

  • RR級別:

通過對比我們可以發現,在RC級別中,事務A修改了所有teacher_id=30的數據,但是當事務Binsert進新數據後,事務A發現莫名其妙多了一行teacher_id=30的數據,而且沒有被之前的update語句所修改,這就是「當前讀」的幻讀。

RR級別中,事務A在update後加鎖,事務B無法插入新數據,這樣事務A在update前後讀的數據保持一致,避免了幻讀。這個鎖,就是Gap鎖。

MySQL中的實現:

在class_teacher這張表中,teacher_id是個索引,那麼它就會維護一套B+樹的數據關係,為了簡化,我們用鏈表結構來表達(實際上是個樹形結構,但原理相同)

如圖所示,InnoDB使用的是聚集索引,teacher_id身為二級索引,就要維護一個索引欄位和主鍵id的樹狀結構(這裡用鏈表形式表現),並保持順序排列。

Innodb將這段數據分成幾個個區間

  • (negative infinity, 5],
  • (5,30],
  • (30,positive infinity);

update class_teacher set class_name=『初三四班』 where teacher_id=30;不僅用行鎖,鎖住了相應的數據行;同時也在兩邊的區間,(5,30]和(30,positive infinity),都加入了gap鎖。這樣事務B就無法在這個兩個區間insert進新數據。

受限於這種實現方式,Innodb很多時候會鎖住不需要鎖的區間。如下所示:

update的teacher_id=20是在(5,30]區間,即使沒有修改任何數據,Innodb也會在這個區間加gap鎖,而其它區間不會影響,事務C正常插入。

如果使用的是沒有索引的欄位,比如update class_teacher set teacher_id=7 where class_name=『初三八班(即使沒有匹配到任何數據)』,那麼會給全表加入gap鎖。同時,它不能像上文中行鎖一樣經過MySQL Server過濾自動解除不滿足條件的鎖,因為沒有索引,則這些欄位也就沒有排序,也就沒有區間。除非該事務提交,否則其它事務無法插入任何數據。

行鎖防止別的事務修改或刪除,GAP鎖防止別的事務新增,行鎖和GAP鎖結合形成的的Next-Key鎖共同解決了RR級別在寫數據時的幻讀問題。

3.3.5.9 Serializable

這個級別很簡單,讀加共享鎖,寫加排他鎖,讀寫互斥。使用的悲觀鎖的理論,實現簡單,數據更加安全,但是並發能力非常差。如果你的業務並發的特別少或者沒有並發,同時又要求數據及時可靠的話,可以使用這種模式。

這裡要吐槽一句,不要看到select就說不會加鎖了,在Serializable這個級別,還是會加鎖的!

3.4 持久化

ACID中的持久化和具體的硬體配置有很大的關係,因為持久化時的性能和具體的伺服器CPU,網路,和硬碟有很大的關係,MySQL中跟持久化有關了的配置主要有:

  • doublewrite buffer的打開和關閉
  • innodb_flush_log_at_trx_commit的配置
  • sync_binlog的配置
  • innodb_file_per_table的配置
  • 存儲設備中的電池備份緩存
  • 運行MySQL資料庫的操作系統選擇,必須要支持fsync() 系統調用
  • 運行MySQL的伺服器電源使用UPS,保護MySQL伺服器和存儲設備不會因為斷電出現異常
  • 對於分散式應用程序,需要考慮應用程序和MySQL數據中心之間的網路

一般MySQL運行的操作系統選擇Linux,如Centos。如果是自建機房,那麼運行MySQL的伺服器盡量單獨一臺,且配置高一些,磁碟最好使用ssd。

下面著重講解MySQL關於持久化的配置

3.4.1 doublewrite buffer的打開和關閉

3.4.1.1 建議打開doublewrite buffer

InnoDB doublewrite 機制提高了InnoDB的可靠性,解決了InnoDB部分數據寫入失敗(即: partial page write頁斷裂)的問題, 保證了數據的一致性。

double write雖然是一個buffer, 但是它是開在物理文件上的一個buffer, 其實也就是file, 所以它會導致系統有更多的fsync操作, 而硬碟的fsync性能是很慢的, 從而降低mysql的整體性能。

不過 doublewrite buffer寫入磁碟共享表空間這個過程是連續存儲,是順序寫,性能非常高,(約佔寫的%10),犧牲一點寫性能來保證數據頁的完整還是很有必要的。所以一般建議打開MySQL的doublewrite buffer

3.4.1.2 監控double write工作負載

mysql> show global status like %dblwr%;
+----------------------------+---------+
| Variable_name | Value |
+----------------------------+---------+
| Innodb_dblwr_pages_written | 2432011 |
| Innodb_dblwr_writes | 188819 |
+----------------------------+---------+
2 rows in set (0.00 sec)

mysql> select (2432011/188819);
+------------------+
| (2432011/188819) |
+------------------+
| 12.8801 |
+------------------+
1 row in set (0.00 sec)

關注點:Innodb_dblwr_pages_written / Innodb_dblwr_writes

開啟doublewrite後,每次臟頁刷新必須要先寫doublewrite,而doublewrite存在於磁碟上的是兩個連續的區,每個區由連續的頁組成,一般情況下一個區最多有64個頁,所以一次IO寫入應該可以最多寫64個頁。

而根據以上系統Innodb_dblwr_pages_written與Innodb_dblwr_writes的比例來看,大概在12左右,遠遠還沒到64(如果約等於64,那麼說明系統的寫壓力非常大,有大量的臟頁要往磁碟上寫),所以從這個角度也可以看出,系統寫入壓力並不高。

3.4.1.3 關閉double write的場景

  • 海量DML
  • 不懼怕數據損壞和丟失
  • 系統寫負載成為主要負載

作為InnoDB的一個關鍵特性,doublewrite功能默認是開啟的

mysql> show variables like %double%;
+----------------------------------+----------------+
| Variable_name | Value |
+----------------------------------+----------------+
| innodb_doublewrite | ON |
| innodb_parallel_doublewrite_path | xb_doublewrite |
+----------------------------------+----------------+
2 rows in set (0.01 sec)

3.4.2 innodb_flush_log_at_trx_commit的配置

innodb_flush_log_at_trx_commit:是 InnoDB 引擎特有的,ib_logfile的刷新方式( ib_logfile:記錄的是redo log和undo log的信息), 其取值可以為0, 1, 2

innodb_flush_log_at_trx_commit=0: Innodb 中的Log Thread每隔1 秒鐘會將log buffer中的數據寫入到文件,同時還會通知文件系統進行文件同步的flush 操作,保證數據確實已經寫入到磁碟上面的物理文件。但是,每次事務的結束(commit 或者是rollback)並不會觸發Log Thread 將log buffer 中的數據寫入文件。所以,當設置為0 的時候,當MySQL Crash 和OS Crash 或者主機斷電之後,最極端的情況是丟失1 秒時間的數據變更。

innodb_flush_log_at_trx_commit=1: Innodb 的默認設置,表示在每次事務提交的時候,都把log buffer刷到文件系統中(os buffer)去,並且調用文件系統的「flush」操作將緩存刷新到磁碟上去。這樣的話,資料庫對IO的要求就非常高了,如果底層的硬體提供的IOPS比較差,那麼MySQL資料庫的並發很快就會由於硬體IO的問題而無法提升。

innodb_flush_log_at_trx_commit=2: 表示在每次事務提交的時候會把log buffer刷到文件系統中去,但並不會立即刷寫到磁碟。如果只是MySQL資料庫掛掉了,由於文件系統沒有問題,那麼對應的事務數據並沒有丟失。只有在資料庫所在的主機操作系統損壞或者突然掉電的情況下,資料庫的事務數據可能丟失1秒之類的事務數據。這樣的好處,減少了事務數據丟失的概率,而對底層硬體的IO要求也沒有那麼高(log buffer寫到文件系統中,一般只是從log buffer的內存轉移的文件系統的內存緩存中,對底層IO沒有壓力)。

3.4.3 sync_binlog的配置

sync_binlog: 是MySQL 的二進位日誌(binary log)同步到磁碟的頻率。其可取的值是: 0 ~ N

sync_binlog=0: 當事務提交之後,MySQL不做fsync之類的磁碟同步指令刷新binlog_cache中的信息到磁碟,而讓Filesystem自行決定什麼時候來做同步,或者cache滿了之後才同步到磁碟。這個是性能最好的。

sync_binlog=1: 當每進行1次事務提交之後,MySQL將進行一次fsync之類的磁碟同步指令來將binlog_cache中的數據強制寫入磁碟。

sync_binlog=N: 當每進行n次事務提交之後,MySQL將進行一次fsync之類的磁碟同步指令來將binlog_cache中的數據強制寫入磁碟。

大多數情況下,對數據的一致性並沒有很嚴格的要求,所以並不會把 sync_binlog 配置成 1. 為了追求高並發,提升性能,可以設置為 100 或直接用 0. 而和 innodb_flush_log_at_trx_commit 一樣,對於支付服務這樣的應用,還是比較推薦 sync_binlog = 1.

3.4.4 innodb_file_per_table配置

Innodb存儲引擎可將所有數據存放於ibdata*的共享表空間,也可將每張表存放於獨立的.ibd文件的獨立表空間。共享表空間以及獨立表空間都是針對數據的存儲方式而言的。

3.4.4.1 共享表空間

某一個資料庫的所有的表數據,索引文件全部放在一個文件中,默認這個共享表空間的文件路徑在data目錄下。 默認的文件名為:ibdata1 初始化為10M。這個表空間可以由很多個文件組成,一個表可以跨多個文件存在,所以其大小限制不再是文件大小的限制,而是其自身的限制。從Innodb的官方文檔中可以看到,其表空間的最大限制為64TB,也就是說,Innodb的單表限制基本上也在64TB左右了,當然這個大小是包括這個表的所有索引等其他相關數據。

  • 共享表空間的優點

1) 可以將表空間分成多個文件存放到各個磁碟上(表空間文件大小不受表大小的限制,如一個表可以分佈在不同的文件上)。2) 數據和文件放在一起方便管理。

  • 共享表空間的缺點

1) 所有的數據和索引存放到一個文件,雖然可以把一個大文件分成多個小文件,但是多個表及索引在表空間中混合存儲,當數據量非常大的時候,表做了大量刪除操作後表空間中將會有大量的空隙,特別是對於統計分析,對於經常刪除操作的這類應用最不適合用共享表空間。 2) 共享表空間分配後不能回縮:當出現臨時建索引或是創建一個臨時表的操作表空間擴大後,就是刪除相關的表也沒辦法回縮那部分空間了(可以理解為oracle的表空間10G,但是才使用10M,但是操作系統顯示mysql的表空間為10G),進行資料庫的冷備很慢;

3.4.4.1 獨立表空間

每一個表都將會生成以獨立的文件方式來進行存儲,每一個表都有一個.frm表描述文件,還有一個.ibd文件。 其中這個文件包括了單獨一個表的數據內容以及索引內容,默認情況下它的存儲位置也是在表的位置之中。

  • 獨立表空間的優點

1) 每個表都有自已獨立的表空間。2) 每個表的數據和索引都會存在自已的表空間中。3) 可以實現單表在不同的資料庫中移動。4) 空間可以回收(除drop table操作處,表空間不能自已回收)i: Drop table操作自動回收表空間,如果對於統計分析或是日值表,刪除大量數據後可以通過:alter table TableName engine=innodb;回縮不用的空間。ii: 對於使innodb-plugin的Innodb使用turncate table也會使空間收縮iii: 對於使用獨立表空間的表,不管怎麼刪除,表空間的碎片不會太嚴重的影響性能,而且還有機會處理。

  • 獨立表空間的缺點

1) 單表增加過大,當單表佔用空間過大時,存儲空間不足,只能從操作系統層面思考解決方法;2) 單表增加過大,如超過100個G,相比較之下,使用獨佔表空間的效率以及性能會更高一點。

3.4.4.3 結論

共享表空間在Insert操作上少有優勢。其它都沒獨立表空間表現好。當啟用獨立表空間時,請合理調整一 下:innodb_open_files 。InnoDB Hot Backup(冷備)的表空間cp不會面對很多無用的copy了。而且利用innodb hot backup及表空間的管理命令可以實現單現移動。

參考文獻

MySQL ACID及四種隔離級別的解釋

事務ACID特性及4種隔離級別詳解

資料庫事務的四大特性以及事務的隔離級別詳解

InnoDB事務模型 (InnoDB Transaction Model)

MySQL的自動提交模式Mysql元數據分析

MySQL information_schema 詳解

MySQL 5.7 INFORMATION_SCHEMA 詳解

information_schema系列二(列,列許可權,事件,存儲引擎)

Innodb三大特性之double write

MySQL InnoDB特性:兩次寫(DoubleWrite)

InnoDB recovery過程解析

MySQL InnoDB Update和Crash Recovery流程

InnoDB Crash Recovery 流程源碼實現分析

MySQL InnoDB事務隔離級別

Innodb中的事務隔離級別和鎖的關係

MySQL Server參數優化 - innodb_file_per_table


推薦閱讀:
相關文章