在MySQL中,行級鎖並不是直接鎖記錄,而是鎖索引。索引分為主鍵索引和非主鍵索引兩種,如果一條sql語句操作了主鍵索引,MySQL就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。
在UPDATE、DELETE操作時,MySQL不僅鎖定WHERE條件掃描過的所有索引記錄,而且會鎖定相鄰的鍵值(update時的set),即所謂的next-key locking。
案例分析1:
tab_test 結構如下:
id:主鍵;
state:狀態;
time:時間;
索引:idx_1(state,time)
出現死鎖的2條sql語句
update tab_test set state=1064,time=now() where state=1061 and time < date_sub(now(), INTERVAL 30 minute) update tab_test set state=1067,time=now () where id in (9921180)
原因分析:
當“update tab_test set state=1064,time=now() where state=1061 and time < date_sub(now(), INTERVAL 30 minute)”執行時,MySQL會使用idx_1索引,因此首先鎖定相關的索引記錄,因為idx_1是非主鍵索引,為執行該語句,MySQL還會鎖定主鍵索引。
假設“update tab_test set state=1067,time=now () where id in (9921180)”幾乎同時執行時,本語句首先鎖定主鍵索引,由於需要更新state的值,所以還需要鎖定idx_1的某些索引記錄。
這樣第一條語句鎖定了idx_1的記錄,等待主鍵索引,而第二條語句則鎖定了主鍵索引記錄,而等待idx_1的記錄,這樣死鎖就產生了。
在第一條語句給主鍵加鎖前,第二條語句已經給主鍵加了鎖,所以在高並發的數據操作時,死鎖的情況就容易產生
InnoDB 會自動檢測一個事務的死鎖並回滾一個或多個事務來防止死鎖。Innodb會選擇代價比較小的事務回滾,此次事務(1)解鎖並回滾,語句(2)繼續運行直至事務結束。
解決辦法
拆分第一條sql,先查出符合條件的主鍵值,再按照主鍵更新記錄:
select id from tab_test where state=1061 and time < date_sub(now(), INTERVAL 30 minute); update tab_test state=1064,time=now() where id in(......);
案例分析2
teamUser表的表結構如下:
PRIMARY KEY (`uid`,`Id`),
KEY `k_id_titleWeight_score` (`Id`,`titleWeight`,`score`),
ENGINE=InnoDB
出現死鎖的2條sql語句
insert into teamUser_20110121 select * from teamUser DELETE FROM teamUser WHERE teamId=$teamId AND titleWeight<32768 AND joinTime<'$daysago_1week'
兩語句加鎖情況
在innodb默認的事務隔離級別下,普通的SELECT是不需要加行鎖的,但LOCK IN SHARE MODE、FOR UPDATE及高串行化級別中的SELECT都要加鎖。有一個例外,此案例中,語句(1)insert into teamUser_20110121 select * from teamUser會對表teamUser_20110121(ENGINE= MyISAM)加表鎖,並對teamUser表所有行的主鍵索引(即聚簇索引)加共享鎖。默認對其使用主鍵索引。
而語句(2)DELETE FROM teamUser WHERE teamId=$teamId AND titleWeight<32768 AND joinTime<'$daysago_1week'為刪除操作,會對選中行的主鍵索引加排他鎖。由於此語句還使用了非聚簇索引KEY `k_teamid_titleWeight_score` (`teamId`,`titleWeight`,`score`)的前綴索引,於是,還會對相關行的此非聚簇索引加排他鎖。
鎖沖突的產生
由於共享鎖與排他鎖是互斥的,當一方擁有了某行記錄的排他鎖后,另一方就不能其擁有共享鎖,同樣,一方擁有了其共享鎖后,另一方也無法得到其排他鎖。所 以,當語句(1)、(2)同時運行時,相當於兩個事務會同時申請某相同記錄行的鎖資源,於是會產生鎖沖突。由於兩個事務都會申請主鍵索引,鎖沖突只會發生 在主鍵索引上。
避免死鎖的方法
InnoDB給MySQL提供了具有提交,回滾和崩潰恢復能力的事務安全(ACID兼容)存儲引擎。InnoDB鎖定在行級並且也在SELECT語句提供非鎖定讀。這些特色增加了多用戶部署和性能。
但其行鎖的機制也帶來了產生死鎖的風險,這就需要在應用程序設計時避免死鎖的發生。以單個SQL語句組成的隱式事務來說,建議的避免死鎖的方法如下:
1.如果使用insert…select語句備份表格且數據量較大,在單獨的時間點操作,避免與其他sql語句爭奪資源,或使用select into outfile加上load data infile代替 insert…select,這樣不僅快,而且不會要求鎖定
2. 一個鎖定記錄集的事務,其操作結果集應盡量簡短,以免一次占用太多資源,與其他事務處理的記錄沖突。
3.更新或者刪除表格數據,sql語句的where條件都是主鍵或都是索引,避免兩種情況交叉,造成死鎖。對於where子句較復雜的情況,將其單獨通過sql得到后,再在更新語句中使用。
4. sql語句的嵌套表格不要太多,能拆分就拆分,避免占有資源同時等待資源,導致與其他事務沖突。
5. 對定點運行腳本的情況,避免在同一時間點運行多個對同一表進行讀寫的腳本,特別注意加鎖且操作數據量比較大的語句。
6.應用程序中增加對死鎖的判斷,如果事務意外結束,重新運行該事務,減少對功能的影響。
案例分析3
多表操作發生的死鎖
session1
session2
session2結果分析
案例分析4
當前具有兩個 session 登錄到 MySQL 服務器中, 簡稱 sessionA 與 sessionB。
sessionA 中執行下面操作。
sessionA> show global status like "table_locks%";
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 36 |
| Table_locks_waited | 0 |
+-----------------------+-------+
2 rows in set (0.00 sec)
解釋:查詢當前 MySQL 中表鎖定信息。
sessionA> lock table sbtest.new write;
Query OK, 0 rows affected (0.00 sec)
解釋:對測試表 sbtest.new 鎖定,該操作只會影響其他會話對 sbtest.new 表執行 DDL 及 DML 操作。
sessionA> show global status like "table_locks%";
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 37 |
| Table_locks_waited | 0 |
+-----------------------+-------+
2 rows in set (0.00 sec)
解釋:當執行 lock table 操作之后,系統會對 sbtest.new 表執行一次鎖定操作,當完成在表中數據庫頭部標記鎖定資源操作后,釋放鎖。
在當前 sessionA 執行鎖定操作狀態下,不影響 sessionA 對表 sbtest.new 進行增刪改操作,參考例子。
sessionA> insert into sbtest.new values (4),(5),(6);
Query OK, 3 rows affected (0.04 sec)
Records: 3 Duplicates: 0 Warnings: 0
sessionA> select * from sbtest.new;
+------+
| id |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
+------+
6 rows in set (0.00 sec)
sessionA> delete from sbtest.new where id > 3;
Query OK, 3 rows affected (0.06 sec)
完成上述操作后, 切換到 sessionB 會話中, 執行下面操作。
sessionB> select * from sbtest.new;
解釋:當 sessionB 進行表查詢時,由於 sessionA 執行鎖定操作,導致查詢等待,直到鎖定結束為止。
利用管理員創建 sessionC 登錄到 MySQL, 利用 show processlist 命令顯示當前登錄到 MySQL 終端的所有狀態狀態信息。
sessionC> show processlist;
+----+------+---------+------+---------------------------------+--------------------------+
| Id | db | Command | Time | State | Info |
+----+------+---------+------+---------------------------------+--------------------------+
| 1 | NULL | Sleep | 120 | | NULL |
| 2 | NULL | Query | 0 | NULL | show processlist |
| 3 | NULL | Query | 112 | Waiting for table metadata lock | select * from sbtest.new |
+----+------+---------+------+---------------------------------+--------------------------+
3 rows in set (0.00 sec)
要使用 show processlist 必須具有 PROCESS 權限,由於排版關系,返回信息進行部分折斷,從 State 狀態欄中可以清楚看到, ID 3 的會話當前正在處於查詢等待狀態, Waiting for table metadata lock 顯示當前等待狀態信息。
只要 sessionA 對表執行解鎖操作,sessionB 就能夠重新獲得資源,繼續之前 SQL 操作。
sessionA> unlock tables;
Query OK, 0 rows affected (0.00 sec)
sessionB> select * from sbtest.new;
+------+
| id |
+------+
| 1 |
| 2 |
| 3 |
+------+
3 rows in set (29 min 52.91 sec)
1 show processlist;
SHOW PROCESSLIST顯示哪些線程正在運行。您也可以使用mysqladmin processlist語句得到此信息。如果您有SUPER權限,您可以看到所有線程。否則,您只能看到您自己的線程(也就是,與您正在使用的MySQL賬戶相關的線程)。如果有線程在update或者insert 某個表,此時進程的status為updating 或者 sending data。
如果您得到“too many connections”錯誤信息,並且想要了解正在發生的情況,本語句是非常有用的。MySQL保留一個額外的連接,讓擁有SUPER權限的賬戶使用,以確保管理員能夠隨時連接和檢查系統(假設您沒有把此權限給予所有的用戶)。
Status |
含義 |
Checking table |
正在檢查數據表(這是自動的)。 |
Closing tables |
正在將表中修改的數據刷新到磁盤中,同時正在關閉已經用完的表。這是一個很快的操作,如果不是這樣的話,就應該確認磁盤空間是否已經滿了或者磁盤是否正處於重負中。 |
Connect Out |
復制從服務器正在連接主服務器。 |
Copying to tmp table on disk |
由於臨時結果集大於tmp_table_size,正在將臨時表從內存存儲轉為磁盤存儲以此節省內存。 |
Creating tmp table |
正在創建臨時表以存放部分查詢結果。 |
deleting from main table |
服務器正在執行多表刪除中的第一部分,剛刪除第一個表。 |
deleting from reference tables |
服務器正在執行多表刪除中的第二部分,正在刪除其他表的記錄。 |
Flushing tables |
正在執行FLUSH TABLES,等待其他線程關閉數據表。 |
Killed |
發送了一個kill請求給某線程,那么這個線程將會檢查kill標志位,同時會放棄下一個kill請求。MySQL會在每次的主循環中檢查kill標志位,不過有些情況下該線程可能會過一小段才能死掉。如果該線程程被其他線程鎖住了,那么kill請求會在鎖釋放時馬上生效。 |
Locked |
被其他查詢鎖住了。 |
Sending data |
正在處理SELECT查詢的記錄,同時正在把結果發送給客戶端。 |
Sorting for group |
正在為GROUP BY做排序。 |
Sorting for order |
正在為ORDER BY做排序。 |
Opening tables |
這個過程應該會很快,除非受到其他因素的干擾。例如,在執ALTER TABLE或LOCK TABLE語句行完以前,數據表無法被其他線程打開。正嘗試打開一個表。 |
Removing duplicates |
正在執行一個SELECT DISTINCT方式的查詢,但是MySQL無法在前一個階段優化掉那些重復的記錄。因此,MySQL需要再次去掉重復的記錄,然后再把結果發送給客戶端。 |
Reopen table |
獲得了對一個表的鎖,但是必須在表結構修改之后才能獲得這個鎖。已經釋放鎖,關閉數據表,正嘗試重新打開數據表。 |
Repair by sorting |
修復指令正在排序以創建索引。 |
Repair with keycache |
修復指令正在利用索引緩存一個一個地創建新索引。它會比Repair by sorting慢些。 |
Searching rows for update |
正在講符合條件的記錄找出來以備更新。它必須在UPDATE要修改相關的記錄之前就完成了。 |
Sleeping |
正在等待客戶端發送新請求。 |
System lock |
正在等待取得一個外部的系統鎖。如果當前沒有運行多個mysqld服務器同時請求同一個表,那么可以通過增加--skip-external-locking參數來禁止外部系統鎖。 |
Upgrading lock |
INSERT DELAYED正在嘗試取得一個鎖表以插入新記錄。 |
Updating |
正在搜索匹配的記錄,並且修改它們。 |
User Lock |
正在等待GET_LOCK()。 |
Waiting for tables |
該線程得到通知,數據表結構已經被修改了,需要重新打開數據表以取得新的結構。然后,為了能的重新打開數據表,必須等到所有其他線程關閉這個表。以下幾種情況下會產生這個通知:FLUSH TABLES tbl_name, ALTER TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE,或OPTIMIZE TABLE。 |
waiting for handler insert |
INSERT DELAYED已經處理完了所有待處理的插入操作,正在等待新的請求。 |
大部分狀態對應很快的操作,只要有一個線程保持同一個狀態好幾秒鍾,那么可能是有問題發生了,需要檢查一下。還有其他的狀態沒在上面中列出來,不過它們大部分只是在查看服務器是否有存在錯誤是才用得着。
2 show full processlist;
show processlist;只列出前100條,如果想全列出請使用show full processlist;