1. 程式人生 > >MySQL中的事務及讀寫鎖實現並發訪問控制

MySQL中的事務及讀寫鎖實現並發訪問控制

hang dea 執行c 定時 ack 幫助 持久 表操作 查看

一、並發控制中鎖的概念

  鎖是並發控制中最核心的概念之一,在MySQL中的鎖分兩大類,一種是讀鎖,一種是寫鎖,讀鎖也可以稱為共享鎖(shared lock),寫鎖也通常稱為排它鎖(exclusive lock)。

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

  MySQL中的鎖有兩種粒度,一種是表鎖

,在表級別加鎖,是MySQL中最基本的鎖策略,並且開銷最小,這種鎖的並發性能較低;另一種為行鎖,在行級加鎖,並發性較高。表鎖與行鎖沒有絕對的性能強弱之分,在應用中可以根據實際場景選擇,在鎖粒度與數據安全之間尋求一種平衡機制。

  鎖的具體實現協議大體分為兩種:顯式鎖和隱式鎖。顯式鎖是指根據用戶需要手動去請求的鎖。隱式鎖則是指存儲引擎自行根據需要施加的鎖。顯式鎖的用法示例:

  例1:開啟兩個ssh連接同一主機,進入MySQL,在連接A上對表tbl2做讀鎖操作:

1 mysql> USE mysql;
2 mysql> LOCK TABLE tbl2 READ;

  在連接B上讀取數據是可以的,但是寫入數據不行:

1 mysql> USE mysql;
2 mysql> SELECT * FROM tbl2;
3 mysql> INSERT INTO tbl2 VALUES (1,tom); #會一直卡在這一步,不向後執行。

技術分享圖片

  當在連接1上將tbl2解鎖後,就能寫入數據了:

技術分享圖片

技術分享圖片

  例2: FLUSH TABLES 命令可以將整個庫上鎖,表示刷寫所有表,把所有表在緩存中的數據全寫入磁盤。在對整個數據庫進行備份時可能會用到。

mysql> FLUSH TABLES WITH READ LOCK; #刷寫所有表,並持有讀鎖;

  解鎖也是用命令 UNLOCK TABLES

  鎖是任何存儲引擎都支持的並發訪問控制機制,但對事務型存儲引擎來講,這種鎖機制都是單語句級別的,而事務存儲引擎更需要多語句的並發控制機制。以上兩個例子所實現的鎖都是服務器層的,和存儲引擎無關。另外在生產環境中除了事務中禁用了 AUTOCOMMIT 時可以使用 LOCK TABLES 之外,其他任何時候都不要顯式的執行 LOCK TABLES

二、事務

1.事務的概念

  事務其實就是一組原子性的SQL查詢,或者說一個獨立的工作單元。用一個經典的存取錢的例子可以來解釋什麽是事務:假設一個銀行的數據庫有支票(checking)和存錢(savings)兩張表。現在要從用戶A的支票賬戶中轉移200元到他的存錢賬戶,邏輯上我們需要三個步驟:

1.檢查支票賬戶余額大於200;

2.在支票賬戶中減去200;

3.在存錢賬戶中加上200。

上述三個步驟必須作為一個整體來操作,這個整體就可以稱為事務。事務中任何一步失敗都必須回滾(ROLLBACK)所有步驟。

  可以用 START TRANSACTION 語句開始一個事務,然後可以用 COMMIT 將事務提交並將修改的數據永久保存在磁盤,也可用 ROLLBACK 撤銷所有修改。銀行例子的SQL樣本如下:

1 START TRANSACTION;
2 SELECT balance FROM checking WHERE customer_id = 12345;
3 UPDATE checking SET balance = balance - 200 WHERE customer_id = 12345;
4 UPDATE savings SET balance = balance + 200 WHERE customer_id = 12345;
5 COMMIT;

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

原子性:一個事務必須被視為一個不可分割的最小工作單元,整個事務中的所有操作要麽全部提交成功,要麽全部失敗回滾。

一致性:數據庫總是從一個一致性的狀態轉換到另一個一致性的狀態。

隔離性:通常來說,一個事務所做的修改在最終提交前,對其它事務是不可見的。例如在前面的例子中,當第二條語句執行完,第三條語句還未開始時,另一個賬戶的匯款事務也開始執行,則其看到的賬戶狀態是存取事務運行前的狀態。這就涉及到了隔離級別,將在後面來介紹。

持久性:一旦事務提交,則所有修改就永久保存在數據庫的磁盤中。

1.事務日誌

  要保持事務的原子性,需要依靠事務日誌。在一個事務運行到一半發生錯誤時,事務需要根據事務日誌的操作記錄來回滾至原來的狀態。在事務運行中,事務執行修改的需求是先放在事務日誌中的,再對磁盤數據進行修改,事務日誌中存放的是修改的操作,而不是修改數據本身。

  事務日誌可以幫助提高事務的效率。寫事務日誌的操作是磁盤上一小塊區域的順序I/O,而不像隨機I/O在磁盤的多個地方移動磁頭,這就提高了存儲效率。目前大多數存儲引擎都是這樣實現的,修改數據通常需要寫兩次吸盤。

  事務日誌丟失會造成很大的損失,所以建議做雙寫。

2.事務的提交

  自動提交(AUTOCOMMIT):MySQL默認采用自動提交模式。也就是說只要不是顯式的開始一個事務,則每個查詢都被當作一個事務執行提交操作。設置如下:

mysql> SHOW VARIABLES LIKE AUTOCOMMIT;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

mysql> SET AUTOCOMMIT = 0; #設置為0表示OFF,當AUTOAOMMIT=0時,所有的查詢都在一個事務中,直到顯式的執行COMMIT或者ROLLBACK,該事物才結束。

  事務提交相關語句:

mysql> HELP TRANSACTIONS;
topics:
   CHANGE MASTER TO
   CHANGE REPLICATION FILTER
   DEALLOCATE PREPARE
   EXECUTE STATEMENT
   ISOLATION
   LOCK
   PREPARE
   PURGE BINARY LOGS
   RESET MASTER
   RESET SLAVE
   SAVEPOINT
   SET GLOBAL SQL_SLAVE_SKIP_COUNTER
   SET SQL_LOG_BIN
   START SLAVE
   START TRANSACTION #啟動事務
   STOP SLAVE
   XA

  下面將 SET AUTOCOMMIT 改為OFF,用手動方式提交事務進行簡單演示:MySQL默認庫中創建表tbl2,插入行id=1,Name=tom

mysql> SELECT * FROM tbl2;
+------+------+
| id   | Name |
+------+------+
|    1 | tom  |
+------+------+
1 row in set (0.00 sec)
mysql> START TRANSACTION;
mysql> USE mysql;
mysql> UPDATE tbl2 SET Name=Tom WHERE id=1; #將小寫t改為大寫T
mysql> SELECT * FROM tbl2;
+------+------+
| id   | Name |
+------+------+
|    1 | Tom  |
+------+------+
1 row in set (0.00 sec)
mysql> START TRANSACTION; #開始事務
mysql> USE mysql;
mysql> UPDATE tbl2 SET Name=Tom WHERE id=1; #將小寫t改為大寫T
mysql> SELECT * FROM tbl2; #查看
+------+------+
| id   | Name |
+------+------+
|    1 | Tom  |
+------+------+
mysql> ROLLBACK; #回滾
mysql> SELECT * FROM tbl2; #恢復到小寫t
+------+------+
| id   | Name |
+------+------+
|    1 | tom  |
+------+------+

  要註意的是,創建表操作的滾動操作只對DML語言有效。

  還可以在每個操作後用指令 SAVEPOINT 做存檔:

mysql> START TRANSACTION;
mysql> SELECT * FROM tbl2;
+------+------+
| id   | Name |
+------+------+
|    1 | tom  |
+------+------+
1 row in set (0.00 sec)
mysql> INSERT INTO tbl2 VALUES (2,jerry);
mysql> SAVEPOINT first; #創建保存點1,名為first
mysql> INSERT INTO tbl2 VALUES (3,cat);
mysql> SAVEPOINT second; #創建保存點2,名為second
mysql> DELETE FROM tbl2 WHERE id=2; 
mysql> SELECT * FROM tbl2;
+------+------+
| id   | Name |
+------+------+
|    1 | tom  |
|    3 | cat  |
+------+------+
2 rows in set (0.00 sec)
mysql> ROLLBACK TO second; #滾到保存點second
mysql> SELECT * FROM tbl2;
+------+-------+
| id   | Name  |
+------+-------+
|    1 | tom   |
|    2 | jerry |
|    3 | cat   |
+------+-------+
3 rows in set (0.00 sec)

  輸入命令 COMMIT 後表示事務已提交,就無法再回滾了。

3.事務隔離級別

  事務的隔離級別有四種:

1.READ UNCOMMITTED(未提交讀)

  在READ UNCOMMITTED級別,事務中的修改即使沒有提交,對其它事務也都是可見的。即事務可讀取未提交的數據,這稱為臟讀(Dirty Read)。這會導致很多問題,在實際應用中一般很少用到。

2.READ COMMITED(提交讀)

  READ COMMITTED表示只能讀取事務修改提交後的數據。此級別有時候也叫做不可重復讀(nonrepeatable read),因為兩次執行同樣的查詢,可能會得到不一樣的結果。

3.REPEATABLE READ(可重復讀)

  此級別解決了臟讀的問題,保證了在同一個事務中多次同樣記錄的結果是一致的。但會帶來新的問題——幻讀(Phantom Read)。MySQL默認使用此級別。

4.SERIALIZABLE(可串行化)

  SERIALIZABLE是最高的隔離級別。它會強制事務串行執行,避免了幻讀、臟讀的問題,但是犧牲了並發性。

  示例:

mysql> SELECT @@session.tx_isolation; #查看當前事務隔離級別
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)

mysql> SET @@session.tx_isolation=級別; 可設置隔離級別

下面演示幻讀的情形,啟動兩個連接至同一MySQL,交叉啟動事務:

連接1的操作:

mysql> START TRANSACTION; #啟動事務
mysql> USE mysql;
mysql> SELECT * FROM tbl2;
+------+-------+
| id   | Name  |
+------+-------+
|    1 | tom   |
|    2 | jerry |
|    3 | cat   |
+------+-------+
3 rows in set (0.00 sec)
mysql> INSERT INTO tbl2 VALUES (4,dog); #插入數據
mysql> SELECT * FROM tbl2;
+------+-------+
| id   | Name  |
+------+-------+
|    1 | tom   |
|    2 | jerry |
|    3 | cat   |
|    4 | dog   |
+------+-------+
4 rows in set (0.00 sec)

同時在連接2的操作:

mysql> START TRANSACTION;
Database changed
mysql> SELECT * FROM tbl2;
+------+-------+
| id   | Name  |
+------+-------+
|    1 | tom   |
|    2 | jerry |
|    3 | cat   |
+------+-------+
3 rows in set (0.00 sec)

這時看不到插入的"4 dog"的數據,在連接1上執行 COMMIT 提交事務:

mysql> COMMIT;

在連接2上依然看不到插入的數據,但數據確實已經寫入到了磁盤,這時只有在連接2上輸入 COMMITROLLBACK 才能看到修改的數據;

mysql> ROLLBACK;
mysql> SELECT * FROM tbl2;
+------+-------+
| id   | Name  |
+------+-------+
|    1 | tom   |
|    2 | jerry |
|    3 | cat   |
|    4 | dog   |
+------+-------+
4 rows in set (0.00 sec)

MySQL中的事務及讀寫鎖實現並發訪問控制