微服務-分佈式鎖(一)-MySQL方案

1 基於唯一索引(insert)實現

記錄鎖的樂觀鎖方案。基於數據庫的實現方式的核心思想是:在數據庫中創建一個表,表中包含方法名等字段,並在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。

1.1 優缺點

優點

  • 實現簡單、易於理解

缺點

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

1.2 實現方案

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖的鍵值',
  `lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '鎖的超時時間',
  `remarks` varchar(255) NOT NULL COMMENT '備註信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_lock_key` (`lock_key`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

① 獲取鎖:想要執行某個方法,就使用這個方法名向表中插入數據

INSERT INTO method_lock (lock_key, lock_timeout, remarks) VALUES ('methodName', '2021-07-19 18:20:00', '測試的methodName');

② 釋放鎖:釋放鎖的時候就刪除記錄

DELETE FROM method_lock WHERE lock_key ='methodName';

1.3 問題與解決

  • 強依賴數據庫可用性,是一個單點(部署雙實例)
  • 沒有失效時間,一旦解鎖失敗,就會導致死鎖(添加定時任務掃描表)
  • 一旦插入失敗就會直接報錯,不會進入排隊隊列(使用while循環,成功後才返回)
  • 是非重入鎖,同一線程在沒有釋放鎖之前無法再次獲得該鎖(添加字段記錄機器和線程信息,查詢時相同則直接分配)
  • 非公平鎖(建中間表記錄等待鎖的線程,根據創建時間排序後進行依次處理)
  • 採用唯一索引衝突防重,在大併發情況下有可能會造成鎖表現象(採用程序生產主鍵進行防重)

2 基於表字段版本號實現

版本號對比更新的樂觀鎖方案。一般是通過爲數據庫表添加一個 version 字段來實現讀取出數據時,將此版本號一同讀出。之後更新時,對此版本號加 1,在更新過程中,會對版本號進行比較,如果是一致的,沒有發生改變,則會成功執行本次操作;如果版本號不一致,則會更新失敗。實際就是個CAS過程。

2.1 優缺點

缺點

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

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

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

3.1 優缺點

優點

  • 實現簡單、易於理解

缺點

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

3.2 實現方案

**

CREATE TABLE `methodLock` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
      `lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖的鍵值',
      `lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '鎖的超時時間',
      `remarks` varchar(255) NOT NULL COMMENT '備註信息',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY ( `id` ),
    UNIQUE KEY `uidx_lock_key` ( `lock_key ` ) USING BTREE 
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '鎖定中的方法';

加解鎖操作

/**
 * 加鎖
 */
public boolean lock() {
        // 開啓事務
        connection.setAutoCommit(false);
        // 循環阻塞,等待獲取鎖
        while (true) {
            // 執行獲取鎖的sql
            String sql = "select * from methodLock where lock_key = xxx for update";
             // 創建prepareStatement對象,用於執行SQL
            ps = conn.prepareStatement(sql);
            // 獲取查詢結果集
            int result = ps.executeQuery();
            // 結果非空,加鎖成功
            if (result != null) {
                return true;
            }
        }
    
        // 加鎖失敗
        return false;
}

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

更多JAVA、高併發、微服務、架構、解決方案、中間件的總結在:https://github.com/yu120/lemon-guide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章