分布式鎖的幾種實現原理


分布式鎖主流有三種模式:

實現方式 功能要求 實現難度 學習成本 運維成本
MySQL 的方案借助表鎖/行鎖實現 滿足基本要求 不難 熟悉 小量OK、大量影響現有業務、1主多從架構,不方便擴容
通過 ZK 創建數據節點的方式實現 滿足要求 熟悉 ZK API 即可 需要學習 重,需要堆機器,有跨機房請求
Redis 使用 setnxex 基本要求 不難 熟悉 擴容方便、現有服務

MySQL 單主架構,寫都會到 master,有瓶頸。ZK 的方式需要自己搭建、運維,而且需要堆機器,利用率不高。最終采用了 Redis 來實現,流量/存儲都可以擴容,運維也不需要自己。

一、基於Mysql實現分布式鎖 (樂觀鎖)

Mysql實現分布式鎖 主要是基於數據庫的排他鎖(也叫行級排他鎖), 采用樂觀鎖的方式去做。
我們可以通過一個update語句是否成功來判斷線程搶占鎖是否成功,比如如下sql語句:

CREATE TABLE `t_schedule_cluster` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '@cname:主鍵', `execute` int(1) NOT NULL COMMENT '@cname:執行狀態', `version` int(11) NOT NULL COMMENT '@cname:版本號 ', `task_name` varchar(128) NOT NULL COMMENT '@cname:任務名稱', `execute_ip` varchar(32) DEFAULT NULL COMMENT '@cname:執行ip ', `update_time` datetime DEFAULT NULL COMMENT '@cname:修改時間', PRIMARY KEY (`id`), KEY `Index_series_id` (`execute`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='@cname:多機定時任務調度';

爭搶鎖的sql語句:
update t_schedule_cluster set execute = 1 version = ?, execute_ip = ?, update_time = ? where task_name = ? and version = ?

實現原理入下圖:

但是數據庫的性能有限,如果在高並發的情況下會頻發的訪問數據庫,對數據庫會造成較大的壓力。

二,基於redis的分布式鎖實現

基於Redis實現的分布式鎖其實很簡單,底層就是使用redis的setnx指令來實現的加鎖,我們來看看官方對setnx的定義:
SETNX key value
將 key 的值設為 value ,當且僅當 key 不存在。
若給定的 key 已經存在,則 SETNX 不做任何動作。
SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。
返回值:
設置成功,返回 1 。
設置失敗,返回 0 。

redis> EXISTS job # job 不存在
(integer) 0 redis> SETNX job "programmer" # job 設置成功 (integer) 1 redis> SETNX job "code-farmer" # 嘗試覆蓋 job ,失敗 (integer) 0 redis> GET job # 沒有被覆蓋 "programmer"

以上內容來自於:http://redisdoc.com/string/setnx.html

既然setnx這么強大,那么我們是不是可以高枕無憂直接使用了? 當然了,我們還要考慮一些極端場景。

2.1 死鎖問題

既然設置了value值,那么我們肯定會想到過期時間,那么就需要再使用setnx指令后繼續使用expire指令。但是這兩部操作必定不是原子性的,如果執行expire失敗怎么辦?
其實Redis官方也考慮到了這個問題,在Redis2.8 之后,官方執行setnx 和 expire命令一起使用了。如下:
SET lock_key lock_value NX PX 30000
其中:
1.lock_key:即鎖名稱,這個名稱應是公開的,在分布式環境中,對於某一確定的公共資源,所有爭用方(客戶端)都應該知道對應鎖的名字。對於 Redis 而言,lock_name 就是 key-value 中的 key,具有唯一性。

  1. lock_value:是由客戶端生成的一個隨機字符串,它要保證在足夠長的一段時間內在所有客戶端的所有獲取鎖的請求中都是唯一的,用於唯一標識鎖的持有者。
  2. NX 表示只有當 lock_key(key) 不存在的時候才能 SET 成功,從而保證只有一個客戶端能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。
  3. PX 30000 表示這個鎖節點有一個 30 秒的自動過期時間(目的是為了防止持有鎖的客戶端故障后,無法主動釋放鎖而導致死鎖,因此要求鎖的持有者必須在過期時間之內執行完相關操作並釋放鎖)。
    具體操作如下圖:

2.2 鎖自動過期存在的隱患

例如我們有兩個線程A、B,此時線程A搶到了鎖,且設置自動過期時間為10s鍾,因為系統其他原因導致系統A發生阻塞。而此刻10s鍾后鎖自動過期,線程C獲取到了同一個資源的鎖,線程A從阻塞中恢復,認為自己仍然持有鎖,繼續操作同一資源。這樣就使得加鎖的互斥性失效了。

解決方案:
我們在上面set lock_key lock_value 時講過,lock_value是一個隨機生成的字符串,在每次獲取鎖的時候都會重新生成。那么我們在執行真正的業務邏輯(類似於和db進行交互的操作,同一時刻只能一個線程操作的情況)時,判斷當前生成的隨機字符串和lock_value是否一致,如果不一致則說明redis中的lock_value被修改過,也就說明此刻鎖已經被其他線程所占有。

具體操作流程如下圖:

主要使用的就是這兩種方案,在這里只是做個簡單總結,其實還有其他一些可以實現分布式鎖,根據自己項目本身情況選擇最合適的。
另外 已經Redis也有開源的框架可以很好地支持基於Redis的分布式鎖,這里推薦一個:Redission https://github.com/redisson/redisson


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM