請列舉你了解的分布式鎖_這幾種常見的“分布式鎖”寫法,搞懂再也不怕面試官,安排!...

什么是分布式鎖?

大家好,我是jack xu,今天跟大家聊一聊分布式鎖。首先說下什么是分布式鎖,當我們在進行下訂單減庫存,搶票,選課,搶紅包這些業務場景時,如果在此處沒有鎖的控制,會導致很嚴重的問題。學過多線程的小伙們知道,為了防止多個線程同時執行同一段代碼,我們可以用 synchronized 關鍵字或 JUC 里面的 ReentrantLock 類來控制,但是目前幾乎任何一個系統都是部署多臺機器的,單機部署的應用很少,synchronized 和 ReentrantLock 發揮不出任何作用,此時就需要一把全局的鎖,來代替 JAVA 中的 synchronized 和 ReentrantLock。

分布式鎖的實現方式流行的主要有三種,分別是基于緩存 Redis 的實現方式,基于 zk 臨時順序節點的實現以及基于數據庫行鎖的實現。我們先來說下用 Jedis 中的 setnx 命令來構建這把鎖。

Jedis寫法

使用 Redis 做分布式鎖的思路是,在 redis 中設置一個值表示加了鎖,然后釋放鎖的時候就把這個 key 刪除。思路是很簡單,但是在使用過程中要避免一些坑,我們先看下加鎖的代碼:

/**

* 嘗試獲取分布式鎖

*

* @param jedis Redis客戶端

* @param lockKey 鎖

* @param requestId 請求標識

* @param expireTime 超期時間

* @return 是否獲取成功

*/

public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

// set支持多個參數 NX(not exist) XX(exist) EX(seconds) PX(million seconds)

String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);

if (LOCK_SUCCESS.equals(result)) {

return true;

}

return false;

}

這段代碼很簡單,主要說下這里用的命令是 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL],而沒有使用 SETNX+EXPIRE 的命令,原因是 SETNX+EXPIRE 是兩條命令無法保證原子性,而 SET 是原子操作。那這里為什么要設置超時時間呢?原因是當一個客戶端獲得了鎖在執行任務的過程中掛掉了,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,這將會導致死鎖,所以必須設置一個超時時間。

釋放鎖的代碼如下:

/**

* 釋放分布式鎖

*

* @param jedis Redis客戶端

* @param lockKey 鎖

* @param requestId 請求標識,當前工作線程線程的名稱

* @return 是否釋放成功

*/

public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

if (RELEASE_SUCCESS.equals(result)) {

return true;

}

return false;

}

這里也有兩點注意的地方,第一是解鈴還須系鈴人,怎么理解呢,就是 A 加的鎖 B 不能去 del 掉吧,不然豈不是全亂套了,誰加的鎖就誰去解,我們一般把 value 設為當前線程的 Id,Thread.currentThread().getId(),然后在刪的時候判斷下是不是當前線程。第二點是驗證和釋放鎖是兩個獨立操作,不是原子性,這個怎么解決呢?使用 Lua 腳本,即 if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end,它能給我們保證原子性。

Redisson寫法

Redisson 是 Java 的 Redis 客戶端之一,提供了一些 API 方便操作 Redis。但是 Redisson 這個客戶端可有點厲害,我們先打開官網看下 http://github.com/redisson/re…

這個目錄里面有很多的功能,Redisson 跟 Jedis 定位不同,它不是一個單純的 Redis 客戶端,而是基于 Redis 實現的分布式的服務,我們可以看到還有 JUC 包下面的類名,Redisson 幫我們搞了分布式的版本,比如 AtomicLong,直接用 RedissonAtomicLong 就行了。鎖只是它的冰山一角,并且它對主從,哨兵,集群等模式都支持,當然了,單節點模式肯定是支持的。

在 Redisson 里面提供了更加簡單的分布式鎖的實現,我們來看下它的用法,相當的簡單,兩行代碼搞定,比 Jedis 要簡單的多,而且在 Jedis 里需要考慮的問題,它都已經幫我們封裝好了。

我們來看下,這里獲取鎖有很多種的方式,有公平鎖有讀寫鎖,我們使用的是 redissonClient.getLock, 這是一個可重入鎖。

現在我把程序啟動一下

打開 Redis Desktop Manager 工具,看下到底它存的是什么。原來在加鎖的時候,寫入了一個 HASH 類型的值,key 是鎖名稱 jackxu,field 是線程的名稱,而 value 是 1(即表示鎖的重入次數)。

小伙伴可能覺得我在一派胡言,沒關系,我們點進去看下它的源碼是具體實現的。

點進 tryLock() 方法的 tryAcquire() 方法,再到->tryAcquireAsync() 再到->tryLockInnerAsync(),終于見到廬山真面目了,原來它最終也是通過 Lua 腳本來實現的。

現在我把這段Lua腳本拿出來分析一下,很簡單。

// KEYS[1] 鎖名稱 updateAccount

// ARGV[1] key 過期時間 10000ms

// ARGV[2] 線程名稱

// 鎖名稱不存在

if (redis.call('exists', KEYS[1]) == 0) then

// 創建一個 hash,key=鎖名稱,field=線程名,value=1

redis.call('hset', KEYS[1], ARGV[2], 1);

// 設置 hash 的過期時間

redis.call('pexpire', KEYS[1], ARGV[1]);

return nil;

end;

// 鎖名稱存在,判斷是否當前線程持有的鎖

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then

// 如果是,value+1,代表重入次數+1

redis.call('hincrby', KEYS[1], ARGV[2], 1);

// 重新獲得鎖,需要重新設置 Key 的過期時間

redis.call('pexpire', KEYS[1], ARGV[1]);

return nil;

end;

// 鎖存在,但是不是當前線程持有,返回過期時間(毫秒)

return redis.call('pttl', KEYS[1]);

unlock() 中的 unlockInnerAsync() 釋放鎖,同樣也是通過 Lua 腳本實現。

// KEYS[1] 鎖的名稱 updateAccount

// KEYS[2] 頻道名稱 redisson_lock__channel:{updateAccount}

// ARGV[1] 釋放鎖的消息 0

// ARGV[2] 鎖釋放時間 10000

// ARGV[3] 線程名稱

// 鎖不存在(過期或者已經釋放了)

if (redis.call('exists', KEYS[1]) == 0) then

// 發布鎖已經釋放的消息

redis.call('publish', KEYS[2], ARGV[1]);

return 1;

end;

// 鎖存在,但是不是當前線程加的鎖

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then

return nil;

end;

// 鎖存在,是當前線程加的鎖

// 重入次數-1

local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);

// -1 后大于 0,說明這個線程持有這把鎖還有其他的任務需要執行

if (counter > 0) then

// 重新設置鎖的過期時間

redis.call('pexpire', KEYS[1], ARGV[2]);

return 0;

else

// -1 之后等于 0,現在可以刪除鎖了

redis.call('del', KEYS[1]);

// 刪除之后發布釋放鎖的消息

redis.call('publish', KEYS[2], ARGV[1]);

return 1;

end;

// 其他情況返回 nil

return nil;

看完它的使用后,我們發現真的使用起來像 JDK 中的 ReentrantLock 一樣絲滑。

RedLock

RedLock 的中文是直譯過來的,就叫紅鎖。紅鎖并非是一個工具,而是 Redis 官方提出的一種分布式鎖的算法。我們知道如果采用單機部署模式,會存在單點問題,只要 redis 故障了,加鎖就不行了。如果采用 master-slave 模式,加鎖的時候只對一個節點加鎖,即便通過 sentinel 做了高可用,但是如果 master 節點故障了,發生主從切換,此時就會有可能出現鎖丟失的問題。基于以上的考慮,其實 redis 的作者 Antirez 也考慮到這個問題,他提出了一個 RedLock 的算法。

我在這里畫了一個圖,圖中這五個實例都是獨自部署的,沒有主從關系,它們就是5個 master 節點。

通過以下步驟獲取一把鎖:獲取當前時間戳,單位是毫秒

輪流嘗試在每個 master 節點上創建鎖,過期時間設置較短,一般就幾十毫秒

嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)

客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了

要是鎖建立失敗了,那么就依次刪除這個鎖

只要別人建立了一把分布式鎖,你就得不斷輪詢去嘗試獲取鎖

但是這樣的這種算法還是頗具爭議的,可能還會存在不少的問題,無法保證加鎖的過程一定正確。Martin Kleppmann 針對這個算法提出了質疑,接著 antirez 又回復了 Martin Kleppmann 的質疑。一個是很有資歷的分布式架構師,一個是 Redis 之父,這個就是著名的關于紅鎖的神仙打架事件。

最后 Redisson 官網上也給出了如何使用紅鎖 redlock,幾行代碼搞定,依然很絲滑,感興趣的小伙伴可以看下。

Zookeeper寫法

在介紹 zookeeper 實現分布式鎖的機制之前,先粗略介紹一下 zk 是什么東西: zk 是一種提供配置管理、分布式協同以及命名的中心化服務。它的模型是這樣的:包含一系列的節點,叫做znode,就好像文件系統一樣每個 znode 表示一個目錄,然后 znode 有一些特性,我們可以把它們分為四類:持久化節點(zk斷開節點還在)

持久化順序節點(如果是第一個創建的子節點,那么生成的子節點為/lock/node-0000000000,下一個節點則為/lock/node-0000000001,依次類推)

臨時節點(客戶端斷開后節點就刪除了)

臨時順序節點

zookeeper分布式鎖恰恰應用了臨時順序節點,下面我們就用圖解的方式來看下是怎么實現的。

獲取鎖

首先,在 Zookeeper 當中創建一個持久節點 ParentLock。當第一個客戶端想要獲得鎖時,需要在ParentLock這個節點下面創建一個臨時順序節點 Lock1。

之后,Client1 查找 ParentLock 下面所有的臨時順序節點并排序,判斷自己所創建的節點 Lock1 是不是順序最靠前的一個。如果是第一個節點,則成功獲得鎖。

這時候,如果再有一個客戶端 Client2 前來獲取鎖,則在 ParentLock 下再創建一個臨時順序節點Lock2。

Client2 查找 ParentLock 下面所有的臨時順序節點并排序,判斷自己所創建的節點 Lock2 是不是順序最靠前的一個,結果發現節點 Lock2 并不是最小的。

于是,Client2 向排序僅比它靠前的節點 Lock1 注冊 Watcher,用于監聽 Lock1 節點是否存在。這意味著 Client2 搶鎖失敗,進入了等待狀態。

這時候,如果又有一個客戶端 Client3 前來獲取鎖,則在ParentLock下載再創建一個臨時順序節點Lock3。

Client3 查找 ParentLock 下面所有的臨時順序節點并排序,判斷自己所創建的節點 Lock3 是不是順序最靠前的一個,結果同樣發現節點 Lock3 并不是最小的。

于是,Client3 向排序僅比它靠前的節點 Lock2 注冊 Watcher,用于監聽 Lock2 節點是否存在。這意味著 Client3 同樣搶鎖失敗,進入了等待狀態。

這樣一來,Client1 得到了鎖,Client2 監聽了 Lock1,Client3 監聽了 Lock2。這恰恰形成了一個等待隊列,很像是 Java 當中 ReentrantLock 所依賴的 AQS(AbstractQueuedSynchronizer)。

釋放鎖

釋放鎖分為兩種情況:

1.任務完成,客戶端顯示釋放

當任務完成時,Client1 會顯示調用刪除節點 Lock1 的指令。

2.任務執行過程中,客戶端崩潰

獲得鎖的 Client1 在任務執行過程中,如果 Duang 的一聲崩潰,則會斷開與 Zookeeper 服務端的鏈接。根據臨時節點的特性,相關聯的節點 Lock1 會隨之自動刪除。

由于 Client2 一直監聽著 Lock1 的存在狀態,當 Lock1 節點被刪除,Client2 會立刻收到通知。這時候 Client2 會再次查詢 ParentLock 下面的所有節點,確認自己創建的節點 Lock2 是不是目前最小的節點。如果是最小,則 Client2 順理成章獲得了鎖。

同理,如果 Client2 也因為任務完成或者節點崩潰而刪除了節點 Lock2,那么 Client3 就會接到通知。

最終,Client3 成功得到了鎖。

Curator

在 Apache 的開源框架 Apache Curator 中,包含了對 Zookeeper 分布式鎖的實現。 http://github.com/apache/cura…

它的使用方式也很簡單,如下所示:

我們看了下依然絲滑,源碼我就不分析了,感興趣的可以看我同事的博客 Curator的ZK分布式鎖實現原理 。

總結

zookeeper 天生設計定位就是分布式協調,強一致性,鎖很健壯。如果獲取不到鎖,只需要添加一個監聽器就可以了,不用一直輪詢,性能消耗較小。缺點: 在高請求高并發下,系統瘋狂的加鎖釋放鎖,最后 zk 承受不住這么大的壓力可能會存在宕機的風險。在這里簡單的提一下,zk 鎖性能比 redis 低的原因:zk 中的角色分為 leader,flower,每次寫請求只能請求 leader,leader 會把寫請求廣播到所有 flower,如果 flower 都成功才會提交給 leader,其實這里相當于一個 2PC 的過程。在加鎖的時候是一個寫請求,當寫請求很多時,zk 會有很大的壓力,最后導致服務器響應很慢。

redis 鎖實現簡單,理解邏輯簡單,性能好,可以支撐高并發的獲取、釋放鎖操作。缺點: Redis 容易單點故障,集群部署,并不是強一致性的,鎖的不夠健壯; key 的過期時間設置多少不明確,只能根據實際情況調整;需要自己不斷去嘗試獲取鎖,比較消耗性能。

最后不管 redis 還是 zookeeper,它們都應滿足分布式鎖的特性:具備可重入特性(已經獲得鎖的線程在執行的過程中不需要再次獲得鎖)

異常或者超時自動刪除,避免死鎖

互斥(在分布式環境下同一時刻只能被單個線程獲取)

分布式環境下高性能、高可用、容錯機制

各有千秋,具體業務場景具體使用。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/457513.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/457513.shtml
英文地址,請注明出處:http://en.pswp.cn/news/457513.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

leetcode 268

等差數列求值 1 class Solution {2 public:3 int missingNumber(vector<int>& nums) {4 int nnums.size();5 int kn*(n1)/2;6 for(int i0;i<n;i)7 k-nums[i];8 return k;9 } 10 }; 轉載于:https://www.cnblogs.…

301緩存重定向?301 Moved Permanently (from disk cache)

今天在寫一個博客系統時&#xff0c;發現首頁數據經常刷新不出來&#xff0c;甚至后端根本就沒有接受到這個請求&#xff0c;以為是Ajax的問題&#xff0c;但通過抓包發現Ajax請求確實已經發出去了&#xff0c;但狀態碼是 301 Moved Permanently (from disk cache),301是永久重…

Firefox 50優化Electrolysis

Mozilla正式發布Firefox 50。最新的版本中提升了來自多個內容進程用戶的用戶體驗&#xff0c;并修復了十幾個高影響的安全漏洞。\\在Firefox最新版本的變更中&#xff0c;我們注意到了它對于Electrolysis的進一步改進。Electrolysis是Mozilla實現在后臺進程中呈現和執行web相關…

ModuleNotFoundError: No module named '_ctypes' ERROR:Command errored out with exit status 1: python

Ubuntu下載 nginx 時報錯&#xff1a; ERROR: Command errored out with exit status 1:command: /usr/local/bin/python3.7 -c import sys, setuptools, tokenize; sys.argv[0] ""/tmp/pip-install-7e0xdb36/uwsgi/setup.py""; __file__""/tmp…

python opc plc_PYthon簡易OPC數據采集寫入Access

利用hollias comm opcserver 與Python實現交互。代碼如下&#xff1a;# -*- coding: utf-8 -*-from sys import *from getopt import *#from os import * 造成f open(test.txt, r) TypeError: an integer is required錯誤import signalimport sysimport osimport typesimport …

邊工作邊刷題:70天一遍leetcode: day 73

Read N Characters Given Read4 I/II 要點&#xff1a;這題的要點就是搞清楚幾個變量的內在邏輯&#xff1a;只有buffer是整4 bytes的。而client要讀的bytes&#xff08;需求&#xff09;和實際上disk上有的bytes&#xff08;供給&#xff09;都是不整的。所以&#xff0c; 循環…

javascript時間戳和日期字符串相互轉換

1 <html xmlns"http://www.w3.org/1999/xhtml">2 <head>3 <meta http-equiv"Content-Type" content"text/html; charsetutf-8" />4 <script type"text/javascript">5 // 獲取當前時間戳(以s為單位)6 var time…

wireshark 十六進制過濾_CTF流量分析之wireshark使用

01.基本介紹在CTF比賽中&#xff0c;對于流量包的分析取證是一種十分重要的題型。通常這類題目都是會提供一個包含流量數據的pcap文件&#xff0c;參賽選手通過該文件篩選和過濾其中無關的流量信息&#xff0c;根據關鍵流量信息找出flag或者相關線索。pcap流量包的分析通常都是…

vim 插件管理

1  進入自己的vim mkdir ./bundle/vundle 2  在vimrc同級中執行 git clone https://github.com/gmarik/vundle.git ./bundle/vundle 將一些插件文件 下載到./bundle/vundle中 3  編寫自己的vim配置&#xff0c;其實很簡單 set nocompatible " be iMp…

ubuntu install wiznote

sudo add-apt-repository ppa:wiznote-team #添加官方源sudo apt-get update #更新源sudo apt-get install wiznote #安裝為知筆記

python 對象序列化 pickling_python操作文件——序列化pickling和JSON

當我們在內存中定義一個dict的時候&#xff0c;我們是可以隨時修改變量的內容的&#xff1a;>>> ddict(namewc,age28)>>>d{name: wc, age: 28}我們可以隨時修改name和age的值。但是當我們重新運行程序的時候&#xff0c;name、age的初始化值還是wc和28&#…

python實現Redis訂閱發布

Redis 發布訂閱 Redis 發布訂閱可以用在像消息通知&#xff0c;群聊&#xff0c;定向推送&#xff0c;參數刷新加載等業務場景 發布訂閱模型有三個角色&#xff1a; 發布者&#xff08;Publisher&#xff09;訂閱者(Subscriber)頻道(channel) 每個訂閱者可以訂閱多個頻道&am…

nfs的快速部署

1、nfs內核自帶協議模塊不用安裝&#xff0c;如果沒有yum安裝yum -y install nfs-utils2、配置vim /etc/exports #配置文件 此文件一般為空&#xff0c;編寫格式為&#xff1a; /share/word 192.168.31.254(rw) 192.168.31.252(ro) # 共享/share/word目錄給192.168.…

redistemplate怎么修改數據_如何使用RedisTemplate訪問Redis數據結構?

在springboot項目中&#xff0c;集成各種框架變得非常容易。下面簡單介紹一下如何在springboot項目中集成單機模式redis。集群模式也差不多&#xff0c;這里就不過多介紹了。首先你得安裝redis服務&#xff0c;無論在linux還是windows系統上。如果沒有安裝&#xff0c;請自行百…

HyperLogLog原理與在Redis中的使用

Redis-HyperLogLog 基于HyperLogLog算法&#xff0c;使用極小的空間完成巨量運算 Redis 中HyperLogLog 基本使用 常用命令 PFADD key element [element …]: 將任意數量的元素添加到指定的 HyperLogLog 里面。PFCOUNT key [key …]: 計算hyperloglog的獨立總數prmerge destk…

iOS開發UI篇—xib的簡單使用

一、簡單介紹 xib和storyboard的比較&#xff0c;一個輕量級一個重量級。 共同點&#xff1a; 都用來描述軟件界面 都用Interface Builder工具來編輯 不同點: Xib是輕量級的&#xff0c;用來描述局部的UI界面 Storyboard是重量級的&#xff0c;用來描述整個軟件的多個界面&…

【云棲計算之旅】線下沙龍第2期精彩預告:Docker在云平臺上的最佳實踐

Docker是一個開源的應用容器引擎&#xff0c;提供了一種在安全、可重復的環境中自動部署軟件的方式&#xff0c;允許開發者將他們的應用和依賴包打包到一個可移植的容器中&#xff0c;然后發布到任何流行的Linux機器上&#xff0c;也可以實現虛擬化。容器完全使用沙箱機制&…

小程序mpvue圖片繪制水印_開發筆記:使用 mpvue 開發斗圖小程序

之前用過 wepy 框架寫了個小程序 GitHub - yshkk/shanbay-mina: 基于 wepy 框架的 “扇貝閱讀” 微信小程序 &#xff0c;感覺寫法上類似 vue&#xff0c;但不那么徹底。現在美團點評發布的 mpvue 支持開發者可以用 vue 的語法開發微信小程序&#xff0c;正好有強需求需要一個斗…

mysql int類型的長度值

整數類型的存儲和范圍(來自mysql手冊) 類型字節最小值最大值(帶符號的/無符號的)(帶符號的/無符號的)TINYINT1-1281270255SMALLINT2-3276832767065535MEDIUMINT3-83886088388607016777215INT4-2147483648214748364704294967295BIGINT8-92233720368547758089223372036854775807…

龍王我當定了(一個在QQ刷龍王的腳本)

自從學了python&#xff0c;龍王再也沒丟過&#xff0c;就是經常被打, QQ 和 TIM 都可以&#xff0c;發送時要把聊天窗口打開。 # 如果import報錯&#xff0c;那可以pip下載這幾個模塊試一試 import win32gui import win32con import win32clipboard as w import random from…