MySQL 性能優化篇 (七) 數據庫鎖定機制

前言

爲了保證數據的一致完整性,任何一個數據庫都存在鎖定機制。鎖定機制的優劣直接應想到一個數據庫系統的併發處理能力 和性能,所以鎖定機制的實現也就成爲了各種數據庫的核心技術之一。本章將對MySQL 中兩種使用最爲頻繁的存儲引擎MyISAM 和Innodb 各自的鎖定機制進行較爲詳細的分析。

7.1 MySQL 鎖定機制簡介

數據庫鎖定機制簡單來說就是數據庫爲了保證數據的 一致性而使各種共享資源在被併發訪問訪問變得有序所設計的一種規則。對於任何一種數據庫來說都需要有相應的鎖定機制,所以MySQL 自然也不能例外。MySQL 數據庫由於其自身架構的特點,存在多種數據存儲引擎,每種存儲引擎所針對的應用場景特點都不太一樣,爲了滿足各自特定應用場景的需求,每種存儲引擎的鎖定 機制都是爲各自所面對的特定場景而優化設計,所以各存儲引擎的鎖定機制也有較大區別。

總的來說,MySQL 各存儲引擎使用了三種類型(級別)的鎖定機制:行級鎖定,頁級鎖定和表級鎖定。下面我們先分析一下MySQL 這三種鎖定的特點和各自的優劣所在。

行級鎖定(row-level)

行級鎖定最大的特點就是鎖定對象的顆粒度很小,也是目前各大數據庫管理軟件所實現的鎖定顆粒度最小的。由於鎖定顆粒度很小,所以發生鎖定資源爭用的概率也最小,能夠給予應用程序儘可能大的併發處理能力而提高一些需要高併發應用系統的整體性能。

雖然能夠在併發處理能力上面有較大的優勢,但是行級鎖定也因此帶來了不少弊端。由於鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發生死鎖。

表級鎖定(table-level)

和行級鎖定相反,表級別的鎖定是MySQL 各存儲引擎中最大顆粒度的鎖定機制。該鎖定機制最大的特點是實現邏輯非常簡單,帶來的系統負面影響最小。所以獲取鎖和釋放鎖的速度很快。由於表級鎖一次會將整個表鎖定,所以可以很好的避免困擾我們的死鎖問題。

當然,鎖定顆粒度大所帶來最大的負面影響就是出現鎖定資源爭用的概率也會最高,致使並大度大打折扣。

頁級鎖定(page-level)

頁 級鎖定是MySQL 中比較獨特的一種鎖定級別,在其他數據庫管理軟件中也並不是太常見。頁級鎖定的特點是鎖定顆粒度介於行級鎖定與表級鎖之間,所以獲取鎖定所需要的資源開 銷,以及所能提供的併發處理能力也同樣是介於上面二者之間。另外,頁級鎖定和行級鎖定一樣,會發生死鎖。

在數據庫實現資源鎖定的過程中,隨着鎖定資源顆粒度的減小,鎖定相同數據量的數據所需要消耗的內存數量是越來越多的,實現算法也會越來越複雜。不過,隨着鎖定資源顆粒度的減小,應用程序的訪問請求遇到鎖等待的可能性也會隨之降低,系統整體併發度也隨之提升。

在MySQL 數據庫中,使用表級鎖定的主要是MyISAM,Memory,CSV 等一些非事務性存儲引擎,而使用行級鎖定的主要是Innodb存儲引擎和NDB Cluster 存儲引擎,頁級鎖定主要是BerkeleyDB 存儲引擎的鎖定方式。

MySQL 的如此的鎖定機制主要是由於其最初的歷史所決定的。在最初,MySQL 希望設計一種完全獨立於各種存儲引擎的鎖定機制,而且在早期的MySQL 數據庫中,MySQL 的存儲引擎(MyISAM 和Momery)的設計是建立在“任何表在同一時刻都只允許單個線程對其訪問(包括讀)”這樣的假設之上。但是,隨着MySQL 的不斷完善,系統的不斷改進,在MySQL3.23 版本開發的時候,MySQL 開發人員不得不修正之前的假設。因爲他們發現一個線程正在讀某個表的時候,另一個線程是可以對該表進行insert 操作的,只不過只能INSERT 到數據文件的最尾部。這也就是從MySQL 從3.23 版本開始提供的我們所說的Concurrent Insert。

當出現 Concurrent Insert 之後,MySQL 的開發人員不得不修改之前系統中的鎖定實現功能,但是僅僅只是增加了對Concurrent Insert 的支持,並沒有改動整體架構。可是在不久之後,隨着BerkeleyDB存儲引擎的引入,之前的鎖定機制遇到了更大的挑戰。因爲BerkeleyDB存儲 引擎並沒有MyISAM 和Memory存儲引擎同一時刻只允許單一線程訪問某一個表的限制,而是將這個單線程訪問限制的顆粒度縮小到了單個page,這又一次迫使MySQL 開發人員不得不再一次修改鎖定機制的實現。

由於新的存儲引擎的引入,導致鎖定機制不能滿足要求,讓MySQL 的人意識到已經不可能實現一種完全獨立的滿足各種存儲引擎要求的鎖定實現機制。如果因爲鎖定機制的拙劣實現而導致存儲引擎的整體性能的下降,肯定會嚴重打 擊存儲引擎提供者的積極性,這是MySQL 公司非常不願意看到的,因爲這完全不符合MySQL 的戰略發展思路。所以工程師們不得不放棄了最初的設計初衷,在鎖定實現機制中作出修改,允許存儲引擎自己改變MySQL 通過接口傳入的鎖定類型而自行決定該怎樣鎖定數據。

7.2 各種鎖定機制分析

在整體瞭解了MySQL 鎖定機制之後,這一節我們將詳細分析MySQL 自身提供的表鎖定機制和其他儲引擎實自身實現的行鎖定機制,並通過MyISAM 存儲引擎和Innodb 存儲引擎實例演示。表級鎖定

MySQL 的表級鎖定主要分爲兩種類型,一種是讀鎖定,另一種是寫鎖定。在MySQL 中,主要通過四個隊列來維護這兩種鎖定:兩個存放當前正在鎖定中的讀和寫鎖定信息,另外兩個存放等待中的讀寫鎖定信息,如下:

  • Current read-lock queue (lock->read)
  • Pending read-lock queue (lock->read_wait)
  • Current write-lock queue (lock->write)
  • Pending write-lock queue (lock->write_wait)

當 前持有讀鎖的所有線程的相關信息都能夠在Current read-lock queue 中找到,隊列中的信息按照獲取到鎖的時間依序存放。而正在等待鎖定資源的信息則存放在Pending read-lock queue 裏面,另外兩個存放寫鎖信息的隊列也按照上面相同規則來存放信息。

雖然對於我們這些使用者來說MySQL 展現出來的鎖定(表鎖定)只有讀鎖定和寫鎖定這兩種類型,但是在MySQL 內部實現中卻有多達11 種鎖定類型,由系統中一個枚舉量(thr_lock_type)定義,各值描述如下:

鎖定類型 說明
IGNORE 當發生鎖請求的時候內部交互使用,在鎖定結構和隊列中並不會有任何信息存儲
UNLOCK 釋放鎖定請求的交互用所類型
READ 普通讀鎖定
WRITE 普通寫鎖定
READ_WITH_SHARED_LOCKS 在 Innodb 中使用到,由如下方式產生
如:SELECT ... LOCK IN SHARE MODE
READ_HIGH_PRIORITY 高優先級讀鎖定
READ_NO_INSERT 不允許 Concurent Insert 的鎖定
WRITE_ALLOW_WRITE 這個類型實際上就是當由存儲引擎自行處理鎖定的時候,mysqld 允許其他的線程再獲取讀或者寫鎖定,因爲即使資源衝突,存儲引擎自己也會知道怎麼來處理
WRITE_ALLOW_READ 這種鎖定發生在對錶做 DDL(ALTER TABLE ...)的時候,MySQL 可以允許其他線程獲取讀鎖定,因爲 MySQL 是通過重建整個表然後再 RENAME 而實現的該功能,所在整個過程原表仍然可以提供讀服務
WRITE_CONCURRENT_INSERT 正在進行 Concurent Insert 時候所使用的鎖定方式,該鎖定進行的時候,除了 READ_NO_INSERT 之外的其他任何讀鎖定請求都不會被阻塞
WRITE_DELAYED 在使用 INSERT DELAYED 時候的鎖定類型
WRITE_LOW_PRIORITY 顯 示 聲 明 的 低 級 別 鎖 定 方 式 , 通 過 設 置LOW_PRIORITY_UPDAT = 1 而產生
WRITE_ONLY 當在操作過程中某個鎖定異常中斷之後系統內部需要進行 CLOSE TABLE 操作,在這個過程中出現的鎖定類型就是 WRITE_ONLY

讀鎖定

一個新的客戶端請求在申請獲取讀鎖定資源的時候,需要滿足兩個條件:

  1. 請求鎖定的資源當前沒有被寫鎖定;
  2. 寫鎖定等待隊列(Pending write-lock queue)中沒有更高優先級的寫鎖定等待;

如果滿足了上面兩個條件之後,該請求會被立即通過,並將相關的信息存入Current read-lock queue 中,而如果上面兩個條件中任何一個沒有滿足,都會被迫進入等待隊列Pending read-lock queue中等待資源的釋放。

寫鎖定

當客戶端請求寫鎖定的時候,MySQL 首先檢查在Current write-lock queue 是否已經有鎖定相同資源的信息存在。

如 果Current write-lock queue 沒有,則再檢查Pending write-lock queue,如果在Pending write-lock queue 中找到了,自己也需要進入等待隊列並暫停自身線程等待鎖定資源。反之,如果Pending write-lock queue 爲空,則再檢測Current read-lock queue,如果有鎖定存在,則同樣需要進入Pending write-lock queue 等待。當然,也可能遇到以下這兩種特殊情況:

  1. 請求鎖定的類型爲WRITE_DELAYED;
  2. 請求鎖定的類型爲WRITE_CONCURRENT_INSERT 或者是TL_WRITE_ALLOW_WRITE, 同時Current read lock 是READ_NO_INSERT 的鎖定類型。

當 遇到這兩種特殊情況的時候,寫鎖定會立即獲得而進入Current write-lock queue 中如果剛開始第一次檢測就Current write-lock queue 中已經存在了鎖定相同資源的寫鎖定存在,那麼就只能進入等待隊列等待相應資源鎖定的釋放了。

讀請求和寫等待隊列中的寫鎖請求的優先級規則主要爲以下規則決定:

  1. 除了READ_HIGH_PRIORITY 的讀鎖定之外,Pending write-lock queue 中的WRITE 寫鎖定能夠阻塞所有其他的讀鎖定;
  2. READ_HIGH_PRIORITY 讀鎖定的請求能夠阻塞所有Pending write-lock queue 中的寫鎖定;
  3. 除了WRITE 寫鎖定之外,Pending write-lock queue 中的其他任何寫鎖定都比讀鎖定的優先級低。

寫鎖定出現在Current write-lock queue 之後,會阻塞除了以下情況下的所有其他鎖定的請求:

  1. 在某些存儲引擎的允許下,可以允許一個WRITE_CONCURRENT_INSERT 寫鎖定請求
  2. 寫鎖定爲WRITE_ALLOW_WRITE 的時候,允許除了WRITE_ONLY 之外的所有讀和寫鎖定請求
  3. 寫鎖定爲WRITE_ALLOW_READ 的時候,允許除了READ_NO_INSERT 之外的所有讀鎖定請求
  4. 寫鎖定爲WRITE_DELAYED 的時候,允許除了READ_NO_INSERT 之外的所有讀鎖定請求
  5. 寫鎖定爲WRITE_CONCURRENT_INSERT 的時候,允許除了READ_NO_INSERT 之外的所有讀鎖定請求

隨着MySQL 存儲引擎的不斷髮展,目前MySQL 自身提供的鎖定機制已經沒有辦法滿足需求了,很多存儲引擎都在MySQL 所提供的鎖定機制之上做了存儲引擎自己的擴展和改造。

MyISAM 存儲引擎基本上可以說是對MySQL 所提供的鎖定機制所實現的表級鎖定依賴最大的一種存儲引擎了,雖然MyISAM 存儲引擎自己並沒有在自身增加其他的鎖定機制,但是爲了更好的支持相關特性,MySQL 在原有鎖定機制的基礎上爲了支持其Concurrent Insert 的特性而進行了相應的實現改造。

而其他幾種支持事務的存儲存儲引擎,如Innodb,NDB Cluster 以及Berkeley DB 存儲引擎則是讓MySQL 將鎖定的處理直接交給存儲引擎自己來處理,在MySQL 中僅持有WRITE_ALLOW_WRITE 類型的鎖定。由於MyISAM 存儲引擎使用的鎖定機制完全是由MySQL 提供的表級鎖定實現,所以下面我們將以MyISAM 存儲引擎作爲示例存儲引擎,來實例演示表級鎖定的一些基本特性。由於,爲了讓示例更加直觀,我將使用顯示給表加鎖來演示:

時刻 Session a Session b
  READ  
1 sky@localhost : example 11:21:08>
lock table test_table_lock read;
Query OK, 0 rows affected (0.00 sec)
顯示給test_table_lock 加讀鎖定
 
2 sky@localhost : example 11:21:10>
select * from test_table_lock limit
1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.01 sec)
自己的讀操作未被阻塞
sky@localhost : example 11:21:13>
select * from test_table_lock limit 1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.01 sec)
其他線程的讀也未被阻塞
3 sky@localhost : example 11:21:15>
update test_table_lock set b = a
limit 1;
ERROR 1099 (HY000): Table
'test_table_lock' was locked with a
READ lock and can't be updated
sky@localhost : example 11:21:20>
update test_table_lock set b = a
limit 1;
寫一下試試看?被阻塞了
4 sky@localhost : example 11:21:09>
unlock tables;
Query OK, 0 rows affected (0.00 sec)
解除讀鎖
 
5 sky@localhost : example 11:48:19>
lock table test_table_lock read
local;
Query OK, 0 rows affected (0.00 sec)
獲取讀鎖定的時候增加local 選項
sky@localhost : example 11:21:20>
update test_table_lock set b = a
limit 1;
Query OK, 0 rows affected (1 min
15.52 sec)
Rows matched: 1 Changed: 0 Warnings:
0
在session a 釋放鎖定資源之後,session
b 獲得了資源,更新成功
sky@localhost : example 11:48:20>
insert into test_table_lock
values(1,'s','c');
Query OK, 1 row affected (0.00 sec)
其他session 的insert 未被阻塞
  WRITE  
6 這次加寫鎖試試看:
sky@localhost : example 11:27:01>
lock table test_table_lock write;
Query OK, 0 rows affected (0.00 sec)
 
7 sky@localhost : example 11:27:10>
select * from test_table_lock limit
1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.01 sec)
自己session 可以繼續讀
sky@localhost : example 11:27:16>
select * from test_table_lock limit 1;
其他session 被阻塞
8 sky@localhost : example 11:27:02>
unlock tables;
Query OK, 0 rows affected (0.00 sec)
釋放鎖定資源
 
9   sky@localhost : example 11:27:16>
select * from test_table_lock limit 1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (36.36 sec)
其他session 獲取的資源
  WRITE_ALLOW_READ  
10 sky@localhost : example 11:42:24>
alter table test_table_lock add(c
varchar(16));
Query OK, 5242880 rows affected
(7.06 sec)
Records: 5242880 Duplicates: 0
Warnings: 0
通過執行DDL(ALTER TABLE),
獲取WRITE_ALLOW_READ 類型的寫鎖定
sky@localhost : example 11:42:25>
select * from test_table_lock limit 1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.01 sec)
其他session 的讀未被阻塞

行級鎖定

行級鎖定不是MySQL 自己實現的鎖定方式,而是由其他存儲引擎自己所實現的,如廣爲大家所知的Innodb 存儲引擎,以及MySQL 的分佈式存儲引擎NDB Cluster 等都是實現了行級鎖定。

Innodb 鎖定模式及實現機制

考慮到行級鎖定君由各個存儲引擎自行實現,而且具體實現也各有差別,而Innodb 是目前事務型存儲引擎中使用最爲廣泛的存儲引擎,所以這裏我們就主要分析一下Innodb 的鎖定特性。

總 的來說,Innodb 的鎖定機制和Oracle 數據庫有不少相似之處。Innodb 的行級鎖定同樣分爲兩種類型,共享鎖和排他鎖,而在鎖定機制的實現過程中爲了讓行級鎖定和表級鎖定共存, Innodb 也同樣使用了意向鎖(表級鎖定)的概念,也就有了意向共享鎖和意向排他鎖這兩種。

當一個事務需要給自己需要的某個資源加鎖的時候,如果遇到 一個共享鎖正鎖定着自己需要的資源的時候,自己可以再加一個共享鎖,不過不能加排他鎖。但是,如果遇到自己需要鎖定的資源已經被一個排他鎖佔有之後,則只 能等待該鎖定釋放資源之後自己才能獲取鎖定資源並添加自己的鎖定。而意向鎖的作用就是當一個事務在需要獲取資源鎖定的時候,如果遇到自己需要的資源已經被 排他鎖佔用的時候,該事務可以需要鎖定行的表上面添加一個合適的意向鎖。如果自己需要一個共享鎖,那麼就在表上面添加一個意向共享鎖。而如果自己需要的是 某行(或者某些行)上面添加一個排他鎖的話,則先在表上面添加一個意向排他鎖。意向共享鎖可以同時並存多個,但是意向排他鎖同時只能有一個存在。所以,可 以說Innodb 的鎖定模式實際上可以分爲四種:共享鎖(S),排他鎖(X),意向共享鎖(IS)和意向排他鎖(IX),我們可以通過以下表格來總結上面這四種所的共存邏 輯關係:

  共享鎖
(S)
排他鎖
(X)
意向共享鎖
(IS)
意向排他鎖
(IX)
共享鎖(S) 兼容 衝突 兼容 衝突
排他鎖(X) 衝突 衝突 衝突 衝突
意向共享鎖
(IS)
兼容 衝突 兼容 兼容
意向排他鎖
(IX)
衝突 衝突 兼容 兼容

雖 然Innodb 的鎖定機制和Oracle 有不少相近的地方,但是兩者的實現確是截然不同的。總的來說就是Oracle 鎖定數據是通過需要鎖定的某行記錄所在的物理block 上的事務槽上表級鎖定信息,而Innodb的鎖定則是通過在指向數據記錄的第一個索引鍵之前和最後一個索引鍵之後的空域空間上標記鎖定信息而實現的。 Innodb 的這種鎖定實現方式被稱爲“NEXT-KEY locking”(間隙鎖),因爲Query 執行過程中通過過範圍查找的華,他會鎖定整個範圍內所有的索引鍵值,即使這個鍵值並不存在。

間隙鎖有一個比較致命的弱點,就是當鎖定一個範 圍鍵值之後,即使某些不存在的鍵值也會被無辜的鎖定,而造成在鎖定的時候無法插入鎖定鍵值範圍內的任何數據。在某些場景下這可能會對性能造成很大的危害。 而Innodb 給出的解釋是爲了組織幻讀的出現,所以他們選擇的間隙鎖來實現鎖定。

除了間隙鎖給Innodb 帶來性能的負面影響之外,通過索引實現鎖定的方式還存在其他幾個較大的性能隱患:

  • 當Query 無法利用索引的時候,Innodb 會放棄使用行級別鎖定而改用表級別的鎖定,造成併發性能的降低;
  • 當Quuery 使用的索引並不包含所有過濾條件的時候,數據檢索使用到的索引鍵所只想的數據可能有部分並不屬於該Query 的結果集的行列,但是也會被鎖定,因爲間隙鎖鎖定的是一個範圍,而不是具體的索引鍵;
  • 當Query 在使用索引定位數據的時候,如果使用的索引鍵一樣但訪問的數據行不同的時候(索引只是過濾條件的一部分),一樣會被鎖定

Innodb 各事務隔離級別下鎖定及死鎖

Innodb 實現的在ISO/ANSI SQL92 規範中所定義的Read UnCommited,Read Commited,Repeatable Read 和Serializable 這四種事務隔離級別。同時,爲了保證數據在事務中的一致性,實現了多版本數據訪問。

之前在第一節中我們已經 介紹過,行級鎖定肯定會帶來死鎖問題,Innodb 也不可能例外。至於死鎖的產生過程我們就不在這裏詳細描述了,在後面的鎖定示例中會通過一個實際的例子爲大家愛展示死鎖的產生過程。這裏我們主要介紹一 下,在Innodb 中當系檢測到死鎖產生之後是如何來處理的。

在Innodb 的事務管理和鎖定機制中,有專門檢測死鎖的機制,會在系統中產生死鎖之後的很短時間內就檢測到該死鎖的存在。當Innodb 檢測到系統中產生了死鎖之後,Innodb 會通過相應的判斷來選這產生死鎖的兩個事務中較小的事務來回滾,而讓另外一個較大的事務成功完成。那Innodb 是以什麼來爲標準判定事務的大小的呢?MySQL 官方手冊中也提到了這個問題,實際上在Innodb 發現死鎖之後,會計算出兩個事務各自插入、更新或者刪除的數據量來判定兩個事務的大小。也就是說哪個事務所改變的記錄條數越多,在死鎖中就越不會被回滾 掉。但是有一點需要注意的就是,當產生死鎖的場景中涉及到不止Innodb 存儲引擎的時候,Innodb 是沒辦法檢測到該死鎖的,這時候就只能通過鎖定超時限制來解決該死鎖了。另外,死鎖的產生過程的示例將在本節最後的Innodb 鎖定示例中演示。

Innodb 鎖定機制示例

mysql> create table test_innodb_lock (a int(11),b varchar(16)) engine=innodb;
Query OK, 0 rows affected (0.02 sec)
mysql> create index test_innodb_a_ind on test_innodb_lock(a);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> create index test_innodb_lock_b_ind on test_innodb_lock(b);
Query OK, 11 rows affected (0.01 sec)
Records: 11 Duplicates: 0 Warnings: 0

時刻 Session a Session b
  行鎖定基本演示  
1 mysql> set autocommit=0;
Query OK, 0 rows affected (0.00
sec)
mysql> update test_innodb_lock
set b = 'b1' where a = 1;
Query OK, 1 row affected (0.00
sec)
Rows matched: 1 Changed: 1
Warnings: 0
更新,但是不提交
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
2   mysql> update test_innodb_lock set b
= 'b1' where a = 1;
被阻塞,等待
3 mysql> commit;
Query OK, 0 rows affected (0.05
sec)
提交
 
4   mysql> update test_innodb_lock set b
= 'b1' where a = 1;
Query OK, 0 rows affected (36.14 sec)
Rows matched: 1 Changed: 0 Warnings:
0
解除阻塞,更新正常進行
  無索引升級爲表鎖演示  
5 mysql> update test_innodb_lock
set b = '2' where b = 2000;
Query OK, 1 row affected (0.02
sec)
Rows matched: 1 Changed: 1
Warnings: 0
 
6   mysql> update test_innodb_lock set b
= '3' where b = 3000;
被阻塞,等待
7 mysql> commit;
Query OK, 0 rows affected (0.10
sec)
 
8   mysql> update test_innodb_lock set b
= '3' where b = 3000;
Query OK, 1 row affected (1 min 3.41
sec)
Rows matched: 1 Changed: 1 Warnings:
0
阻塞解除,完成更新
  間隙鎖帶來的插入問題演示  
9 mysql> select * from
test_innodb_lock;
+------+------+
| a | b |
+------+------+
| 1 | b2 |
| 3 | 3 |
| 4 | 4000 |
| 5 | 5000 |
| 6 | 6000 |
| 7 | 7000 |
| 8 | 8000 |
| 9 | 9000 |
| 1 | b1 |
+------+------+
9 rows in set (0.00 sec)
mysql> update test_innodb_lock
set b = a * 100 where a < 4 and
a > 1;
Query OK, 1 row affected (0.02
sec)
Rows matched: 1 Changed: 1
Warnings: 0
 
10   mysql> insert into test_innodb_lock
values(2,'200');
被阻塞,等待
11 mysql> commit;
Query OK, 0 rows affected (0.02
sec)
 
12   mysql> insert into test_innodb_lock
values(2,'200');
Query OK, 1 row affected (38.68 sec)
阻塞解除,完成插入
  使用共同索引不同數據的阻塞示例  
13 mysql> update test_innodb_lock
set b = 'bbbbb' where a = 1 and
b = 'b2';
Query OK, 1 row affected (0.00
sec)
Rows matched: 1 Changed: 1
Warnings: 0
 
14   mysql> update test_innodb_lock set b
= 'bbbbb' where a = 1 and b = 'b1';
被阻塞
15 mysql> commit;
Query OK, 0 rows affected (0.02
sec)
 
16   mysql> update test_innodb_lock set b
= 'bbbbb' where a = 1 and b = 'b1';
Query OK, 1 row affected (42.89 sec)
Rows matched: 1 Changed: 1 Warnings:
0
session 提交事務,阻塞去除,更新完成
  死鎖示例  
17 mysql> update t1 set id = 110
where id = 11;
Query OK, 0 rows affected (0.00
sec)
Rows matched: 0 Changed: 0
Warnings: 0
 
18   mysql> update t2 set id = 210 where
id = 21;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings:
0
19 mysql> update t2 set id = 2100
where id = 21;
等待session b 釋放資源,被阻塞
 
20   mysql> update t1 set id = 1100 where
id = 11;
Query OK, 0 rows affected (0.39 sec)
Rows matched: 0 Changed: 0 Warnings:
0
等待session a 釋放資源,被阻塞
  兩個session 互相等等待對方的資源釋放之後才能釋放自己的資源,造成了死鎖

7.3 合理利用鎖機制優化MySQL

MyISAM 表鎖優化建議

對 於MyISAM 存儲引擎,雖然使用表級鎖定在鎖定實現的過程中比實現行級鎖定或者頁級鎖所帶來的附加成本都要小,鎖定本身所消耗的資源也是最少。但是由於鎖定的顆粒度比 較到,所以造成鎖定資源的爭用情況也會比其他的鎖定級別都要多,從而在較大程度上會降低併發處理能力。

所以,在優化MyISAM 存儲引擎鎖定問題的時候,最關鍵的就是如何讓其提高併發度。由於鎖定級別是不可能改變的了,所以我們首先需要儘可能讓鎖定的時間變短,然後就是讓可能併發進行的操作儘可能的併發。

1、縮短鎖定時間

縮短鎖定時間,短短几個字,說起來確實聽容易的,但實際做起來恐怕就並不那麼簡單了。如何讓鎖定時間儘可能的短呢?唯一的辦法就是讓我們的Query 執行時間儘可能的短。

a) 盡兩減少大的複雜Query,將複雜Query 分拆成幾個小的Query 分佈進行;
b) 儘可能的建立足夠高效的索引,讓數據檢索更迅速;
c) 儘量讓MyISAM 存儲引擎的表只存放必要的信息,控制字段類型;
d) 利用合適的機會優化MyISAM 表數據文件;

2、分離能並行的操作

說 到MyISAM 的表鎖,而且是讀寫互相阻塞的表鎖,可能有些人會認爲在MyISAM 存儲引擎的表上就只能是完全的串行化,沒辦法再並行了。大家不要忘記了,MyISAM 的存儲引擎還有一個非常有用的特性,那就是Concurrent Insert(併發插入)的特性。

MyISAM 存儲引擎有一個控制是否打開Concurrent Insert 功能的參數選項:concurrent_insert,可以設置爲0,1 或者2。三個值的具體說明如下:

a) concurrent_insert=2,無論MyISAM 存儲引擎的表數據文件的中間部分是否存在因爲刪除數據而留下的空閒空間,都允許在數據文件尾部進行Concurrent Insert;
b) concurrent_insert=1,當MyISAM 存儲引擎表數據文件中間不存在空閒空間的時候,可以從文件尾部進行Concurrent Insert;
c) concurrent_insert=0,無論MyISAM 存儲引擎的表數據文件的中間部分是否存在因爲刪除數據而留下的空閒空間,都不允許Concurrent Insert。

3、合理利用讀寫優先級

在 本章各種鎖定分析一節中我們瞭解到了MySQL 的表級鎖定對於讀和寫是有不同優先級設定的,默認情況下是寫優先級要大於讀優先級。所以,如果我們可以根據各自系統環境的差異決定讀與寫的優先級。如果我 們的系統是一個以讀爲主,而且要優先保證查詢性能的話,我們可以通過設置系統參數選項low_priority_updates=1,將寫的優先級設置爲 比讀的優先級低,即可讓告訴MySQL 儘量先處理讀請求。當然,如果我們的系統需要有限保證數據寫入的性能的話,則可以不用設置low_priority_updates
參數了。

這 裏我們完全可以利用這個特性,將concurrent_insert 參數設置爲1,甚至如果數據被刪除的可能性很小的時候,如果對暫時性的浪費少量空間並不是特別的在乎的話,將concurrent_insert 參數設置爲2 都可以嘗試。當然,數據文件中間留有空域空間,在浪費空間的時候,還會造成在查詢的時候需要讀取更多的數據,所以如果刪除量不是很小的話,還是建議將 concurrent_insert 設置爲1 更爲合適。

Innodb 行鎖優化建議

Innodb 存儲引擎由於實現了行級鎖定,雖然在鎖定機制的實現方面所帶來的性能損耗可能比表級鎖定會要更高一些,但是在整體併發處理能力方面要遠遠優於MyISAM 的表級鎖定的。當系統併發量較高的時候,Innodb 的整體性能和MyISAM 相比就會有比較明顯的優勢了。但是,Innodb 的行級鎖定同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓Innodb 的整體性能表現不僅不能比MyISAM 高,甚至可能會更差。

要想合理利用Innodb 的行級鎖定,做到揚長避短,我們必須做好以下工作:

a) 儘可能讓所有的數據檢索都通過索引來完成,從而避免Innodb 因爲無法通過索引鍵加鎖而升級爲表級鎖定;
b) 合理設計索引,讓Innodb 在索引鍵上面加鎖的時候儘可能準確,儘可能的縮小鎖定範圍,避免造成不必要的鎖定而影響其他Query 的執行;
c) 儘可能減少基於範圍的數據檢索過濾條件,避免因爲間隙鎖帶來的負面影響而鎖定了不該鎖定的記錄;
d) 儘量控制事務的大小,減少鎖定的資源量和鎖定時間長度;
e) 在業務環境允許的情況下,儘量使用較低級別的事務隔離,以減少MySQL 因爲實現事務隔離級別所帶來的附加成本;

由於Innodb 的行級鎖定和事務性,所以肯定會產生死鎖,下面是一些比較常用的減少死鎖產生概率的的小建議,讀者朋友可以根據各自的業務特點針對性的嘗試:

a) 類似業務模塊中,儘可能按照相同的訪問順序來訪問,防止產生死鎖;
b) 在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖產生概率;
c) 對於非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率;

系統鎖定爭用情況查詢

對於兩種鎖定級別,MySQL 內部有兩組專門的狀態變量記錄系統內部鎖資源爭用情況,我們先看看MySQL 實現的表級鎖定的爭用狀態變量:

mysql> show status like 'table%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 100 |
| Table_locks_waited | 0 |
+-----------------------+-------+

這裏有兩個狀態變量記錄MySQL 內部表級鎖定的情況,兩個變量說明如下:

  • Table_locks_immediate:產生表級鎖定的次數;
  • Table_locks_waited:出現表級鎖定爭用而發生等待的次數;

兩個狀態值都是從系統啓動後開始記錄,沒出現一次對應的事件則數量加1。如果這裏的Table_locks_waited 狀態值比較高,那麼說明系統中表級鎖定爭用現象比較嚴重,就需要進一步分析爲什麼會有較多的鎖定資源爭用了。

對於Innodb 所使用的行級鎖定,系統中是通過另外一組更爲詳細的狀態變量來記錄的,如下:

mysql> show status like 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name | Value |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 490578 |
| Innodb_row_lock_time_avg | 37736 |
| Innodb_row_lock_time_max | 121411 |
| Innodb_row_lock_waits | 13 |
+-------------------------------+--------+

Innodb 的行級鎖定狀態變量不僅記錄了鎖定等待次數,還記錄了鎖定總時長,每次平均時長,以及最大時長,此外還有一個非累積狀態量顯示了當前正在等待鎖定的等待數量。對各個狀態量的說明如下:

  • Innodb_row_lock_current_waits:當前正在等待鎖定的數量;
  • Innodb_row_lock_time:從系統啓動到現在鎖定總時間長度;
  • Innodb_row_lock_time_avg:每次等待所花平均時間;
  • Innodb_row_lock_time_max:從系統啓動到現在等待最常的一次所花的時間;
  • Innodb_row_lock_waits:系統啓動後到現在總共等待的次數;

對 於這5 個狀態變量, 比較重要的主要是Innodb_row_lock_time_avg( 等待平均時長) ,Innodb_row_lock_waits(等待總次數)以及Innodb_row_lock_time(等待總時長)這三項。尤其是當等待次數很 高,而且每次等待時長也不小的時候,我們就需要分析系統中爲什麼會有如此多的等待,然後根據分析結果着手指定優化計劃。

此外,Innodb 出了提供這五個系統狀態變量之外,還提供的其他更爲豐富的即時狀態信息供我們分析使用。可以通過如下方法查看:

1. 通過創建Innodb Monitor 表來打開Innodb 的monitor 功能:

mysql> create table innodb_monitor(a int) engine=innodb;
Query OK, 0 rows affected (0.07 sec)

2. 然後通過使用“SHOW INNODB STATUS”查看細節信息(由於輸出內容太多就不在此記錄了);可能會有讀者朋友問爲什麼要先創建一個叫innodb_monitor 的表呢?因爲創建該表實際上就是告訴Innodb 我們開始要監控他的細節狀態了,然後Innodb 就會將比較詳細的事務以及鎖定信息記錄進入MySQL 的error log 中,以便我們後面做進一步分析使用。

7.4 小結

本 章以MySQL Server 中的鎖定簡介開始,分析了當前MySQL 中使用最爲廣泛的鎖定方式表級鎖定和行級鎖定的基本實現機制,並通過MyISAM 和Innodb 這兩大典型的存儲引擎作爲示例存儲引擎所使用的表級鎖定和行級鎖定做了較爲詳細的分析和演示。然後,再通過分析兩種鎖定方式的特性,給出相應的優化建議和 策略。最後瞭解了一下在MySQL Server 中如何獲得系統當前各種鎖定的資源爭用狀況。希望本章內容能夠對各位讀者朋友在理解MySQL 鎖定機制方面有一定的幫助。

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