《高性能 MySQL》筆記——第1章 MySQL架構與歷史(一)

1.1 MySQL 邏輯架構

下圖展示了 MySQL 的邏輯架構圖。

在這裏插入圖片描述

  1. 最上層的服務並不是 MySQL 所獨有的,大多數基於網絡的客戶端/服務器的工具或者服務都有類似的架構。比如連接處理、授權認證、安全等等。
  2. 第二層構架是 MySQL 比較有意思的部分。大多數 MySQL 的核心服務功能都在這一層,包括查詢解析、分析、優化、緩存以及所有的內置函數(例如:日期、時間、數學和加密函數),所有跨存儲引擎的功能都在這一層實現:存儲過程、觸發器、視圖等。
  3. 第三層包含了存儲引擎。存儲引擎負責 MySQL 中數據的存儲和提取。和 GNU/Linux 下的各種文件系統一樣,每個存儲引擎都有它的優勢和劣勢。服務器通過 API 與存儲引擎進行通信。這些接口屏蔽了不同存儲引擎之間的差異,使得這些差異對上層的查詢過程透明。存儲引擎 API 包含了幾十個底層函數,用於執行諸如 “開始一個事務” 或者 “根據主鍵提取一行記錄” 等操作。但存儲引擎不會去解析 SQL (InnoDB 是一個例外,它會解析外鍵定義,因爲 MySQL 服務器本身沒有實現該功能),不同存儲引擎之間也不會互相通信,而只是簡單地響應上層服務器的請求。

1.1.1 連接管理與安全性

每個客戶端連接都會在服務器進程中擁有一個線程,這個連接的查詢只會在這個單獨的線程中執行,該線程只能輪流在某個 CPU 核心或者 CPU 中運行。服務器會負責緩存線程,因此不需要爲每一個新建的連接創建或者銷燬線程。

當客戶端(應用)連接到 MySQL 服務器時,服務器需要對其進行認證。認證基於用戶名,原始主機信息和密碼。如果使用了安全套接字(SSL)的方式連接,還可以使用 X.509 證書認證。一旦客戶端連接成功,服務器會繼續驗證客戶端是否具有某個特定查詢的權限(例如,是否允許客戶端對 world 數據庫的 Country 表執行 SELECT 語句)。

1.1.2 優化與執行

MySQL 會解析查詢,並創建內部數據結構(解析樹),然後對其進行各種優化,包括重寫查詢,決定表的讀取順序,以及選擇合適的索引等。用戶可以通過特殊的關鍵字提示(hint)優化器,影響它的決策過程。也可以請求優化器解釋(explain)優化過程的各個因素,使用戶可以知道服務器是如何進行優化決策的,並提供一個參考基準,便於用戶重構查詢和 schema,修改相關配置,是應用盡可能高效運行。

優化器並不關心使用的是什麼存儲引擎,但存儲引擎對於優化查詢是有影響的。優化器會請求存儲引擎提供容量或某個具體操作的開銷信息,以及表數據的統計信息等。例如,某些存儲引擎的某種索引,可能對一些特定的查詢有優化。

對於 SELECT 語句,在解析查詢之前,服務器會先檢查查詢緩存(Query Cache),如果能夠在其中找到對應的查詢,服務器就不必再執行查詢解析、優化和執行的整個過程,而是直接返回查詢緩存中的結果集。

1.2 併發控制

無論何時,只有有多個查詢需要在同一時刻修改數據,都會產生併發控制的問題。這裏討論 MySQL 在兩個層面的併發控制:服務器層與存儲引擎層。併發控制是一個內容龐大的話題,有大量的理論文獻對其進行詳細的論述。這裏只是簡要地討論 MySQL 如何控制併發讀寫,因此讀者需要有相關的知識來理解本章的內容。

以 Unix 系統的 email box 爲例子,典型的 mbox 文件格式是非常簡單的。一個 mbox 郵箱中的所有郵件都串行在一起,彼此首尾相連。這種格式對於讀取和分析郵件信息非常友好,同時投遞郵件也很容易,只要在文件末尾附加新的郵件內容即可。

但是如果兩個進程在同一時刻對同一個郵箱投遞郵件,會發生什麼情況?顯然,郵箱的數據會被破壞,兩封郵件的內容會交叉地附加在郵箱文件的末尾。設計良好的郵箱投遞系統會通過鎖(lock)來防止數據損壞如果客戶試圖投遞郵件,而郵箱已經被其他客戶鎖住,那麼就必須等待,直到鎖釋放才能進行投遞

這種鎖的方案在實際應用環境中雖然工作良好,但是並不支持併發處理。因爲在任意一個時刻,只有一個進程可以修改郵箱的數據,這在大容量的郵箱系統中是個問題。

1.2.1 讀寫鎖

從郵箱中讀取數據沒有這樣的麻煩,即使同一時刻多個用戶併發讀取也不會有什麼問題。因爲讀取不會修改數據,所以不會出錯。但是如果某個客戶正在讀取郵箱,同時另一個用戶試圖刪除編號爲 25 的郵件,會產生什麼結果?結論是不確定的,讀的客戶可能會報錯退出,也可能讀取不到一致的郵箱數據。所以,爲了安全起見,即使是讀取郵箱也需要特別注意。

如果把上述的郵箱當成數據庫中的一張表,把郵件當成表中的一行記錄,就很容易看出,同樣的問題依然存在。從很多方面來說,郵箱就是一張簡單的數據庫表。修改數據庫表中的記錄,和刪除或者修改郵箱中的郵件信息,十分類似。

解決這類經典的問題的方法就是併發控制(讀鎖和寫鎖),其實非常簡單。在處理併發讀或者寫的時候,可以通過實現一個由兩種類型的鎖組成鎖系統來解決問題。這兩種類型的鎖通常被稱爲共享鎖(shared lock)和排他鎖(exclusive lock),也叫讀鎖(read lock)和寫鎖(write lock)

這裏先不討論如何具體實現,描述一下鎖的概念如下:讀鎖是共享的,或者說是相互不阻塞的。多個客戶在同一時刻可以同時讀取同一資源,而互不干擾。寫鎖則是排他的,也就是說一個寫鎖會阻塞其他的寫鎖和讀鎖,這是出於安全策略的考慮,只有這樣,才能確保給定的時間裏,只有一個用戶能執行寫入,並防止其他用戶讀取正在寫入的同一資源

在實際的數據庫系統中,每時每刻都在發生鎖定,當某個用戶在修改某一部分數據的時候,MySQL 會通過鎖定防止其他用戶讀取同一數據。大多數時候,MySQL 鎖的內部管理都是透明的。

1.2.2 鎖粒度

一種提供共享資源併發性的方式就是讓鎖定對象更有選擇性。儘量只鎖定需要修改的部分數據,而不是所有的資源。更理想的方式是,只對會修改的數據片(具體到鎖定所修改的字段)進行精確的鎖定。任何時候,在給定的資源上,鎖定的數據量越少,則系統的併發程度越高,只要互相之間不發生衝突即可。

問題是加鎖也需要消耗資源。鎖的各種操作,包括獲得鎖,檢查鎖是否已經被解除,釋放鎖等,都會增加系統的開銷。如果系統花費大量的時間來管理鎖,而不是存取數據,那麼系統的性能可能因此受到影響。

所謂的鎖策略,就是在鎖的開銷和數據的安全性之間尋求平衡,這種平衡當然有會影響到性能。大多數商業數據庫系統沒有提供更多的選擇,一般都是在表上施加行級鎖(row-level lock),並以各種複雜的方式來實現,以便在鎖比較多的情況下儘可能地提供更好的性能。

而 MySQL 則提供多種選擇,每種 MySQL 存儲引擎都可以實現自己的鎖策略和鎖粒度。在存儲引擎的設計中,鎖管理是個非常重要的決定。將鎖粒度固定在某個級別,可以爲某些特定的應用場景提供更好的性能。但是同時卻會失去對另外一些應用場景的良好支持。好在 MySQL 支持多個存儲引擎的架構,所以不需要單一的通用解決方案。下面介紹兩種最重要的鎖策略

1.2.2.1 表鎖(table lock)

表鎖是 MySQL 中最基本的鎖策略,並且是開銷最小的策略。表鎖非常類似於前文描述的郵箱加鎖機制:它會鎖定整張表。一個用戶在對錶進行寫操作(插入,刪除,更新等等)前,需要先獲得寫鎖,這個會阻塞其他用戶對該表的所有讀寫操作。只有沒有寫鎖的時候,其他讀取的用戶才能獲得讀鎖,讀鎖之間是不互相阻塞的。

在特定的場景中,表鎖也可能有良好的性能。例如,read local 表鎖支持某些類型的併發寫操作。另外,寫鎖也比讀鎖有更高的優先級,因此一個寫鎖請求可能會被插入到讀鎖隊列的前面(寫鎖可以插入到鎖隊列中讀鎖的前面,反之讀鎖則不能插入寫鎖的前面)。

儘管存儲引擎可以管理自己的鎖,MySQL 本身還是會使用各種有效的表鎖來實現不同的目的。例如服務器會諸如 ALTER TABLE 之類的語句使用表鎖,而忽略存儲引擎的鎖機制。

1.2.2.2 行級鎖(row lock)

行級鎖可以最大程度地支持併發處理(同時也帶來了最大的鎖開銷)。衆多周知,在 InnoDB 和 XtraDB,以及其他一些存儲引擎中實現了行級鎖。行級鎖只在存儲引擎層實現,而 MySQL 服務器層沒有實現。服務器層完全不瞭解存儲引擎中的鎖實現。

1.3 事務

事務是一組原子性的 SQL 查詢,或者說是一個獨立的工作單元。如果數據庫引擎能夠成功地對數據庫應用該組查詢的全部語句,那麼就執行該組查詢。如果其中任何一條語句因爲崩潰或其他原因無法執行,那麼所有語句都不會執行。也就是說,事務內的語句,要麼全部執行成功,要麼全部執行失敗

銀行應用是解釋事務必要性的一個經典例子。假設一個銀行的數據庫有兩張表:支票(checking)表和儲存(savings)表。現在要從用戶 Jane 的支票賬戶轉移 200 美元到她的儲存賬戶,那麼至少需要三個步驟:

  1. 檢查支票賬戶的餘額高於 200 美元。
  2. 從支票賬戶餘額中減去 200 美元。
  3. 在儲存賬戶餘額中增加 200 美元。

上述三個步驟的操作必須打包在一個事務中,任何一個步驟失敗,則必須回滾所有的步驟。

可以用 START TRANSACTION 語句開始一個事務,然後要麼使用 COMMIT 提交將修改的數據持久保存,要麼使用 ROLLBACK 撤銷所有的修改。事務 SQL 的樣本如下:

START TRANSACTION;
SELECT balance From checking WHERE custom_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE custom_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE custom_id = 10233276;
COMMIT;

單純的事務概念並不是故事的全部。試想一下,如果執行到第四條語句時服務器崩潰了,會發生什麼?天知道,用戶可能會損失 200 美元。再假如,在執行到第三條語句和第四條語句之間時,另外一個進程要刪除支票賬戶的所有餘額,那麼結果可能就是銀行在不知道這個邏輯的情況下白白給了 Jane 200 美元。

除非系統通過嚴格的 ACID 測試,否則空談事務的概念是不夠的。

1.3.1 事務的四大特性(ACID)

ACID 表示原子性(atomicity)、一致性(consistency)、隔離性(isolation)和持久性(durability)。一個運行良好的事務處理系統,必須具備這些標準特性。

  • 原子性:一個事物必須被視爲一個不可分割的最小工作單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾,對於一個事物來說,不可能只執行其中的一個部分操作,這就是事務的原子性。
  • 一致性:數據庫總是從一個一致性狀態轉化到另一個一致性狀態。在前面的例子中,一致性確保了,即使在執行第三、第四語句之間時系統崩潰,支票賬戶中也不會損失 200 美元,因爲事務最終沒有提交,所以事務中所做的修改也不會保存到數據庫中。
  • 隔離性:通常來說,一個事物所做的修改在最終提交以前,對其他事務是不可見的。在前面的例子中,當執行完第三條語句、第四條語句還未開始時,此時有另外一個賬戶彙總程序開始執行,則其看到的支票賬戶的餘額並沒有被減去 200 美元。後面我們討論隔離級別(Isolation level)的時候,會發現爲什麼我們要說 “通常來說” 是不可見的。
  • 持久性:一旦事務提交,則其所作的修改就會永久保存到數據庫中。此時即使系統崩潰,修改的數據也不會丟失。持久性是個有點模糊的概念,因爲實際上持久性也分很多不同的級別。有些持久性策略能夠提供非常強的安全保障,而有些則未必。而且不可能有能做到 100% 的持久性保證的策略。

1.3.2 隔離級別

在 SQL 標準中定義了四種隔離級別,每一種級別都規定了一個事務中所做的修改,哪些是在事務內和事務間可見的,哪些是不可見的。較低級別的隔離通常可以執行更高的併發,系統的開銷也更低。

下面簡單地介紹一下四種隔離級別。

1.3.2.1 READ UNCOMMITTED(讀未提交)

在 READ UNCOMMITED 級別,事務中的修改,即使沒有提交,對其他事務也都是可見的。事務可以讀取未提交的數據,這也成爲髒讀(Dirty Read)。這個級別會導致很多問題,從性能上說 READ UNCOMMITED 不會比其他的級別好太多,但卻缺乏其他級別的很多好處,除非有非常必要的理由,在實際的應用中一般很少使用。

實例演示

  1. 創建數據庫,執行下面語句

    DROP TABLE IF EXISTS `account`;
    CREATE TABLE `account` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(20) DEFAULT NULL,
      `balance` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
    
    INSERT INTO `account` VALUES ('1', 'ZhangSan', '1000');
    INSERT INTO `account` VALUES ('2', 'LiSi', '1500');
    INSERT INTO `account` VALUES ('3', 'WangWu', '2000');
    
  2. 打開客戶端 A,設置當前事務模式爲 READ UNCOMMITED(讀未提交)級別,開啓事務,但不進行提交

    -- 客戶端 A
    mysql> set session transaction isolation level read uncommitted;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  3. 打開客戶端 B,設置當前事務模式爲 READ UNCOMMITED(讀未提交)級別,開啓事務,更新表 account,將 id 爲 1 的用戶餘額減去 500,但不進行提交

    -- 客戶端 B
    mysql> set session transaction isolation level read uncommitted;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
    mysql> update account set balance = balance - 500 where id = 1;
    Query OK, 1 row affected
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  4. 回到客戶端 A,查詢 account 表的數據,可以看到,儘管 B 的事務未提交,但客戶端 A 已經能看到客戶端 B 修改的數據了

    -- 客戶端 A
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  5. 此時,如果客戶端 B 回滾,讓 account 恢復到修改前的狀態

    -- 客戶端 B
    mysql> rollback;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  6. 客戶端 A 發現 id 爲 1 的用戶餘額又變爲了 1000

    -- 客戶端 A
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    

總結:上面的例子就是客戶端 A 讀到了客戶端 B 未提交的數據,所以客戶端 B 回滾後,客戶 A 再次讀取到的數據和之前的不一致。

1.3.2.2 READ COMMITED(讀已提交)

大多數數據庫系統的默認隔離級別都是 READ COMMITED(但 MySQL 不是)。READ COMMITED 滿足前面提到的隔離性的簡單定義:一個事務開始時,只能看見已經提交的事務所做的修改。換句話說,一個事務從開始到提交之前,所做的任何修改對其他事務都是不可見的。這個級別有時候也叫做不可重複讀(nonerepeatable read),因爲兩次執行同樣的查詢,可能會得到不一樣的結果

實例演示

  1. 打開客戶端 A,設置當前事務模式爲 READ COMMITED(讀已提交)級別,開啓事務,但不進行提交

    -- 客戶端 A
    mysql> set session transaction isolation level read committed;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  2. 打開客戶端 B,設置當前事務模式爲 READ COMMITED(讀已提交)級別,開啓事務,更新表 account,將 id 爲 1 的用戶餘額減去 500,但不進行提交

    -- 客戶端 B
    mysql> set session transaction isolation level read committed;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
    mysql> update account set balance = balance - 500 where id = 1;
    Query OK, 1 row affected
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  3. 回到客戶端 A,查詢 account 表的數據,可以看到,客戶端 A 所看到的數據和之前一樣,沒有讀到未提交的數據。

    -- 客戶端 A
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  4. 此時,如果客戶端 B 提交了事務

    -- 客戶端 B
    mysql> commit;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  5. 那麼客戶端 A 重新讀取 account 表的數據,會發現兩次讀取的數據不一致

    -- 客戶端 A
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    

總結:讀已提交的一個弊病,就是在一個事物中,兩次讀取可能會讀到不一樣的結果。

1.3.2.3 REPEATABLE READ(可重複讀)

REPEATABLE READ 解決了髒讀問題。該級別保證了在同一個事務中多次讀取同樣的記錄的結果是一致的。但是,理論上,可重複讀隔離級別還是無法解決另一個幻讀 (PhantomRead)的問題。所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另外一個事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行(Phantom Row)。InnoDB 和 XtraDB 存儲引擎通過多版併發控制(MVCC,Multivesion Concurrency Control )解決了幻讀問題。

實例演示

  1. 打開客戶端 A,設置當前事務模式爲 REPEATABLE READ(可重複讀)級別,開啓事務,但不進行提交

    -- 客戶端 A
    mysql> set session transaction isolation level repeatable read;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  2. 打開客戶端 B,設置當前事務模式爲 REPEATABLE READ(可重複讀)級別,開啓事務,更新表 account,將 id 爲 1 的用戶餘額減去 500,提交事務

    -- 客戶端 B
    mysql> set session transaction isolation level repeatable read;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
    mysql> update account set balance = balance - 500 where id = 1;
    Query OK, 1 row affected
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> commit;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  3. 回到客戶端 A 再次讀取 account 表的數據,和之前的數據做對比,可以發現此時就算客戶端 B 修改了 account 表的數據並提交事務,但客戶端 A 在一次事物中讀取的數據不變

    -- 客戶端 A
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |    1000 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  4. 切換到客戶端 B,插入一條新數據

    -- 客戶端 B
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> insert into account(name, balance) value("Jack", 1200);
    Query OK, 1 row affected
    
    mysql> commit;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    |  5 | Jack     |    1200 |
    +----+----------+---------+
    4 rows in set
    
  5. 再回到客戶端 A,查詢 account 表的數據,可以看到數據還是和之前展示的一樣

    -- 客戶端 A
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    

1.3.2.4 SERIALIZABLE(可串行化)

SERIALIZABLE 是最高的隔離級別。它通過強制事務串行執行,避免了前面說的幻讀問題。簡單的來說,SERIALIZABLE 會在讀的每一行數據上都加上鎖,所以可能導致大量的超時和鎖徵用問題。實際應用中也很少用到這個隔離級別,只有在非常需要確保數據的一致性而且可以接受沒有併發的情況,纔可考慮用該級別。

實例演示

  1. 打開客戶端 A,設置當前事務模式爲 SERIALIZABLE(可傳性化)級別,開啓事務,但不進行提交

    -- 客戶端 A
    mysql> set session transaction isolation level serializable;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> select * from account;
    +----+----------+---------+
    | id | name     | balance |
    +----+----------+---------+
    |  1 | ZhangSan |     500 |
    |  2 | LiSi     |    1500 |
    |  3 | WangWu   |    2000 |
    +----+----------+---------+
    3 rows in set
    
  2. 打開客戶端 B,設置當前事務模式爲 SERIALIZABLE(可傳性化)級別,開啓事務,並插入一條記錄

    -- 客戶端 B
    mysql> set session transaction isolation level serializable;
    Query OK, 0 rows affected
    
    mysql> start transaction;
    Query OK, 0 rows affected
    
    mysql> insert into account(name, balance) value("Rose", 3000);
    1205 - Lock wait timeout exceeded; try restarting transaction
    terminated by user
    

總結:由於客戶端 A 的事務先操作了 account 表,因此事務 A 會對 account 表產生表鎖,事物 B 無法操作 account 表,過了事務超時時間,就會拋出異常

1.3.2.5 總結

存在問題

  1. 髒讀:一個事務,讀取到另一個事務中沒有提交的數據。
  2. 不可重複讀:在同一個事務中,兩次讀取到的數據不一樣。
  3. 幻讀:一個事務查詢所有數據,另一個事務添加了一條數據,則第一個事務查詢不到第二個事務的修改。

隔離級別

隔離級別 髒讀可能性 不可重複讀可能性 幻讀可能性 加鎖讀
READ UNCOMMITED YES YES YES NO
READ COMMITED NO YES YES NO
REPEATABLE READ NO NO YES NO
SERIALIZABLE NO NO NO YES

注意:隔離級別從小到大安全性越來越高,但是效率越來越低。

1.3.3 死鎖

死鎖是指兩個或多個事務在同一資源上相互佔用,並請求鎖定對方佔用的資源,從而導致惡性循環的現象。當多個事務試圖以不同的順序鎖定資源時,也會產生死鎖。多個事物同時鎖定同一資源,也會產生死鎖。

例如,設想下面兩個事務同時處理 StockPrice 表:

-- 事務1
  START TRANSACTION;
  UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2002-05-01';
  UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02';
  COMMIT;
 
-- 事務2
  START TRANSACTION;
  UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2002-05-02';
  UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2002-05-01';
  COMMIT;

如果湊巧,兩個事務都執行了第一條 UPDATE 語句,更新了一行數據,同時也鎖定了該行數據,接着每個事務都嘗試去執行第二條 UPDATE 語句,卻發現該行已經被對方鎖定,然後兩個事務都等待對方釋放鎖,同時又持有對方需要的鎖,則陷入死循環。除非有外部因素介入纔可能解除死鎖。

爲了解決這個問題,數據庫系統實現了各種死鎖檢測和死鎖超時機制。越複雜的系統,比如 InnoDB 存儲引擎,越能檢測到死鎖的循環依賴,並立即返回一個錯誤。這種解決方式很有效,否則死鎖會導致出現非常慢的查詢。還有一種解決方式,當查詢的時間達到鎖等待超時的設定後放棄鎖請求,這種方式通常來說不太好。InnoDB 目前處理死鎖的方法是:將持有最少行級排他鎖的事務進行回滾(這是相對比較簡單的死鎖回滾算法)。

鎖的行爲和順序是和存儲引擎相關的,以同樣的順序去執行語句時,有些存儲引擎會產生死鎖,有些則不會。因此死鎖的產生有雙重原因:有些是因爲真正的數據衝突,這種情況通常很難避免,但有些則是完全由於存儲引擎的實現方式所導致的。

死鎖發生以後,只有部分或者完全回滾其中一個事務,才能打破死鎖。對於事務型的系統,這是無法避免的,所以應用程序在設計時必須如何考慮處理死鎖。大多數情況下只需要重新執行因死鎖回滾的事務即可。

1.3.4 事務日誌

事務日誌可以幫助提高事務的效率。使用事務日誌,存儲引擎在修改表的數據時只需要修改其內存拷貝,再把該修改行爲記錄到保存在硬盤上的事務日誌中,而不是每次都將修改的數據本身寫入磁盤。事務日誌採用的是追加的方式,因此寫日誌的操作是磁盤上的一小塊區域內的順序 I/O,而不像隨機 I/O 需要在磁盤的多個地方移動磁頭,所以採用事務日誌的方式相對來說要快的多。事務日誌保存到磁盤上之後,內存中被修改的數據在後臺可以慢慢地刷回磁盤。目前大多數存儲引擎都是這麼實現的,我們通常稱之爲預寫式日誌,修改數據需要寫兩次磁盤。

如果數據的修改已經記錄到事務日誌並持久化,但數據本身沒有保存到磁盤上,此時系統崩掉了,存儲引擎在重啓時會自動恢復這些被修改的數據。具體的恢復方式則隨着存儲引擎的不同而不同。

1.3.5 MySQL 中的事務

MySQL 提供了兩種事務型的存儲引擎:InnoDB 和 NDB Cluster。另外還有一些第三方存儲引擎也支持事務,比較知名的包括 XtraDB 和 PBXT。

1.3.5.1 自動提交(AUTOCOMMIT)

MySQL 默認採用自動提交(AUTOCOMMIT)模式。也就是說,如果不是顯式地開始一個事務,則每個查詢都會被當做一個事務執行提交操作。在當前連接中,可以通過設置 AUTOCOMMIT 變量來啓用或禁用自動提交模式。

AUTOCOMMIT 變量來啓動或禁用自動提交模式:

mysql> SHOW VARIABLES LIKE 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set

mysql> SET AUTOCOMMIT = 1;
Query OK, 0 rows affected

1 或者 ON 表示啓用,0 或者 OFF 表示禁用。當 AUTOCOMMIT=0 時,所有的查詢都是在一個是事務中,直到顯式的執行 COMMIT 提交或者 ROLLBACK 回滾,該事務結束。同時又開始了另一個新事務。修改 AUTOCOMMIT 對非事務型的表,比如 MyISAM 或者內存表,不會有任何影響。對這類表來說,沒有 COMMIT 或者 ROLLBACK 的概念,也可以說是相當於一直處於 AUTOCOMMIT 啓用的模式。

還有一些命令,在執行之前會強制執行 COMMIT 提交當前的活動事務。在數據定義語言(DDL)中,如果是會導致大量數據改變的操作,比如 ALTER TABLE。另外還有 LOCK TABLES 等其他語句也會導致同樣的效果。如果有需要,請檢查對應版本的官方文檔來確認所有可能導致自動提交的語句列表。

MySQL 可以通過執行 SET TRANSACTION ISOLATION LEVEL 命令來設置隔離級別。新的隔離級別會在下一個事務開始的時候生效。可以在配置文件中設置整個數據庫的隔離級別,也可以只改變當前會話的隔離級別:

mysql> set session transaction isolation level read committed;

MySQL 能識別所有的 4 個 ANSI 隔離級別,InnoDB 引擎也支持所有的隔離級別。

1.3.5.2 在事務中混合使用存儲引擎

MySQL服務層不管理事務,事務是由下層的存儲引擎實現的。所以在同一個事務中,使用多種存儲引擎是不可靠的

如果在事務中混合使用了事務型和非事務型的表(例如 InnoDB 和 MyISAM 表)。在正常提交的情況下不會有什麼問題。

但如果該事務需要回滾,非事務型的表上的變更就無法撤銷,這會導致數據庫處於不一致的狀態,這種情況很難修復,事務的最終結果將無法判定

在非事務型的表上執行事務相關操作的時候,MySQL 通常不會發出提醒,也不會報錯。有時候只有回滾的時候纔會發出一個警告:“某些非事務型的表上的變更不能被回滾”。但大多數情況下,對非事務型表的操作都不會有提示。

1.3.5.3 隱式和顯式鎖定

InnoDB 採用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行的過程中,隨時都可以執行鎖定,鎖只有在執行 COMMIT 或 ROLLBACK 的時候纔會釋放,並且所有的鎖是在同一時刻被釋放。這些鎖都是隱式鎖定,InnoDB 會根據隔離級別在需要的時候自動加鎖。

另外,InnoDB 也支持通過特定的語句進行顯式鎖定,這些語句不屬於 SQL 規範:

SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE

MySQL 也只支持 LOCK TABLES 和 UNLOCK TABLES 語句,這是在服務層實現的,和存儲引擎無關。它們有自己的用途,並不能代替事務處理。如果應用需要用到事務,還是應該選擇事務型存儲引擎。

經常可以發現,應用語句將表從 MyISAM 轉換到 InnoDB,但還是顯式地使用 LOCK TABLES 語句,這非但沒有必要,會嚴重影響性能,實際上 InnoDB 的行級鎖工作得更好。

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