MySQL之鎖與事務處理分離水平(一)

Rock_R Newtol
html

爲何須要鎖

咱們在以前介紹的都是關於只有一個用戶操做數據庫的狀況,可是在實際的項目中,面臨的更多狀況是多用戶操做數據庫。例如電商平臺的秒殺系統等,可能會在短期具備多個用戶對數據庫進行操做,若是沒有進行特殊的處理,這是極其容易形成數據衝突的。咱們下面舉例說明一下發生衝突的一種狀況mysql

如圖,A用戶和C用戶共有一個餘額爲1000元的公用帳戶,這天,A和C同時到了不一樣銀行,A和C同時得到了帳戶的餘額信息,這時間A就決定往帳戶中存入1000元,C決定從帳戶中取出1000元。正常狀況下:帳戶餘額依舊爲1000元。可是若是不採起措施,那麼數據庫只會保存A用戶的2000元的餘額信息。這是爲啥呢?當A和C拿到餘額信息時,帳戶餘額爲1000元。假設C用戶取款須要1min,A用戶存款須要2min.那麼當C用戶取款成功後,帳戶餘額被更改成0。可是此時的用戶A並不知道。她仍然是按照帳戶還有1000元的基礎上進行操做,那麼當他反饋給帳戶的時候,1min前餘額才被更改成0的帳戶就會被更改成2000。因此爲了不這樣的狀況發生,就必須使用鎖定。鎖定是爲了當某個用戶在進行操做而拒絕其餘用戶操做的一種機制,解除鎖定時被稱爲解鎖。 加鎖之後的流程大體爲:sql

鎖的種類

按照使用的目的能夠分爲:數據庫

  • 共享鎖(Shared Lock,也叫S鎖): 共享鎖是當用戶參照數據時,將數據對象變爲只讀形式的鎖定。例如在上面的流程中,A用戶第一次獲取帳戶餘額信息的行爲並不會被拒絕,可是,A用戶在C用戶結束操做以前,依然是沒法對數據進行修改的。也被稱爲:讀取鎖定性能優化

  • 排他鎖 (Exclusive Lock,也叫X鎖): 排他鎖是使用【INSERT】,【UPDATE】,【DELETE】命令對數據進行修改時,使用的機制。例如在上面的流程中,使用的就是排他鎖,即A用戶的第一次讀取操做也會被拒絕。只有當C用戶完成全部操做之後,A用戶才能進行操做。也被稱爲寫入鎖定或者獨佔鎖定bash

注意事項

爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。session

  • 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。架構

  • 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。併發

鎖的粒度

  • 表鎖(數據表): 開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突機率高,併發度最低,通常是作ddl處理時使用。高併發

  • 行鎖(記錄): 開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖衝突的機率低,併發度高,MySQL通常都是用行鎖來處理併發事務

  • 頁鎖(數據庫): 開銷和加鎖速度介於表鎖和行鎖之間;會出現死鎖;鎖定粒度介於表鎖和行鎖之間,併發度通常

鎖定的粒度會影響程序的併發數。通常狀況下,鎖定的粒度越小,併發性纔會更高。例如:在使用了行鎖的狀況下,還能夠對同一數據表的不一樣行進行數據的處理,而若是使用了表鎖定,其餘進程只能等到前一個進程完成了事務處理後才能進行操做。那麼是否是鎖定的粒度越小越好呢?其實否則,由於鎖定會極大的消耗着數據的資源,也就是說,鎖定的數目越多,消耗的服務的資源也就越多。

注意事項

  • 若是數據庫中行單位粒度的鎖定大量發生的狀況時,數據庫有將這些鎖定的粒度自動向上提高的機制,被稱爲鎖定提高(Lock Escalation)

MySQL引擎對應支持的鎖

根據不一樣的存儲引擎,MySQL中鎖的特性能夠大體概括以下:

存儲引擎 行鎖 表鎖 頁鎖
MyISAM

BDB
InnoDB

注意事項

目前主要有兩種鎖定協議:

  • 一段鎖協議:爲了預防在高併發的環境中發生死鎖的狀況,事先就將須要使用到數據所有進行鎖定,等全部操做結束後再進行解鎖。

  • 兩段鎖協議: 將事務分紅兩個階段,加鎖階段和解鎖階段。

    • 加鎖階段:在對任何數據進行讀操做以前要申請並得到共享鎖(其它事務能夠繼續加共享鎖,但不能加排它鎖),在進行寫操做以前要申請並得到排它鎖(其它事務不能再得到任何鎖)。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。

    • 解鎖階段:當事務釋放了一個封鎖之後,事務進入解鎖階段,在該階段只能進行解鎖操做不能再進行加鎖操做。

InnoDB採用的是兩階段鎖定協議,由於在事務開始階段,數據庫並不知道會用到哪些數據。在事務執行過程當中,隨時均可以執行鎖定,鎖只有在執行 COMMIT或者ROLLBACK的時候纔會釋放,而且全部的鎖是在同一時刻被釋放。前面描述的鎖定都是隱式鎖定,InnoDB會根據事務隔離級別在須要的時候自動加鎖。

InnoDB引擎中鎖的使用

表鎖

使用情形

  • 須要更新或插入大量數據且表結構又比較複雜。在這種狀況是使用行鎖很容易形成鎖衝突和長時間的等待。

  • 事務涉及多表的操做,若是在這種狀況使用行鎖,很容易引發死鎖,形成大量的事務回滾。

注意事項

  • 若是屢次涉及到上述兩種事務情形,可根據實際狀況考慮是否使用MyISAM引擎。

  • 在用【 LOCK TABLES】對【InnoDB】表加鎖時要注意,要將【AUTOCOMMIT】設爲0,不然MySQL不會給表加鎖;事務結束前,不要用【UNLOCK TABLES】釋放表鎖,由於【UNLOCK TABLES】會隱含地提交事務;【COMMIT】或【ROLLBACK】並不能釋放用【LOCK TABLES】加的表級鎖,必須用【UNLOCK TABLES】釋放表鎖。

  • 使用【LOCK TABLES】雖然能夠給InnoDB加表級鎖,但必須說明的是,表鎖不是由InnoDB存儲引擎層管理的,而是由其上一層──MySQL Server負責的,僅當【autocommit】爲0、【InnoDB

    table
    locks】=1(默認設置)時,InnoDB層才能知道MySQL加的表鎖,MySQL Server也才能感知InnoDB加的行鎖,這種狀況下,InnoDB才能自動識別涉及表級鎖的死鎖,不然,InnoDB將沒法自動檢測並處理這種死鎖。

行鎖

注意事項

  • 在InnoDB引擎中行鎖是經過給索引上的索引項加鎖來實現的,也就意味着只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖

  • 行鎖是針對索引加的鎖,不是針對記錄加的鎖,因此雖然是訪問不一樣行的記錄,可是若是是使用相同的索引鍵,是會出現鎖衝突的。

  • 當表有多個索引的時候,不一樣的事務可使用不一樣的索引鎖定不一樣的行,另外,不管是使用主鍵索引、惟一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。

  • 即使在條件中使用了索引字段,可是否使用索引來檢索數據是由MySQL經過判斷不一樣執行計劃的代價來決定的,若是MySQL認爲全表掃描效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下InnoDB將使用表鎖,而不是行鎖。所以,在分析鎖衝突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。

樂觀鎖,悲觀鎖,死鎖

悲觀鎖(Pessimistic Lock):假定會發生併發衝突,屏蔽一切可能違反數據完整性的操做

它指的是對數據被外界(包括本系統當前的其餘事務,以及來自外部系統的事務處理)修改持保守態度,所以,在整個數據處理過程當中,將數據處於鎖定狀態。簡要說就是:每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會進入等待狀態。例如:Java中的【synchronized】 就屬於悲觀鎖的一種實現,每次線程要修改數據時都先得到鎖,保證同一時刻只有一個線程能操做數據。悲觀鎖的實現,每每依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,不然,即便在本系統中實現了加鎖機制,也沒法保證外部系統不會修改數據)。

樂觀鎖(Optimistic Lock):假設不會發生併發衝突,只在提交操做時檢查是否違反數據完整性

它指的是對數據被外界(包括本系統當前的其餘事務,以及來自外部系統的事務處理)修改持開放態度。每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個數據。悲觀鎖大多數狀況下依靠數據庫的鎖機制實現,以保證操做最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷每每沒法承受。而樂觀鎖機制在必定程度上解決了這個問題,樂觀鎖適用於讀多寫少的應用場景,這樣大大提升吞吐量。樂觀鎖主要的實現方式有:

  • 使用數據版本(Version)記錄機制實現,這是樂觀鎖最經常使用的一種實現方式。何謂數據版本?即爲數據增長一個版本標識,通常是經過爲數據庫表增長一個數字類型的 「version」 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值+1。當咱們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,若是數據庫表當前版本號與第一次取出來的version值相等,則予以更新,不然認爲是過時數據。

  • 使用時間戳(timestamp)。樂觀鎖定的第二種實現方式和第一種差很少,一樣是在須要樂觀鎖控制的table中增長一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version相似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和本身更新前取到的時間戳進行對比,若是一致則OK,不然就是版本衝突。

死鎖(Deadlock):兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的互相等待

在多任務系統下,當一個或多個進程等待系統資源,而資源又被進程自己或其餘進程佔用時,就造成了死鎖。在數據庫中,由於MyISAM老是一次性得到所需的所有鎖,所以不會出現死鎖。因此死鎖主要發生於InnoDB引擎中。可是,發生死鎖後,InnoDB通常都能自動檢測到,並使一個事務釋放鎖並回退,另外一個事務得到鎖,繼續完成事務。但在涉及外部鎖,或涉及表鎖的狀況下,InnoDB並不能徹底自動檢測到死鎖,這須要經過設置鎖等待超時參數【innodb

lock
wait_timeout】 來解決。這個參數並非只用來解決死鎖問題,在併發訪問比較高的狀況下,若是大量事務因沒法當即得到所需的鎖而掛起,會佔用大量計算機資源,形成嚴重性能問題,甚至拖跨數據庫。咱們經過設置合適的鎖等待超時閾值,能夠避免這種狀況發生。

  • 在應用中,若是不一樣的程序會併發存取多個表,應儘可能約定以相同的順序爲訪問表,這樣能夠大大下降產生死鎖的機會。若是兩個session訪問兩個表的順序不一樣,發生死鎖的機會就很是高!但若是以相同的順序來訪問,死鎖就可能避免。

  • 在程序以批量方式處理數據的時候,若是事先對數據排序,保證每一個線程按固定的順序來處理記錄,也能夠大大下降死鎖的可能。 例如:

Session1:mysql> select * from test where id in (8,9) for update;+----+--------+------+| id | course | name | +----+--------+------+|  8 | XXX     | xxx   | |  9 | FFF    | fff   | +----+--------+------+2 rows in set (0.04 sec)Session2:select * from test where id in (10,8,5) for update;//鎖等待中……//其實這個時候id=10這條記錄沒有被鎖住的,但id=5的記錄已經被鎖住了,鎖的等待在id=8的這裏。Session3:mysql> select * from test where id=5 for update;//鎖等待中Session4:mysql> select * from test where id=10 for update;+----+--------+------+| id | course | name | +----+--------+------+| 10 | KKK    | kkk   | +----+--------+------+1 row in set (0.00 sec)在其它session中id=5是加不了鎖的,可是id=10是能夠加上鎖的。複製代碼
  • 在開發中,使用【insert into test(xx,xx) on duplicate key update xx='XX';】來解決:根據字段值查詢(有索引),若是不存在,則插入;不然更新的需求。不然很容易出現死鎖,例如:

以id爲主鍵爲例,目前尚未id=22的行Session1:select * from t3 where id=22 for update;Empty set (0.00 sec)session2:select * from t3 where id=23  for update;Empty set (0.00 sec)Session1:insert into t3 values(22,'ac','a',now());鎖等待中……Session2:insert into t3 values(23,'bc','b',now());ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction複製代碼

到這裏,咱們就講解完了關於鎖與事務處理分離水平的第一部分的內容,後面咱們將介紹關於事務處理分離水平。

參考文獻

美團點評技術團隊(ameng ·2014-08-20 15:50).Innodb中的事務隔離級別和鎖的關係 博文地址:https://tech.meituan.com/innodb_lock.html

《MySQL性能優化與架構設計》

文章開發於公衆號【Newtol】,關注做者我的技術公衆號,有更多視頻學習資源分享

相關文章
相關標籤/搜索