資料庫的鎖機制,今天徹底理清楚。文章中有參考整理其他一些有價值的部落格以及mysql官方文件的內容,如有侵權請聯絡刪除。

資料庫鎖

先看一張圖自己整理的資料庫鎖的樹形圖

概要

資料庫鎖一般可以分為兩類,一個是悲觀鎖,一個是樂觀鎖。

  • 樂觀鎖一般是指使用者自己實現的一種鎖機制,假設認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。樂觀鎖的實現方式一般包括使用版本號和時間戳。

  • 悲觀鎖一般就是我們通常說的資料庫鎖機制,以下討論都是基於悲觀鎖。

    悲觀鎖主要表鎖、行鎖、頁鎖。在MyISAM中只用到表鎖,不會有死鎖的問題,鎖的開銷也很小,但是相應的併發能力很差。innodb實現了行級鎖和表鎖,鎖的粒度變小了,併發能力變強,但是相應的鎖的開銷變大,很有可能出現死鎖。同時inodb需要協調這兩種鎖,演算法也變得複雜。InnoDB行鎖是通過給索引上的索引項加鎖來實現的,只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖。

    表鎖和行鎖都分為共享鎖和排他鎖(獨佔鎖),而更新鎖是為了解決行鎖升級(共享鎖升級為獨佔鎖)的死鎖問題。

    innodb中表鎖和行鎖一起用,所以為了提高效率才會有意向鎖(意向共享鎖和意向排他鎖)。

為了表鎖和行鎖而存在的意向鎖

官方文件中是這麼描述的,

Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table

  1. 在mysql中有表鎖,讀鎖鎖表,會阻塞其他事務修改表資料。寫鎖鎖表,會阻塞其他事務讀和寫。
  2. Innodb引擎又支援行鎖,行鎖分為共享鎖,一個事務對一行的共享只讀鎖。排它鎖,一個事務對一行的排他讀寫鎖。
  3. 這兩中型別的鎖共存的問題考慮這個例子:事務A鎖住了表中的一行,讓這一行只能讀,不能寫。之後,事務B申請整個表的寫鎖。如果事務B申請成功,那麼理論上它就能修改表中的任意一行,這與A持有的行鎖是衝突的。資料庫需要避免這種衝突,就是說要讓B的申請被阻塞,直到A釋放了行鎖。

資料庫要怎麼判斷這個衝突呢?

  • step1:判斷表是否已被其他事務用表鎖鎖表
  • step2:判斷表中的每一行是否已被行鎖鎖住。

注意step2,這樣的判斷方法效率實在不高,因為需要遍歷整個表。於是就有了意向鎖。在意向鎖存在的情況下,事務A必須先申請表的意向共享鎖,成功後再申請一行的行鎖。

在意向鎖存在的情況下,上面的判斷可以改成

  • step1:不變
  • step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,因此,事務B申請表的寫鎖會被阻塞。

注意:申請意向鎖的動作是資料庫完成的,就是說,事務A申請一行的行鎖的時候,資料庫會自動先開始申請表的意向鎖,不需要我們程式設計師使用程式碼來申請。

行鎖的細分

  • 共享鎖

    1. 加鎖與解鎖:當一個事務執行select語句時,資料庫系統會為這個事務分配一把共享鎖,來鎖定被查詢的資料。在預設情況下,資料被讀取後,資料庫系統立即解除共享鎖。例如,當一個事務執行查詢“SELECT * FROM accounts”語句時,資料庫系統首先鎖定第一行,讀取之後,解除對第一行的鎖定,然後鎖定第二行。這樣,在一個事務讀操作過程中,允許其他事務同時更新accounts表中未鎖定的行。

    2. 相容性:如果資料資源上放置了共享鎖,還能再放置共享鎖和更新鎖。

    3. 併發效能:具有良好的併發效能,當資料被放置共享鎖後,還可以再放置共享鎖或更新鎖。所以併發效能很好。

  • 排他鎖

    1. 加鎖與解鎖:當一個事務執行insert、update或delete語句時,資料庫系統會自動對SQL語句操縱的資料資源使用獨佔鎖。如果該資料資源已經有其他鎖(任何鎖)存在時,就無法對其再放置獨佔鎖了。

    2. 相容性:獨佔鎖不能和其他鎖相容,如果資料資源上已經加了獨佔鎖,就不能再放置其他的鎖了。同樣,如果資料資源上已經放置了其他鎖,那麼也就不能再放置獨佔鎖了。

    3. 併發效能:最差。只允許一個事務訪問鎖定的資料,如果其他事務也需要訪問該資料,就必須等待。

  • 更新鎖

    更新鎖在的初始化階段用來鎖定可能要被修改的資源,這可以避免使用共享鎖造成的死鎖現象。例如,對於以下的update語句:

    UPDATE accounts SET balance=900 WHERE id=1

    更新操作需要分兩步:讀取accounts表中id為1的記錄 –> 執行更新操作。

    如果在第一步使用共享鎖,再第二步把鎖升級為獨佔鎖,就可能出現死鎖現象。例如:兩個事務都獲取了同一資料資源的共享鎖,然後都要把鎖升級為獨佔鎖,但需要等待另一個事務解除共享鎖才能升級為獨佔鎖,這就造成了死鎖。

    更新鎖有如下特徵:

    1. 加鎖與解鎖:當一個事務執行update語句時,資料庫系統會先為事務分配一把更新鎖。當讀取資料完畢,執行更新操作時,會把更新鎖升級為獨佔鎖。

    2. 相容性:更新鎖與共享鎖是相容的,也就是說,一個資源可以同時放置更新鎖和共享鎖,但是最多放置一把更新鎖。這樣,當多個事務更新相同的資料時,只有一個事務能獲得更新鎖,然後再把更新鎖升級為獨佔鎖,其他事務必須等到前一個事務結束後,才能獲取得更新鎖,這就避免了死鎖。

    3. 併發效能:允許多個事務同時讀鎖定的資源,但不允許其他事務修改它。

資料庫隔離級別

瞭解了資料的鎖機制,資料庫的隔離級別也就好理解多了。每一種隔離級別滿足不同的資料要求,使用不同程度的鎖。

Read Uncommitted,讀寫均不使用鎖,資料的一致性最差,也會出現許多邏輯錯誤。

Read Committed,使用寫鎖,但是讀會出現不一致,不可重複讀。

Repeatable Read, 使用讀鎖和寫鎖,解決不可重複讀的問題,但會有幻讀。

Serializable, 使用事務串形化排程,避免出現因為插入資料沒法加鎖導致的不一致的情況。

  • 讀不提交,造成髒讀(Read Uncommitted)

    一個事務中的讀操作可能讀到另一個事務中未提交修改的資料,如果事務發生回滾就可能造成錯誤。

    例子:A打100塊給B,B看賬戶,這是兩個操作,針對同一個資料庫,兩個事物,如果B讀到了A事務中的100塊,認為錢打過來了,但是A的事務最後回滾了,造成損失。

    避免這些事情的發生就需要我們在寫操作的時候加鎖,使讀寫分離,保證讀資料的時候,資料不被修改,寫資料的時候,資料不被讀取。從而保證寫的同時不能被另個事務寫和讀。

  • 讀提交(Read Committed)

    我們加了寫鎖,就可以保證不出現髒讀,也就是保證讀的都是提交之後的資料,但是會造成不可重讀,即讀的時候不加鎖,一個讀的事務過程中,如果讀取資料兩次,在兩次之間有寫事務修改了資料,將會導致兩次讀取的結果不一致,從而導致邏輯錯誤。

  • 可重讀(Repeatable Read)

    解決不可重複讀問題,一個事務中如果有多次讀取操作,讀取結果需要一致(指的是固定一條資料的一致,幻讀指的是查詢出的數量不一致)。 這就牽涉到事務中是否加讀鎖,並且讀操作加鎖後是否在事務commit之前持有鎖的問題,如果不加讀鎖,必然出現不可重複讀,如果加鎖讀完立即釋放,不持有,那麼就可能在其他事務中被修改,若其他事務已經執行完成,此時該事務中再次讀取就會出現不可重複讀,

    所以讀鎖在事務中持有可以保證不出現不可重複讀,寫的時候必須加鎖且持有,這是必須的了,不然就會出現髒讀。Repeatable Read(可重讀)也是MySql的預設事務隔離級別,上面的意思是讀的時候需要加鎖並且保持

  • 可序列化(Serializable)

    解決幻讀問題,在同一個事務中,同一個查詢多次返回的結果不一致。事務A新增了一條記錄,事務B在事務A提交前後各執行了一次查詢操作,發現後一次比前一次多了一條記錄。幻讀是由於併發事務增加記錄導致的,這個不能像不可重複讀通過記錄加鎖解決,因為對於新增的記錄根本無法加鎖。需要將事務序列化,才能避免幻讀。
    這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的資料行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭

.