.

當面試官問到:《MySQL資料庫的鎖機制》該如何拿分?

 

為什麼要加鎖?

問題背景

  • 當多個使用者併發地存取資料時,在資料庫中就會產生多個事務同時存取同一資料的情況。若對併發操作不加控制就可能會讀取和儲存不正確的資料,破壞資料庫的一致性。

要解決的問題

  • 多使用者環境下保證資料庫完整性和一致性

鎖是什麼

  • 在電腦科學中,鎖是在執行多執行緒時用於強行限制資源訪問的同步機制,即用於在併發控制中保證對互斥要求的滿足。
  • 加鎖是實現資料庫併發控制的一個非常重要的技術。當事務在對某個資料物件進行操作前,先向系統發出請求,對其加鎖。加鎖後事務就對該資料物件有了一定的控制,在該事務釋放鎖之前,其他的事務不能對此資料物件進行更新操作。

鎖的分類

  • 行級鎖
  • 行級鎖是Mysql中鎖定粒度最細的一種鎖,表示只針對當前操作的行進行加鎖。行級鎖能大大減少資料庫操作的衝突。其加鎖粒度最小,但加鎖的開銷也最大。行級鎖分為共享鎖 和 排他鎖。
  • 特點
  • 開銷D,加鎖慢;會出現死鎖;鎖定粒度最小,F生鎖衝突的概率最低,並F度也最高。

  • 表級鎖是MySQL中鎖定粒度最大的一種鎖,表示對當前操作的整張表加鎖,它實現簡單,資源消耗較少,被大部分MySQL引擎支援。最常使用的MYISAM與INNODB都支援表級鎖定。表級鎖定分為表共享讀鎖(共享鎖)與表獨佔寫鎖(排他鎖)。
  • 特點
  • 開銷小,加鎖快;不會出現死鎖;鎖定粒度D,F出鎖衝突的概率最高,併發度最低。

  • 頁級鎖是MySQL中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但衝突多,行級衝突少,但速度慢。所以取了折衷的頁級,一次鎖定相鄰的一組記錄。BDB支援頁級鎖
  • 特點
  • 開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般

MySQL常用儲存引擎的鎖機制

MyISAM和MEMORY採用表級鎖(table-level locking)

BDB採用頁面鎖(page-level locking)或表級鎖,預設為頁面鎖

InnoDB支援行級鎖(row-level locking)和表級鎖,預設為行級鎖

  • InnoDB行鎖是通過給索引上的索引項加鎖來實現的,InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!

在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖衝突,從而影響併發效能。

行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。行級鎖的缺點是:由於需要請求大量的鎖資源,所以速度慢,記憶體消耗大。

例項說明

  • MySQL InnoDB引擎預設的修改資料語句:update,delete,insert都會自動給涉及到的資料加上排他鎖。

select語句預設不會加任何鎖型別,如果加排他鎖可以使用select …for update語句,加共享鎖可以使用select … lock in share mode語句。

所以加過排他鎖的資料行在其他事務種是不能修改資料的,也不能通過for update和lock in share mode鎖的方式查詢資料,但可以直接通過select …from…查詢資料,因為普通查詢沒有任何鎖機制。

行級鎖與死鎖

MyISAM中是不會產生死鎖的,因為MyISAM總是一次性獲得所需的全部鎖,要麼全部滿足,要麼全部等待。而在InnoDB中,鎖是逐步獲得的,就造成了死鎖的可能。

在MySQL中,行級鎖並不是直接鎖記錄,而是鎖索引。索引分為主鍵索引和非主鍵索引兩種,如果一條sql語句操作了主鍵索引,MySQL就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。在UPDATE、DELETE操作時,MySQL不僅鎖定WHERE條件掃描過的所有索引記錄,而且會鎖定相鄰的鍵值,即所謂的next-key locking。

當兩個事務同時執行,一個鎖住了主鍵索引,在等待其他相關索引。另一個鎖定了非主鍵索引,在等待主鍵索引。這樣就會發生死鎖。

發生死鎖後,InnoDB一般都可以檢測到,並使一個事務釋放鎖回退,另一個獲取鎖完成事務。

共享鎖與排它鎖

共享鎖(Share Lock)

  • 共享鎖又稱讀鎖,是讀取操作建立的鎖。其他使用者可以併發讀取資料,但任何事務都不能對資料進行修改(獲取資料上的排他鎖),直到已釋放所有共享鎖。

如果事務T對資料A加上共享鎖後,則其他事務只能對A再加共享鎖,不能加排他鎖。獲准共享鎖的事務只能讀資料,不能修改資料。

用法 SELECT ... LOCK IN SHARE MODE;

在查詢語句後面增加LOCK IN SHARE MODE,Mysql會對查詢結果中的每行都加共享鎖,當沒有其他執行緒對查詢結果集中的任何一行使用排他鎖時,可以成功申請共享鎖,否則會被阻塞。其他執行緒也可以讀取使用了共享鎖的表,而且這些執行緒讀取的是同一個版本的資料。

排它鎖(eXclusive Lock)

  • 排他鎖又稱寫鎖,如果事務T對資料A加上排他鎖後,則其他事務不能再對A加任任何型別的封鎖。獲准排他鎖的事務既能讀資料,又能修改資料。

用法 SELECT ... FOR UPDATE;

在查詢語句後面增加FOR UPDATE,Mysql會對查詢結果中的每行都加排他鎖,當沒有其他執行緒對查詢結果集中的任何一行使用排他鎖時,可以成功申請排他鎖,否則會被阻塞。

樂觀鎖(Optimistic Lock)

是什麼

  • 假設認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。

相對於悲觀鎖,在對資料庫進行處理的時候,樂觀鎖並不會使用資料庫提供的鎖機制。一般的實現樂觀鎖的方式就是記錄資料版本。

資料版本,為資料增加的一個版本標識。當讀取資料時,將版本標識的值一同讀出,資料每更新一次,同時對版本標識進行更新。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的版本標識進行比對,如果資料庫表當前版本號與第一次取出來的版本標識值相等,則予以更新,否則認為是過期資料。

實現資料版本有兩種方式,第一種是使用版本號,第二種是使用時間戳。

使用版本號實現樂觀鎖

  • 使用版本號時,可以在資料初始化時指定一個版本號,每次對資料的更新操作都對版本號執行+1操作。並判斷當前版本號是不是該資料的最新的版本號。
1.查詢出商品資訊
select (status,status,version) from t_goods where id=#{id}
2.根據商品資訊生成訂單
3.修改商品status為2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};

優點與不足

  • 樂觀併發控制相信事務之間的資料競爭(data race)的概率是比較小的,因此儘可能做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。但如果直接簡單這麼做,還是有可能會遇到不可預期的結果,例如兩個事務都讀取了資料庫的某一行,經過修改以後寫回資料庫,這時就遇到了問題

悲觀鎖(Pessimistic Lock)

是什麼

  • 在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制 (也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)

悲觀鎖的流程

  • 在對任意記錄進行修改前,先嚐試為該記錄加上排他鎖(exclusive locking)。
  • 如果加鎖失敗,說明該記錄正在被修改,那麼當前查詢可能要等待或者丟擲異常。具體響應方式由開發者根據實際需要決定。
  • 如果成功加鎖,那麼就可以對記錄做修改,事務完成後就會解鎖了。
  • 其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接丟擲異常。

MySQL InnoDB中使用悲觀鎖

  • 要使用悲觀鎖,我們必須關閉mysql資料庫的自動提交屬性,因為MySQL預設使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交。set autocommit=0;
//0.開始事務
begin;
//1.查詢出商品資訊
select status from t_goods where id=1 for update;
//2.根據商品資訊生成訂單
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status為2
update t_goods set status=2;
//4.提交事務
commit;

上面的查詢語句中,我們使用了select…for update的方式,這樣就通過開啟排他鎖的方式實現了悲觀鎖。此時在t_goods表中,id為1的 那條資料就被我們鎖定了,其它的事務必須等本次事務提交之後才能執行。這樣我們可以保證當前的資料不會被其它事務修改。

Java的鎖機制

執行緒的同步問題

  • 一段synchronized的程式碼被一個執行緒執行之前,他要先拿到執行這段程式碼的許可權,在java裡邊就是拿到某個同步物件的鎖(一個物件只有一把鎖);如果這個時候同步物件的鎖被其他執行緒拿走了,他(這個執行緒)就只能等了(執行緒阻塞在鎖池等待佇列中)。取到鎖後,他就開始執行同步程式碼(被synchronized修飾的程式碼);執行緒執行完同步程式碼後馬上就把鎖還給同步物件,其他在鎖池中等待的某個執行緒就可以拿到鎖執行同步程式碼了。這樣就保證了同步程式碼在統一時刻只有一個執行緒在執行。

執行緒的同步方法:

  • 1. 在需要同步的方法的方法簽名中加入synchronized關鍵字。
  • 2. 使用synchronized塊對需要進行同步的程式碼段進行同步。
  • 3. 使用JDK 5中提供的java.util.concurrent.lock包中的Lock物件。

ThreadLocal

當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。在ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應執行緒的變數副本。使用ThreadLocal的典型場景如資料庫連線管理,執行緒會話管理等場景,只適用於獨立變數副本的情況,如果變數為全域性共享的,則不適用在高併發下使用。

獲取資料:

最後給大家分享一些學習資料,裡面包括:(BATJ面試資料、高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)和Java進階學習路線圖。

領取方式:加微訊號 weixin99ting  備註 :(資料) 即可獲取。

最後,祝大家