分佈式鎖-基於Mysql實現

分佈式鎖(4)-基於Mysql實現

1.使用場景

在分佈式系統裏,我們有時執行定時任務,或者處理某些併發請求,需要確保多點系統裏同時只有一個執行線程進行處理。

分佈式鎖就是在分佈式系統裏互斥訪問資源的解決方案。

通常我們會更多地使用Redis分佈式鎖、Zookeeper分佈式鎖的解決方案。

本篇文章介紹的是基於MySQL實現的分佈式鎖方案,性能上肯定是不如Redis、Zookeeper

所以,基於Mysql實現分佈式鎖,適用於對性能要求不高,並且不希望因爲要使用分佈式鎖而引入新組件

2.基於唯一索引(insert)實現

2.1 實現方式

  • 獲取鎖時在數據庫中insert一條數據,包括id、方法名(唯一索引)、線程名(用於重入)、重入計數
  • 獲取鎖如果成功則返回true
  • 獲取鎖的動作放在while循環中,週期性嘗試獲取鎖直到結束或者可以定義方法來限定時間內獲取鎖
  • 釋放鎖的時候,delete對應的數據

2.2 優點:

  • 實現簡單、易於理解

2.3 缺點

  • 沒有線程喚醒,獲取失敗就被丟掉了;
  • 沒有超時保護,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖;
  • 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用;
  • 併發量大的時候請求量大,獲取鎖的間隔,如果較小會給系統和數據庫造成壓力;
  • 這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯,沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作;
  • 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖,因爲數據中數據已經存在了;
  • 這把鎖是非公平鎖,所有等待鎖的線程憑運氣去爭奪鎖。

2.4 簡單實現方案

新建一張表,用於存儲鎖的信息,需要加鎖的時候就插入一條記錄,釋放鎖的時候就刪除這條記錄

新建一張最簡單的表

CREATE TABLE `t_lock` (
  `lock_key` varchar(64) NOT NULL COMMENT '鎖的標識',
  PRIMARY KEY (`lock_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分佈式鎖'

根據插入sql返回受影響的行數,大於0表示成功佔有鎖

insert ignore into t_lock(lock_key) values(:lockKey)

釋放鎖的時候就刪除記錄

delete from t_lock where lock_key = :lockKey

2.5 完善實現方案

上面這種簡單的實現有以下幾個問題:

  • 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。
  • 這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
  • 這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。
  • 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。

當然,我們也可以有其他方式解決上面的問題。

  • 數據庫是單點?搞兩個數據庫,數據之前雙向同步。一旦掛掉快速切換到備庫上。
  • 沒有失效時間?只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
  • 非阻塞的?搞一個while循環,直到insert成功再返回成功。
  • 非重入的?在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。

3.基於排他鎖(for update)實現

3.1 實現方式

  • 獲取鎖可以通過,在select語句後增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖。當某條記錄被加上排他鎖之後,其他線程無法再在該行記錄上增加排他鎖,我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖;
  • 其餘實現與使用唯一索引相同;
  • 釋放鎖通過connection.commit();操作,提交事務來實現。

for update具體可參考數據庫-MySQL中for update的作用和用法一文。

3.2 優點

  • 實現簡單、易於理解。

3.3 缺點

  • 排他鎖會佔用連接,產生連接爆滿的問題;
  • 如果表不大,可能並不會使用行鎖;
  • 同樣存在單點問題、併發量問題。

3.4 僞代碼

CREATE TABLE `methodLock` (
    `id` INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
    `method_name` VARCHAR ( 64 ) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
    `desc` VARCHAR ( 1024 ) NOT NULL DEFAULT '備註信息',
    `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
    PRIMARY KEY ( `id` ),
    UNIQUE KEY `uidx_method_name` ( `method_name ` ) USING BTREE 
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '鎖定中的方法';
/**
 * 加鎖
 */
public boolean lock() {
    // 開啓事務
    connection.setAutoCommit(false);
    // 循環阻塞,等待獲取鎖
    while (true) {
        // 執行獲取鎖的sql
        result = select * from methodLock where method_name = xxx for update;
        // 結果非空,加鎖成功
        if (result != null) {
            return true;
        }
    }
    // 加鎖失敗
    return false;
}

/**
 * 解鎖
 */
public void unlock() {
    // 提交事務,解鎖
    connection.commit();
}

4.樂觀鎖實現

一般是通過爲數據庫表添加一個 version 字段來實現讀取出數據時,將此版本號一同讀出.

之後更新時,對此版本號加1,在更新過程中,會對版本號進行比較,如果是一致的,沒有發生改變,則會成功執行本次操作;如果版本號不一致,則會更新失敗。

實際就是個CAS過程。

缺點:

  1. 這種操作方式,使原本一次的update操作,必須變爲2次操作: select版本號一次;update一次。增加了數據庫操作的次數。
  2. 如果業務場景中的一次業務流程中,多個資源都需要用保證數據一致性,那麼如果全部使用基於數據庫資源表的樂觀鎖,就要讓每個資源都有一張資源表,這個在實際使用場景中肯定是無法滿足的。而且這些都基於數據庫操作,在高併發的要求下,對數據庫連接的開銷一定是無法忍受的。
  3. 樂觀鎖機制往往基於系統中的數據存儲邏輯,因此可能會造成髒數據被更新到數據庫中。

5.總結

數據庫鎖現在使用較多的就上面說的3種方式,排他鎖(悲觀鎖),版本號(樂觀鎖),記錄鎖,各有優缺點。

注意點:

  • 使用mysql分佈式鎖,必須保證多個服務節點使用的是同一個mysql庫。

優點

  • 直接藉助DB簡單易懂。
  • 方便快捷,因爲基本每個服務都會連接,但是不是每個服務都會使用redis或者zookeeper;
  • 如果客戶端斷線了會自動釋放鎖,不會造成鎖一直被佔用;
  • mysql分佈式鎖是可重入鎖,對於舊代碼的改造成本低;

缺點

  • 加鎖直接打到數據庫,增加了數據庫的壓力;
  • 加鎖的線程會佔用一個session,也就是一個連接數,如果併發量大可能會導致正常執行的sql語句獲取不到連接;
  • 服務拆分後如果每個服務使用自己的數據庫,則不合適;
  • 鎖的可用性和數據庫強關聯,一旦數據庫掛了,則整個分佈式鎖不可用;
  • 如果需要考慮極限情況,會有超時等各種問題,在解決問題的過程中會使整個方案變得越來越複雜;
  • 數據庫的性能瓶頸相較於rediszk要低很多,當調用量大的時候,性能問題將成爲關鍵;
  • 還需要考慮超時等問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章