圖-MySQL鎖分類html
老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖,悲觀鎖是由數據庫內部實現,下文所說的共享鎖和排它鎖都是悲觀鎖的範疇。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。java
老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。mysql
從上面對兩種鎖的介紹,咱們知道兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下(多讀場景),即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是是多寫的狀況,通常會常常產生衝突,這就會致使上層應用會不斷的進行retry,這樣反卻是下降了性能,因此通常多寫的場景下用悲觀鎖就比較合適。程序員
樂觀鎖通常會使用版本號機制或CAS算法實現。面試
通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。
舉一個簡單的例子:假設數據庫中賬戶信息表中有一個version字段,當前值爲1;而當前賬戶餘額字段(balance)爲$100。操做員A此時將其讀出(version=1),並從其賬戶餘額中扣除$50($100-$50)。在操做員A操做的過程當中,操做員B也讀入此用戶信息(version=1),並從其賬戶餘額中扣除$20($100-$20)。操做員A完成了修改工做,將數據版本號加一(version=2),連同賬戶扣除後餘額(balance=$50),提交至數據庫更新,此時因爲提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄version更新爲2。操做員B完成了操做,也將版本號加一(version=2)試圖向數據庫提交數據(balance=$80),但此時比對數據庫記錄版本時發現,操做員B提交的數據版本號爲2,數據庫記錄當前版本也爲2,不知足「提交版本必須大於記錄當前版本才能執行更新「的樂觀鎖策略,所以,操做員B的提交被駁回。這樣,就避免了操做員B用基於version=1的舊數據修改的結果覆蓋操做員A的操做結果的可能。算法
即compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的狀況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的狀況下實現變量的同步,因此也叫非阻塞同步(Non-blockingSynchronization)。CAS算法涉及到三個操做數sql
當且僅當V的值等於A時,CAS經過原子方式用新值B來更新V的值,不然不會執行任何操做(比較和替換是一個原子操做)。通常狀況下是一個自旋操做,即不斷的重試。數據庫
若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它仍然是A值,那咱們就能說明它的值沒有被其餘線程修改過了嗎?很明顯是不能的,由於在這段時間它的值可能被改成其餘值,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。這個問題被稱爲CAS操做的"ABA"問題。
JDK1.5之後的AtomicStampedReference類就提供了此種能力,其中的compareAndSet方法就是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。編程
自旋CAS(也就是不成功就一直循環執行直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。segmentfault
CAS只對單個共享變量有效,當操做涉及跨多個共享變量時CAS無效。可是從JDK1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。因此咱們可使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操做。
MyISAM和InnoDB存儲引擎使用的鎖:
MyISAM採用表級鎖(table-level locking)。
InnoDB支持行級鎖(row-levellocking)和表級鎖,默認爲行級鎖,可能出現行鎖升級爲表鎖的狀況。
表級鎖和行級鎖對比:
表級鎖:Mysql中鎖定粒度最大的一種鎖,對當前操做的整張表加鎖,實現簡單,資源消耗也比較少,加鎖快,因爲表級鎖一次會將整個表鎖定,因此能夠很好地避免困擾咱們的死鎖問題。其鎖定粒度最大,觸發鎖衝突的機率最高,併發度最低,MyISAM和InnoDB引擎都支持表級鎖。
行級鎖:Mysql中鎖定粒度最小的一種鎖,只針對當前操做的行進行加鎖。行級鎖能大大減小數據庫操做的衝突。其加鎖粒度最小,併發度高,但加鎖的開銷也最大,加鎖慢,會出現死鎖。
第一種狀況:全表更新。事務須要更新大部分或所有數據,且表又比較大。若使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突。
第二種狀況:多表查詢。事務涉及多個表,比較複雜的關聯查詢,極可能引發死鎖,形成大量事務回滾。這種狀況若能一次性鎖定事務涉及的表,從而能夠避免死鎖、減小數據庫因事務回滾帶來的開銷。
where條件若是不存在索引字段,那麼這個事務是否會致使表鎖?有人回答:只有主鍵和惟一索引纔是行鎖,普通索引是表鎖。結果發現普通索引並不必定會引起表鎖,在普通索引中是否引起表鎖取決於普通索引的高效程度,即行鎖升級爲表鎖的緣由是有SQL語句中未使用到索引,或者說使用的索引未被數據庫承認(至關於沒有使用索引)。
屬性值重複率
當「值重複率」低時,甚至接近主鍵或者惟一索引的效果,「普通索引」依然是行鎖;當「值重複率」高時,MySQL不會把這個「普通索引」當作索引,即形成了一個沒有索引的SQL,此時引起表鎖。
同JVM自動優化java代碼同樣,MySQL也具備自動優化SQL的功能。低效的索引將被忽略,這也就倒逼開發者使用正確且高效的索引。
InnoDB只有在經過索引條件檢索數據時使用行級鎖,不然使用表鎖,行鎖是針對索引加的而不是針對記錄加的鎖。
從上面的案例看出,行鎖變表鎖彷佛是一個坑,可MySQL沒有這麼無聊給你挖坑。這是由於MySQL有本身的執行計劃。當你須要更新一張較大表的大部分甚至全表的數據時。而你又傻乎乎地用索引做爲檢索條件。一不當心開啓了行鎖(沒毛病啊!保證數據的一致性!)。可MySQL卻認爲大量對一張表使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突問題,性能嚴重降低。因此MySQL會將行鎖升級爲表鎖,即實際上並無使用索引。咱們仔細想一想也能理解,既然整張表的大部分數據都要更新數據,在一行一行地加鎖效率則更低。其實咱們能夠經過explain命令查看MySQL的執行計劃,你會發現key爲null。代表MySQL實際上並無使用索引,行鎖升級爲表鎖。
共享鎖又稱讀鎖 (read lock),是讀取操做建立的鎖。若是事務T對數據A加上共享鎖後,則其餘事務只能對A再加共享鎖,不能加排他鎖。得到共享鎖的事務只能讀數據,不能修改數據。當若是事務對讀鎖進行修改操做,極可能會形成死鎖。在查詢語句後面增長LOCK IN SHARE MODE,MySQL會對查詢結果中的每行都加共享鎖,當沒有其它線程對查詢結果集中的任何一行使用排他鎖時,能夠成功申請共享鎖不然會被阻塞。 其餘線程也能夠讀取使用共享鎖的表,並且這些線程讀取的是同一個版本的數據。
若某個事物對某一行加上了排他鎖,只能這個事務對其進行讀寫,在此事務結束以前,其餘事務不能對其進行加任何鎖(排它鎖會阻塞全部的排它鎖和共享鎖),不能進行任何讀寫操做,需等待其釋放,排它鎖是悲觀鎖的一種實現。
讀取爲何要加讀鎖呢?防止數據在被讀取的時候被別的線程加上寫鎖。排他鎖使用方式:在須要執行的語句後面加上for update就能夠了 select status from TABLE where id=1 for update;(能夠參考使用select for share,for update的場景及死鎖陷阱)
當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖。
舉例來講,假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101,下面的SQL:
Select * from emp where empid>100 for update;
複製代碼是一個範圍條件的檢索,InnoDB不只會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的「間隙」加鎖。
InnoDB使用間隙鎖的目的,一方面是爲了防止幻讀,以知足相關隔離級別的要求,對於上面的例子,要是不使用間隙鎖,若是其餘事務插入了empid大於100的任何記錄,那麼本事務若是再次執行上述語句,就會發生幻讀;另一方面,是爲了知足其恢復和複製的須要。
很顯然,在使用範圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍內鍵值的併發插入,這每每會形成嚴重的鎖等待。所以,在實際應用開發中,尤爲是併發插入比較多的應用,咱們要儘可能優化業務邏輯,儘可能使用相等條件來訪問更新數據,避免使用範圍條件。若執行的條件是範圍過大,則InnoDB會將整個範圍內全部的索引鍵值所有鎖定,很容易對性能形成影響。
還要特別說明的是,InnoDB除了經過範圍條件加鎖時使用間隙鎖外,若是使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖。
意向鎖是爲了支持多種粒度鎖同時存在。申請意向鎖的動做是數據庫完成的,即事務A申請一行的行鎖的時候,數據庫會自動先開始申請表的意向鎖,不須要咱們程序員使用代碼來申請。當再向一個表添加表級X鎖的時候若是沒有意向鎖的話,則須要遍歷全部整個表判斷是否有行鎖的存在,以避免發生衝突。若是有了意向鎖,只須要判斷該意向鎖與即將添加的表級鎖是否兼容便可。由於意向鎖的存在表明了有行級鎖的存在或者即將有行級鎖的存在,於是無需遍歷整個表便可獲取結果。(具體能夠參考知乎-InnoDB 的意向鎖有什麼做用?)
Next-Key Locks(簡稱NK鎖)是記錄鎖和間隙鎖的組合,鎖定一個範圍,包含記錄自己,主要是爲了解決幻讀問題。
記錄鎖針對索引記錄,老是鎖定索引記錄,即便表沒有索引InnoDB會建立隱式的索引並使用這個索引實施記錄鎖,能夠參考Clustered and Secondary Indexes
幻讀(Phantom read): 幻讀與不可重複讀相似。它發生在一個事務(T1)讀取了幾行數據,接着另外一個併發事務(T2)插入了一些數據時。在隨後的查詢中,第一個事務(T1)就會發現多了一些本來不存在的記錄,就好像發生了幻覺同樣,因此稱爲幻讀。
InnoDB默認的RR事務隔離級別下,不顯式加『lock in share mode』與『for update』的『select』操做都屬於快照讀,保證事務執行過程當中只有第一次讀以前提交的修改和本身的修改可見,其餘的均不可見,即不加鎖讀,讀取記錄的快照版本而非最新版本,經過MVCC實現。MVCC即多版本併發控制,與之對應的是基於鎖的併發控制。
圖-MVCC(來自高性能MySQL)
#須要關閉自動提交 SET AUTOCOMMIT=0; #session依次執行 select * from table1 where id = 171 for update; select * from table1 where id = 172 for update; #session依次執行 select * from table1 where id = 172 for update; select * from table1 where id = 171 for update; #改成一次性加鎖可避免死鎖,在in裏面的列表值mysql是會自動從小到大排序,加鎖也是一條條從小到大加的鎖 select * from table1 where id in (xx,xx,xx) for update
SET AUTOCOMMIT=0; #session1, #當對存在的行進行鎖的時候(主鍵),mysql就只有行鎖。 #當對未存在的行進行鎖的時候(即便條件爲主鍵),mysql是會鎖住一段範圍(有gap鎖) select * from km_personal_articles where id = 181 for update; #session2 select * from km_personal_articles where id = 182 for update; #session1 INSERT INTO `km_personal_articles` (`id`, `empid`, `name`, `url`, `pv`, `created_at`) VALUES (181, 'G6411', '測試', 'http', 277, '2018-11-19 16:06'); #session2 #插入意向鎖和間隙鎖衝突,因此兩個事務互相等待,最後造成死鎖 INSERT INTO `km_personal_articles` (`id`, `empid`, `name`, `url`, `pv`, `created_at`) VALUES (182, 'G6411', '測試', 'http', 277, '2018-11-19 16:06'); #用mysql特有的語法來解決此問題。由於insert語句對於主鍵來講,插入的行無論有沒有存在,都會只有行鎖。 insert into t3(xx,xx) on duplicate key update `xx`='XX';
CREATE TABLE `students` ( `id` int(11) NOT NULL AUTO_INCREMENT, `no` varchar(24) DEFAULT NULL, `name` varchar(512) DEFAULT NULL, `score` int(11) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `no` (`no`), KEY `name` (`name`), KEY `age` (`age`) ) ENGINE=InnoDB AUTO_INCREMENT=181 DEFAULT CHARSET=utf8mb4 COMMENT='死鎖分析demo'; id no name score age 15 S001 aaa 100 25 18 S002 bbb 100 24 20 S003 ccc 100 24 30 S004 ddd 4 23 37 S005 eee 5 22 49 S006 fff 6 25 50 S007 ggg 7 23 SET AUTOCOMMIT=0; #session1 update students set score = 100 where id < 30; #session2 #在範圍查詢時,加鎖是一條記錄一條記錄挨個加鎖的,若是兩條SQL語句的加鎖順序不同,也會致使死鎖。 #事務A的範圍條件爲id<30,加鎖順序爲:id=15->18->20,事務B走的是二級索引age,除了在二級索引加鎖外,還會在聚簇索引上加鎖。加鎖順序爲:(age,id)=(24,18)->(24,20)->(25,15)->(25,49),其中,對id的加鎖順序爲id=18->20->15->49。能夠看到事務A先鎖15,再鎖18,而事務B先鎖18,再鎖15,從而造成死鎖 update students set score = 100 where age > 23;
產生死鎖的四個必要條件:
互斥條件:一個資源每次只能被一個進程使用。
請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。
不剝奪條件: 進程已得到的資源,在末使用完以前,不能強行剝奪。
循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。
雖然不能徹底避免死鎖,但可使死鎖的數量減至最少。將死鎖減至最少能夠增長事務的吞吐量並減小系統開銷,由於只有不多的事務回滾,而回滾會取消事務執行的全部工做,因爲死鎖回滾的操做由應用程序從新提交。
直觀方法是在兩個事務相互等待時,當一個等待時間超過設置的某一閥值時,對其中一個事務進行回滾,另外一個事務就能繼續執行。這種方法簡單有效,在innodb中,參數innodb_lock_wait_timeout(事務鎖超時時間)用來設置超時時間。
僅用上述方法來檢測死鎖太過被動,innodb還提供了wait-for graph算法來主動進行死鎖檢測,每當加鎖請求沒法當即知足須要並進入等待時,wait-for graph算法都會被觸發。
InnoDB將各個事務看爲一個個節點,資源就是各個事務佔用的鎖,當事務1須要等待事務2的鎖時,就生成一條有向邊從1指向2,最後行成一個有向圖。咱們只要檢測這個有向圖是否出現環路便可,出現環路就是死鎖。這就是wait-for graph算法。
圖-wait-for graph算法
經過檢查innodb_row_lock狀態變量分析系統上中行鎖的爭奪狀況
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 0 |
| Innodb_row_lock_time_avg | 0 |
| Innodb_row_lock_time_max | 0 |
| Innodb_row_lock_waits | 0 |
+-------------------------------+-------+
innodb_row_lock_current_waits: 當前正在等待鎖定的數量
innodb_row_lock_time: 從系統啓動到如今鎖定總時間長度;很是重要的參數,
innodb_row_lock_time_avg: 每次等待所花平均時間;很是重要的參數,
innodb_row_lock_time_max: 從系統啓動到如今等待最常的一次所花的時間;
innodb_row_lock_waits: 系統啓動後到如今總共等待的次數;很是重要的參數。直接決定優化的方向和策略。
行鎖優化
- 儘量讓全部數據檢索都經過索引來完成,避免無索引行或索引失效致使行鎖升級爲表鎖。
- 儘量避免間隙鎖帶來的性能降低,減小或使用合理的檢索範圍。
- 儘量減小事務的粒度,好比控制事務大小,而從減小鎖定資源量和時間長度,從而減小鎖的競爭等,提升性能。
- 儘量低級別事務隔離,隔離級別越高,併發的處理能力越低。
查看加鎖狀況
show open tables; 1表示加鎖,0表示未加鎖。
mysql> show open tables where in_use > 0;
+----------+-------------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+-------------+--------+-------------+
| lock | myisam_lock | 1 | 0 |
+----------+-------------+--------+-------------+
分析表鎖定
能夠經過檢查table_locks_waited和table_locks_immediate狀態變量分析系統上的表鎖定
mysql> show status like 'table_locks%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Table_locks_immediate | 104 |
| Table_locks_waited | 0 |
+----------------------------+-------+
table_locks_immediate: 表示當即釋放表鎖數。
table_locks_waited: 表示須要等待的表鎖數。此值越高則說明存在着越嚴重的表級鎖爭用狀況。
此外,MyISAM的讀寫鎖調度是寫優先,這也是MyISAM不適合作寫爲主表的存儲引擎。由於寫鎖後,其餘線程不能作任何操做,大量的更新會使查詢很可貴到鎖,從而形成永久阻塞。
1)經過應用業務日誌定位到問題代碼,找到相應的事務對應的sql;
由於死鎖被檢測到後會回滾,這些信息都會以異常反應在應用的業務日誌中,經過這些日誌咱們能夠定位到相應的代碼,並把事務的sql給梳理出來。
此外,咱們根據日誌回滾的信息發如今檢測出死鎖時這個事務被回滾。
2)肯定數據庫隔離級別。
執行select @@global.tx_isolation,能夠肯定數據庫的隔離級別,咱們數據庫的隔離級別是RC,這樣能夠很大機率排除gap鎖形成死鎖的嫌疑;
3)找DBA執行下show engine innodb status \G; 看看最近詳細的死鎖日誌,在打印出來的信息中找到「LATEST DETECTED DEADLOCK」一節內容。
#查詢是否鎖表 show OPEN TABLES where In_use > 0; #查詢進程(若是您有SUPER權限,您能夠看到全部線程。不然,您只能看到您本身的線程) show processlist #殺死進程id(就是上面命令的id列) kill id #查看當前的事務 SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; #查看當前鎖定的事務 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; #查看當前等鎖的事務 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; #殺死進程 kill 進程ID
參考資料
segmentfault-MySQL InnoDB鎖機制全面解析分享
segmentfault-MySQL InnoDB 鎖—官方文檔