1. 程式人生 > >如何實現靠譜的分散式鎖?

如何實現靠譜的分散式鎖?

來自:聊聊架構(微訊號:archtime),作者:鞠明業等

分散式鎖,是用來控制分散式系統中互斥訪問共享資源的一種手段,從而避免並行導致的結果不可控。基本的實現原理和單程序鎖是一致的,通過一個共享標識來確定唯一性,對共享標識進行修改時能夠保證原子性和和對鎖服務呼叫方的可見性。由於分散式環境需要考慮各種異常因素,為實現一個靠譜的分散式鎖服務引入了一定的複雜度。

分散式鎖服務一般需要能夠保證以下幾點。

同一時刻只能有一個執行緒持有鎖

鎖能夠可重入

不會發生死鎖

具備阻塞鎖特性,且能夠及時從阻塞狀態被喚醒

鎖服務保證高效能和高可用

當前使用較多的分散式鎖方案主要基於 Redis、ZooKeeper 提供的功能特性加以封裝來實現的,下面我們會簡要分析下這兩種鎖方案的處理流程以及它們各自的問題。

1. 基於 Redis 實現的鎖服務
加鎖流程
SET resource_name my_random_value NX PX max-lock-time
注:資源不存在時才能夠成功執行 set 操作,用於保證鎖持有者的唯一性;同時設定過期時間用於防止死鎖;記錄鎖的持有者,用於防止解鎖時解掉了不符合預期的鎖。

解鎖流程
if redis.get("resource_name") == " my_random_value" return redis.del("resource_name") else return 0
注:使用 Lua 指令碼保證獲取鎖的所有者、對比解鎖者是否所有者、解鎖是一個原子操作。

該方案的問題在於:

通過過期時間來避免死鎖,過期時間設定多長對業務來說往往比較頭疼,時間短了可能會造成:持有鎖的執行緒 A 任務還未處理完成,鎖過期了,執行緒 B 獲得了鎖,導致同一個資源被 A、B 兩個執行緒併發訪問;時間長了會造成:持有鎖的程序宕機,造成其他等待獲取鎖的程序長時間的無效等待。

Redis 的主從非同步複製機制可能丟失資料,會出現如下場景:A 執行緒獲得了鎖,但鎖資料還未同步到 slave 上,master 掛了,slave 頂成主,執行緒 B 嘗試加鎖,仍然能夠成功,造成 A、B 兩個執行緒併發訪問同一個資源。

2、基於 ZooKeeper 實現的鎖服務
加鎖流程

在 /resource_name 節點下建立臨時有序節點 。

獲取當前執行緒建立的節點及 /resource_name 目錄下的所有子節點,確定當前節點序號是否最小,是則加鎖成功。否則監聽序號較小的前一個節點。

注:ZAB 一致性協議保證了鎖資料的安全性,不會因為資料丟失造成多個鎖持有者;心跳保活機制解決死鎖問題,防止由於程序掛掉或者僵死導致的鎖長時間被無效佔用。具備阻塞鎖特性,並通過 Watch 機制能夠及時從阻塞狀態被喚醒。

解鎖流程是刪除當前執行緒建立的臨時接點。

該方案的問題在於通過心跳保活機制解決死鎖會造成鎖的不安全性,可能會出現如下場景:

持有鎖的執行緒 A 僵死或網路故障,導致服務端長時間收不到來自客戶端的保活心跳,服務端認為客戶端程序不存活主動釋放鎖,執行緒 B 搶到鎖,執行緒 A 恢復,同時有兩個執行緒訪問共享資源。

基於上訴對現有鎖方案的討論,我們能看到,一個理想的鎖設計目標主要應該解決如下問題:

鎖資料本身的安全性。

不發生死鎖。

不會有多個執行緒同時持有相同的鎖。

而為了實現不發生死鎖的目標,又需要引入一種機制,當持有鎖的程序因為宕機、GC 活者網路故障等各種原因無法主動過釋放鎖時,能夠有其他手段釋放掉鎖,主流的做法有兩種:

鎖設定過期時間,過期之後 Server 端自動釋放鎖。

對鎖的持有程序進行探活,發現持鎖程序不存活時 Server 端自動釋放。

實際上不管採用哪種方式,都可能造成鎖的安全性被破壞,導致多個執行緒同時持有同一把鎖的情況出現。因此我們認為鎖設計方案應在預防死鎖和鎖的安全性上取得平衡,沒有一種方案能夠絕對意義上保證不發生死鎖並且是安全的。

而鎖一般的用途又可以分為兩種,實際應用場景下,需要根據具體需求出發,權衡各種因素,選擇合適的鎖服務實現模型。無論選擇哪一種模型,需要我們清楚地知道它在安全性上有哪些不足,以及它會帶來什麼後果。

為了效率,主要是避免一件事被重複的做多次,用於節省 IT 成本,即使鎖偶然失效,也不會造成資料錯誤,該種情況首要考慮的是如何防止死鎖。

為了正確性,在任何情況下都要保證共享資源的互斥訪問,一旦發生就意味著資料可能不一致,造成嚴重的後果,該種情況首要考慮的是如何保證鎖的安全。

下面主要介紹一下 SharkLock 的一些設計選擇。

鎖資訊設計如下

lockBy:Client 唯一標識。

condition:Client 在加鎖時傳給 Server,用於定義 Client 期望 Server 的行為方式。

lockTime:加鎖時間。

txID:全域性自增 ID。

lease:租約。

如何保證鎖資料的可靠性
SharkLock 底層儲存使用的是 SharkStore,SharkStore 是一個分散式的持久化 Key-Value 儲存系統。採用多副本來保證資料安全,同時使用 raft 來保證各個副本之間的資料一致性。

如何預防死鎖
Client 定時向 Server 傳送心跳包,Server 收到心跳包之後,維護 Server 端 Session 並立即回覆,Client 收到心跳包響應後,維護 Client 端 Session。心跳包同時承擔了延長 Session 租約的功能。

當鎖持有方發生故障時,Server 會在 Session 租約到期後,自動刪除該 Client 持有的鎖,以避免鎖長時間無法釋放而導致死鎖。Client 會在 Session 租約到期後,進行回撥,可選擇性的決策是否要結束對當前持有資源的訪問。

對於未設定過期的鎖,也就意味著無法通過租約自動釋放故障 Client 持有的鎖。因此額外提供了一種協商機制,在加鎖的時候傳遞一些 condition 到服務端,用於約定 Client 端期望 Server 端對異常情況的處理,包括什麼情況下能夠釋放鎖。譬如可以通過這種機制實現 Server 端在未收到十個心跳請求後自動釋放鎖,Client 端在未收到五個心跳響應後主動結束對共享資源的訪問。

盡最大程度保證鎖被加鎖程序主動釋放。

a)程序正常關閉時呼叫鉤子來嘗試釋放鎖。

b)未釋放的鎖資訊寫檔案,程序重啟後讀取鎖資訊,並嘗試釋放鎖。

如何確保鎖的安全性

  1. 儘量不打破誰加鎖誰解鎖的約束,盡最大程度保證鎖被加鎖程序主動釋放。

a)程序正常關閉時呼叫鉤子來嘗試釋放鎖。

b)未釋放的鎖資訊寫檔案,程序重啟後讀取鎖資訊,並嘗試釋放鎖。

  1. 依靠自動續約來維持鎖的持有狀態,在正常情況下,客戶端可以持有鎖任意長的時間,這可以確保它做完所有需要的資源訪問操作之後再釋放鎖。一定程度上防止如下情況發生。

a)執行緒 A 獲取鎖,進行資源訪問。

b)鎖已經過期,但 A 執行緒未執行完成。

c)執行緒 B 獲得了鎖,導致同時有兩個執行緒在訪問共享資源。

  1. 提供一種安全檢測機制,用於對安全性要求極高的業務場景。

a)對於同一把鎖,每一次獲取鎖操作,都會得到一個全域性增長的版本號。

b)對外暴露檢測 API checkVersion(lock_name,version),用於檢測持鎖程序的鎖是不是已經被其他程序搶佔(鎖已經有了更新的版本號)。

c)加鎖成功的客戶端與後端資源伺服器通訊的時候可帶上版本號,後端資源伺服器處理請求前,呼叫 checkVersion 去檢查鎖是否依然有效。有效則認為此客戶端依舊是鎖的持有者,可以為其提供服務。

d)該機制能在一定程度上解決持鎖 A 執行緒發生故障,Server 主動釋放鎖,執行緒 B 獲取鎖成功,A 恢復了認為自己仍舊持有鎖而發起修改資源的請求,會因為鎖的版本號已經過期而失敗,從而保障了鎖的安全性。

下面對 SharkLock 依賴的 SharkStore 做一個簡單的介紹。

SharkStore 基本模組
在這裡插入圖片描述
Master Server 叢集分片路由等元資料管理、擴容和 Failover 排程等。

Data Server 資料儲存節點,提供 RPC 服務訪問其上的 KV 資料。

Gateway Server 閘道器節點,負責使用者接入。

Sharding
SharkStore 採用多副本的形式來保證資料的可靠性和高可用。同一份資料會儲存多份,少於一半的副本宕機仍然可以正常服務。 SharkStore 的資料分佈如下圖所示。
在這裡插入圖片描述

擴容方案
當某個分片的大小到達一定閾值,就會觸發分裂操作,由一個分片變成兩個,以達到擴容的目的。

Dataserver 上 range 的 leader 自己觸發。 leader 維持寫入操作位元組計數,每到達 check size 大小,就非同步遍歷其負責範圍內的資料,計算大小並同時找出分裂時的中間 key 如果大小到達 split size,向 master 發起 AskSplit 請求,同意後提交一個分裂命令。分裂命令也會通過 raft 複製到其他副本。

本地分裂。分裂是一個本地操作,在本地新建一個 range,把原始 range 的部分資料劃撥給新 range,原始 range 仍然保留,只是負責的範圍減半。分裂是一個輕量級的操作。

Failover 方案
failover 以 range 的級別進行。range 的 leader 定時向其他副本傳送心跳,一段時間內收不到副本的心跳回應,就判斷副本宕機,通過 range 心跳上報給 master。由 master 發起 failover 排程。 Master 會先刪除宕機的副本然後選擇一個合適的新節點,新增到 range 組內之後通過 raft 複製協議來完成新節點的資料同步。

Balance 方案
dataserver 上的 range leader 會通過 range 心跳上報一些資訊,每個 dataserver 還會有一個節點級別的 Node 心跳。 Master 收集這些資訊來執行 balance 操作。Balance 通過在流量低的節點上增加副本,流量高的節點上減少副本促使整個叢集比較均衡,維護叢集的穩定和效能。

Raft 實踐 -MultiRaft

  1. 心跳合併

以目標 dataserver 為維度,合併 dataserver 上所有 Raft 心跳 心跳只攜帶 range ids,心跳只用來維護 leader 的權威和副本健康檢測 range ids 的壓縮,比如差量 + 整型變長 Leader 類似跟蹤複製進度,跟蹤 follower commit 位置。

  1. 快照管理控制

建立 ACK 機制,在對端處理能力之內傳送快照 ; 控制傳送和應用快照的併發度,以及限速 ; 減少對正常業務的衝擊。Raft 實踐 -PreVote。

Raft 演算法中,leader 收到來自其他成員 term 比較高的投票請求會退位變成 follower因此,在節點分割槽後重加入、網路閃斷等異常情況下,日誌進度落後的副本發起選舉,但其本身並無法被選舉為 leader,導致叢集在若干個心跳內丟失 leader,造成效能波動 ;針對這種情況,在 raft 作者的博士論文中,提出了 prevote 演算法: 在發起選舉前,先進行一次預選舉 Pre-Candidate, 如果預選舉時能得到大多數的投票,再增加 term,進行正常的選舉。 prevote 會導致選舉時間變長 (多了一輪 RPC),然而這個影響在實踐中是非常小的, 可以有利於叢集的穩定,是非常值得的實踐。

Raft 實踐 -NonVoter
一個新的 raft 成員加入後,其日誌進度為空 ; 新成員的加入可能會導致 quorum 增加,並且同時引入了一個進度異常的副本 ; 新成員在跟上 leader 日誌進度之前,新寫入的日誌都無法複製給它 ; 如果此時再有原叢集內一個成員宕機, 很有可能導致叢集內可寫副本數到不到 quorum,使得叢集變得不可寫。 很多 raft 的實現中,都會引入了一種特殊身份的 raft 成員 (non-voting 或者 learner) Learner 在計算 quorum 時不計入其內,只被動接收日誌複製,選舉超時不發起選舉 ; 在計算寫入操作是否複製給大多數 (commit 位置) 時,也忽略 learner。 Sharkstore raft 會在 leader 端監測 learner 的日誌進度, 當 learner 的進度跟 leader 的差距小於一定百分比 (適用於日誌總量比較大) 或者小於一定條數時 (適用於日誌總量比較小), 由 leader 自動發起一次 raft 成員變更,提升 leaner 成員為正常成員。

SharkStore 目前已經開源,有興趣的同學可詳細瞭解,期待能跟大家能夠一塊兒溝通交流

相關推薦

如何實現分散式

來自:聊聊架構(微訊號:archtime),作者:鞠明業等 分散式鎖,是用來控制分散式系統中互斥訪問共享資源的一種手段,從而避免並行導致的結果不可控。基本的實現原理和單程序鎖是一致的,通過一個共享標識來確定唯一性,對共享標識進行修改時能夠保證原子性和和對鎖服務呼

使用RedisTemplate實現簡易的分散式(僅供參考)

package com.*.lock; import lombok.extern.log4j.Log4j2; import org.nutz.lang.Strings; import org.springframework.data.redis.core.Re

分散式原始碼剖析(1) Redisson實現非公平分散式

Redisson分散式鎖原始碼剖析(非公平鎖) maven配置檔案: <dependency> <groupId>org.redisson</groupId> <artifactId>redisso

curator實現zookeeper的分散式

基於curator元語實現的分散式鎖種類有好幾種,下面只講解一種實現。InterProcessMutex實現的分散式鎖屬於可重入式鎖,當一個客戶端獲取到lock鎖之後,可以重複呼叫acquire()而不會發生阻塞。基於InterProcessSemaphoreM

SpringBoot基於資料庫實現簡單的分散式

本文介紹SpringBoot基於資料庫實現簡單的分散式鎖。 1.簡介 分散式鎖的方式有很多種,通常方案有: 基於mysql資料庫 基於redis 基於ZooKeeper 網上的實現方式有很多,本文主要介紹的是如果使用mysql實現簡單的分散式鎖,加鎖流程如下圖: 其實大致思想如下: 1.根據一個

Redis實現快取與分散式

# 快取與分散式鎖 哪些資料適合放入快取 - 即時性、資料一致性要求不高的 - 訪問量大且更新頻率不高的資料 **選擇redis做為快取中介軟體** ```xml ``` ### 問題記錄與分析 #### 產生堆外記憶體溢位:OutOfDirectMemoryError 1. spri

淺談分散式--基於快取(Redis,memcached,tair)實現

淺談分散式鎖--基於快取(Redis,memcached,tair)實現篇: 一、Redis分散式鎖 1、Redis實現分散式鎖的原理:     1.利用setnx命令,即只有在某個key不存在情況才能set成功該key,這樣就達到了多個程序併發去set

淺談分散式--基於Zookeeper實現

淺談分散式鎖--基於Zookeeper實現篇: 1、基於zookeeper臨時有序節點可以實現的分散式鎖。其實基於ZooKeeper,就是使用它的臨時有序節點來實現的分散式鎖。 來看下Zookeeper能不能解決前面提到的問題。     鎖無法釋放:使用

淺談分散式--基於資料庫實現

淺談分散式鎖--基於資料庫實現篇 1、基於資料庫表     要實現分散式鎖,最簡單的方式可能就是直接建立一張鎖表,然後通過操作該表中的資料來實現了。     當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的

實現基於redis的分散式並整合spring-boot-starter

文章目錄 概述 使用 1.導包 2.寫一個實現鎖功能的service 3.檢查redis的key 4.呼叫(鎖成功) 5.呼叫(鎖失敗) 實現

註解形式實現,Redis分散式

Redis工具類參考我的博文:https://blog.csdn.net/weixin_38399962/article/details/82753763 一個註解就可以實現分散式鎖?這麼神奇麼? 首先定義註解: /** * Description:分散式Redis鎖 * User:

RedisTemplate實現分散式

使用Redis的SETNX命令獲取分散式鎖的步驟: C1和C2執行緒同時檢查時間戳獲取鎖,執行SETNX命令並都返回0,此時鎖仍被C3持有,並且C3已經崩潰 C1 DEL鎖 C1 使用SETNX命令獲取鎖,並且成功 C2 DEL鎖 C2 使用SETNX命令獲取鎖,並

分散式解決併發的三種實現方式

分散式鎖解決併發的三種實現方式 在很多場景中,我們為了保證資料的最終一致性,需要很多的技術方案來支援,比如分散式事務、分散式鎖等。有的時候,我們需要保證一個方法在同 一時間內只能被同一個執行緒執行。在單機環境中,Java中其實提供了很多併發處理相關的API,但是這些API在分散式場景中就無能

分散式學習筆記七:基於zookeeper實現分散式

一、分散式鎖介紹         分散式鎖主要用於在分散式環境中保護跨程序、跨主機、跨網路的共享資源實現互斥訪問,以達到保證資料的一致性。 二、架構介紹     &nb

分散式學習筆記四:分散式實現方式

目前幾乎很多大型網站及應用都是分散式部署的,分散式場景中的資料一致性問題一直是一個比較重要的話題。分散式的CAP理論告訴我們“任何一個分散式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分割槽容錯性(Partition tolerance),最多隻能同時滿足

分散式(Zookeeper實現

分散式鎖 分散式鎖,這個主要得益於 ZooKeeper 為我們保證了資料的強一致性。鎖服務可以分為兩類,一個是 保持獨佔,另一個是 控制時序。 1. 所謂保持獨佔,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把 zk 上的一個 znode 看作是一把鎖,通過 create

springboot2結合redis,實現分散式

1.新增Maven依賴 <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId>

Springboot整合curator,實現分散式(zookeeper)

0.linux安裝啟動zookeeper yum install nc wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.10/zookeeper-3.4.10.tar.gz tar -zxvf zookeepe

redis分散式實現方式

    前言:分散式鎖的實現方式一般有三種,1:基於資料庫的樂觀鎖。2:基於redis的分散式鎖。3:基於zk的分散式鎖,本文主要介紹第二種實現,由於以前一直是單機寫筆記,所以第一次寫有寫的不好的地方歡迎大家指正。     網上對於redis分散式鎖的實現

redis與zk實現分散式

概述 分散式鎖,如果你有多個機器在訪問同一個共享資源, 那麼這個時候,如果你需要加個鎖,讓多個分散式的機器在訪問共享資源的時候序列起來 那麼這個時候,那個鎖,多個不同機器上的服務共享的鎖,就是分散式鎖 分散式鎖當然有很多種不同的實現方案,redis分散式鎖,