MyISAM表鎖

  MyISAM存儲引擎只支持表鎖,這也是MySQL開始幾個版本中唯一支持的鎖類型。隨着應用對事務完整性和併發性要求的不斷提高,MySQL纔開始開發基於事務的存儲引擎,後來慢慢出現了支持頁鎖的BDB存儲引擎和支持行鎖的InnoDB存儲引擎(實際InnoDB是單獨的一個公司,現在已經被Oracle公司收購)。但是MyISAM的表鎖依然是使用最爲廣泛的鎖類型。本節將詳細介紹MyISAM表鎖的使用。

查詢表級鎖爭用情況

可以通過檢查table_locks_waited和table_locks_immediate狀態變量來分析系統上的表鎖定爭奪:

mysql> show status like 'table%';

+--------------------------------- +-------+

| Variable_name                   | Value |

+---------------------------------+--------+

| Table_locks_immediate      | 2979  |

| Table_locks_waited            | 0        |

+----------------------------------+--------+

2 rows in set (0.00 sec))

如果Table_locks_waited的值比較高,則說明存在着較嚴重的表級鎖爭用情況。

MySQL表級鎖的鎖模式

        MySQL的表級鎖有兩種模式:表共享讀鎖(Table Read Lock)和表獨佔寫鎖(Table Write Lock)。鎖模式的兼容性如表20-1所示。

表20-1                                            MySQL中的表鎖兼容性                

請求鎖模式

         是否兼容

當前鎖模式

None

讀鎖

寫鎖

讀鎖

寫鎖

        可見,對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求;對MyISAM表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;MyISAM表的讀操作與寫操作之間,以及寫操作之間是串行的!根據如表20-2所示的例子可以知道,當一個線程獲得對一個表的寫鎖後,只有持有鎖的線程可以對錶進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放爲止。

表20-2                          MyISAM存儲引擎的寫阻塞讀例子

session_1

session_2

獲得表film_text的WRITE鎖定

mysql> lock table film_text write;

Query OK, 0 rows affected (0.00 sec)

 

當前session對鎖定表的查詢、更新、插入操作都可以執行:

mysql> select film_id,title from film_text where film_id = 1001;

+---------+-------------+

| film_id  | title          |

+---------+-------------+

| 1001    | Update Test |

+---------+-------------+

1 row in set (0.00 sec)

mysql> insert into film_text (film_id,title) values(1003,'Test');

Query OK, 1 row affected (0.00 sec)

mysql> update film_text set title = 'Test' where film_id = 1001;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

其他session對鎖定表的查詢被阻塞,需要等待鎖被釋放:

mysql> select film_id,title from film_text where film_id = 1001;

等待

釋放鎖:

mysql> unlock tables;

Query OK, 0 rows affected (0.00 sec)

等待

 

Session2獲得鎖,查詢返回:

mysql> select film_id,title from film_text where film_id = 1001;

+---------+-------+

| film_id | title |

+---------+-------+

| 1001    | Test  |

+---------+-------+

1 row in set (57.59 sec)

如何加表鎖

        MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預,因此,用戶一般不需要直接用LOCK TABLE命令給MyISAM表顯式加鎖。在本書的示例中,顯式加鎖基本上都是爲了方便而已,並非必須如此。

        給MyISAM表顯示加鎖,一般是爲了在一定程度模擬事務操作,實現對某一時間點多個表的一致性讀取。例如,有一個訂單表orders,其中記錄有各訂單的總金額total,同時還有一個訂單明細表order_detail,其中記錄有各訂單每一產品的金額小計subtotal,假設我們需要檢查這兩個表的金額合計是否相符,可能就需要執行如下兩條SQL:

Select sum(total) from orders;

Select sum(subtotal) from order_detail;

         這時,如果不先給兩個表加鎖,就可能產生錯誤的結果,因爲第一條語句執行過程中,order_detail表可能已經發生了改變。因此,正確的方法應該是:

Lock tables orders read local, order_detail read local;

Select sum(total) from orders;

Select sum(subtotal) from order_detail;

Unlock tables;

要特別說明以下兩點內容。

        上面的例子在LOCK TABLES時加了“local”選項,其作用就是在滿足MyISAM表併發插入條件的情況下,允許其他用戶在表尾併發插入記錄,有關MyISAM表的併發插入問題,在後面的章節中還會進一步介紹。

        在用LOCK TABLES給表顯式加表鎖時,必須同時取得所有涉及到表的鎖,並且MySQL不支持鎖升級。也就是說,在執行LOCK TABLES後,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那麼只能執行查詢操作,而不能執行更新操作。其實,在自動加鎖的情況下也基本如此,MyISAM總是一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因。

        在如表20-3所示的例子中,一個session使用LOCK TABLE命令給表film_text加了讀鎖,這個session可以查詢鎖定表中的記錄,但更新或訪問其他表都會提示錯誤;同時,另外一個session可以查詢表中的記錄,但更新就會出現鎖等待。

表20-3                     MyISAM存儲引擎的讀阻塞寫例子

session_1

session_2

獲得表film_text的READ鎖定

mysql> lock table film_text read;

Query OK, 0 rows affected (0.00 sec)

 

當前session可以查詢該表記錄

mysql> select film_id,title from film_text where film_id = 1001;

+---------+------------------+

| film_id | title            |

+---------+------------------+

| 1001    | ACADEMY DINOSAUR |

+---------+------------------+

1 row in set (0.00 sec)

其他session也可以查詢該表的記錄

mysql> select film_id,title from film_text where film_id = 1001;

+---------+------------------+

| film_id | title            |

+---------+------------------+

| 1001    | ACADEMY DINOSAUR |

+---------+------------------+

1 row in set (0.00 sec)

當前session不能查詢沒有鎖定的表

mysql> select film_id,title from film where film_id = 1001;

ERROR 1100 (HY000): Table 'film' was not locked with LOCK TABLES

其他session可以查詢或者更新未鎖定的表

mysql> select film_id,title from film where film_id = 1001;

+---------+---------------+

| film_id | title         |

+---------+---------------+

| 1001    | update record |

+---------+---------------+

1 row in set (0.00 sec)

mysql> update film set title = 'Test' where film_id = 1001;

Query OK, 1 row affected (0.04 sec)

Rows matched: 1  Changed: 1  Warnings: 0

當前session中插入或者更新鎖定的表都會提示錯誤:

mysql> insert into film_text (film_id,title) values(1002,'Test');

ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated

mysql> update film_text set title = 'Test' where film_id = 1001;

ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated

其他session更新鎖定表會等待獲得鎖:

mysql> update film_text set title = 'Test' where film_id = 1001;

等待

釋放鎖

mysql> unlock tables;

Query OK, 0 rows affected (0.00 sec)

等待

 

Session獲得鎖,更新操作完成:

mysql> update film_text set title = 'Test' where film_id = 1001;

Query OK, 1 row affected (1 min 0.71 sec)

Rows matched: 1  Changed: 1  Warnings: 0

當使用LOCK TABLES時,不僅需要一次鎖定用到的所有表,而且,同一個表在SQL語句中出現多少次,就要通過與SQL語句中相同的別名鎖定多少次,否則也會出錯!舉例說明如下。

(1)對actor表獲得讀鎖:

mysql> lock table actor read;

Query OK, 0 rows affected (0.00 sec)

(2)但是通過別名訪問會提示錯誤:

mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' and a.last_name <> b.last_name;

ERROR 1100 (HY000): Table 'a' was not locked with LOCK TABLES

(3)需要對別名分別鎖定:

mysql> lock table actor as a read,actor as b read;

Query OK, 0 rows affected (0.00 sec)

(4)按照別名的查詢可以正確執行:

mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' and a.last_name <> b.last_name;

+------------+-----------+------------+-----------+

| first_name | last_name | first_name | last_name |

+------------+-----------+------------+-----------+

| Lisa       | Tom       | LISA       | MONROE    |

+------------+-----------+------------+-----------+

1 row in set (0.00 sec)

併發插入(Concurrent Inserts)

        上文提到過MyISAM表的讀和寫是串行的,但這是就總體而言的。在一定條件下,MyISAM表也支持查詢和插入操作的併發進行。

MyISAM存儲引擎有一個系統變量concurrent_insert,專門用以控制其併發插入的行爲,其值分別可以爲0、1或2。

     當concurrent_insert設置爲0時,不允許併發插入。

     當concurrent_insert設置爲1時,如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),

MyISAM允許在一個進程讀表的同時,另一個進程從表尾插入記錄。這也是MySQL的默認設置。

     當concurrent_insert設置爲2時,無論MyISAM表中有沒有空洞,都允許在表尾併發插入記錄。

在如表20-4所示的例子中,session_1獲得了一個表的READ LOCAL鎖,該線程可以對錶進行查詢操作,但不能對錶進行更新操作;其他的線程(session_2),雖然不能對錶進行刪除和更新操作,但卻可以對該表進行併發插入操作,這裏假設該表中間不存在空洞。

表20-4              MyISAM存儲引擎的讀寫(INSERT)併發例子

session_1

session_2

獲得表film_text的READ LOCAL鎖定

mysql> lock table film_text read local;

Query OK, 0 rows affected (0.00 sec)

 

當前session不能對鎖定表進行更新或者插入操作:

mysql> insert into film_text (film_id,title) values(1002,'Test');

ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated

mysql> update film_text set title = 'Test' where film_id = 1001;

ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated

其他session可以進行插入操作,但是更新會等待:

mysql> insert into film_text (film_id,title) values(1002,'Test');

Query OK, 1 row affected (0.00 sec)

mysql> update film_text set title = 'Update Test' where film_id = 1001;

等待

當前session不能訪問其他session插入的記錄:

mysql> select film_id,title from film_text where film_id = 1002;

Empty set (0.00 sec)

 

釋放鎖:

mysql> unlock tables;

Query OK, 0 rows affected (0.00 sec)

等待

當前session解鎖後可以獲得其他session插入的記錄:

mysql> select film_id,title from film_text where film_id = 1002;

+---------+-------+

| film_id | title |

+---------+-------+

| 1002    | Test  |

+---------+-------+

1 row in set (0.00 sec)

Session2獲得鎖,更新操作完成:

mysql> update film_text set title = 'Update Test' where film_id = 1001;

Query OK, 1 row affected (1 min 17.75 sec)

Rows matched: 1  Changed: 1  Warnings: 0

        可以利用MyISAM存儲引擎的併發插入特性,來解決應用中對同一表查詢和插入的鎖爭用。例如,將concurrent_insert系統變量設爲2,總是允許併發插入;同時,通過定期在系統空閒時段執行OPTIMIZE TABLE語句來整理空間碎片,收回因刪除記錄而產生的中間空洞。有關OPTIMIZE TABLE語句的詳細介紹,可以參見第18章中“兩個簡單實用的優化方法”一節的內容。

MyISAM的鎖調度

        前面講過,MyISAM存儲引擎的讀鎖和寫鎖是互斥的,讀寫操作是串行的。那麼,一個進程請求某個MyISAM表的讀鎖,同時另一個進程也請求同一表的寫鎖,MySQL如何處理呢?答案是寫進程先獲得鎖。不僅如此,即使讀請求先到鎖等待隊列,寫請求後到,寫鎖也會插到讀鎖請求之前!這是因爲MySQL認爲寫請求一般比讀請求要重要。這也正是MyISAM表不太適合於有大量更新操作和查詢操作應用的原因,因爲,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。這種情況有時可能會變得非常糟糕!幸好我們可以通過一些設置來調節MyISAM的調度行爲。

        通過指定啓動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利。

       通過執行命令SET LOW_PRIORITY_UPDATES=1,使該連接發出的更新請求優先級降低。

       通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級。

        雖然上面3種方法都是要麼更新優先,要麼查詢優先的方法,但還是可以用其來解決查詢相對重要的應用(如用戶登錄系統)中,讀鎖等待嚴重的問題。

        另外,MySQL也提供了一種折中的辦法來調節讀寫衝突,即給系統參數max_write_lock_count設置一個合適的值,當一個表的讀鎖達到這個值後,MySQL就暫時將寫請求的優先級降低,給讀進程一定獲得鎖的機會。

        上面已經討論了寫優先調度機制帶來的問題和解決辦法。這裏還要強調一點:一些需要長時間運行的查詢操作,也會使寫進程“餓死”!因此,應用中應儘量避免出現長時間運行的查詢操作,不要總想用一條SELECT語句來解決問題,因爲這種看似巧妙的SQL語句,往往比較複雜,執行時間較長,在可能的情況下可以通過使用中間表等措施對SQL語句做一定的“分解”,使每一步查詢都能在較短時間完成,從而減少鎖衝突。如果複雜查詢不可避免,應儘量安排在數據庫空閒時段執行,比如一些定期統計可以安排在夜間執行。

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