一、並發控制中鎖的概念
鎖是並發控制中最核心的概念之一,在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上輸入 COMMIT 和 ROLLBACK 才能看到修改的數據;
mysql> ROLLBACK; mysql> SELECT * FROM tbl2; +------+-------+ | id | Name | +------+-------+ | 1 | tom | | 2 | jerry | | 3 | cat | | 4 | dog | +------+-------+ 4 rows in set (0.00 sec)