分布式鎖是一種用于在分布式系統(tǒng)中控制對(duì)共享資源的訪問(wèn)的鎖。它與傳統(tǒng)的單機(jī)鎖不同,因?yàn)樗枰诙鄠€(gè)節(jié)點(diǎn)之間協(xié)調(diào)以確保互斥訪問(wèn)。
本文將介紹什么是分布式鎖,以及使用redis實(shí)現(xiàn)分布式鎖的幾種方案。
前言
了解分布式鎖之前,需要先了解一下
- 線程鎖
- 進(jìn)程鎖
- CAP理論
線程鎖
線程鎖主要用來(lái)給方法、代碼塊加鎖。
當(dāng)某個(gè)方法或代碼使用鎖,在同一時(shí)刻僅有一個(gè)線程執(zhí)行該方法或該代碼段。
線程鎖只在同一JVM中有效果,因?yàn)榫€程鎖的實(shí)現(xiàn),是通過(guò)線程之間共享內(nèi)存實(shí)現(xiàn)的,
一般實(shí)現(xiàn)方法:
- Synchronized
- Lock
進(jìn)程鎖
進(jìn)程鎖是控制同一操作系統(tǒng)中多個(gè)進(jìn)程訪問(wèn)某個(gè)共享資源
進(jìn)程具有獨(dú)立性,各個(gè)進(jìn)程無(wú)法訪問(wèn)其他進(jìn)程的資源,因此無(wú)法通過(guò)synchronized等線程鎖實(shí)現(xiàn)進(jìn)程鎖。
CAP理論
任何一個(gè)分布式系統(tǒng)都無(wú)法同時(shí)滿(mǎn)足
- 一致性(Consistency)
- 可用性(AvAIlability)
- 分區(qū)容錯(cuò)性(Partition tolerance)
最多只能同時(shí)滿(mǎn)足兩項(xiàng)。
分布式鎖
概念
如果不同的系統(tǒng)或同一個(gè)系統(tǒng)的不同主機(jī)之間共享了某個(gè)臨界資源,往往需要互斥來(lái)防止彼此干擾,以保證一致性,就產(chǎn)生了分布式鎖。包含三個(gè)要素:
- 分布式系統(tǒng)
- 不同進(jìn)程
- 共同訪問(wèn)共享資源
分布式鎖,實(shí)現(xiàn)的是CA,即一致性和可用性
特性
- 互斥性: 任意時(shí)刻,只有一個(gè)客戶(hù)端能持有鎖。
- 鎖超時(shí)釋放:持有鎖超時(shí),可以釋放,防止不必要的資源浪費(fèi),也可以防止死鎖。
- 可重入性:一個(gè)線程如果獲取了鎖之后,可以再次對(duì)其請(qǐng)求加鎖。
- 高性能和高可用:加鎖和解鎖需要開(kāi)銷(xiāo)盡可能低,同時(shí)也要保證高可用,避免分布式鎖失效。
- 安全性:鎖只能被持有的客戶(hù)端刪除,不能被其他客戶(hù)端刪除。
實(shí)現(xiàn)方案
Redisson框架
框架介紹
Redisson是一款基于JAVA的Redis客戶(hù)端,它封裝了Redis的Java客戶(hù)端Jedis、Lettuce等,并且提供了許多額外的功能,例如分布式鎖、分布式集合、分布式對(duì)象、布隆過(guò)濾器等。
框架特點(diǎn)
- 提供了豐富的API,簡(jiǎn)單易用。
- 提供了多種數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn),如分布式鎖、分布式集合、分布式Map、分布式Queue等。
- 支持多種Redis部署方式,如單節(jié)點(diǎn)、主從、哨兵、集群等。
- 提供了基?.NETty的高性能的Redis連接池。
- 提供了基于Ramp模型的分布式遠(yuǎn)程調(diào)用框架,可以方便的進(jìn)行分布式服務(wù)調(diào)用。
簡(jiǎn)單示例
- 引入Redisson的依賴(lài)
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
- 創(chuàng)建RedissonClient對(duì)象
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
- 使用RedissonClient對(duì)象進(jìn)行數(shù)據(jù)操作
// 獲取字符串對(duì)象
RBucket<String> bucket = redissonClient.getBucket("myKey");
bucket.set("myValue");
// 獲取Map對(duì)象
RMap<String, String> map = redissonClient.getMap("myMap");
map.put("key1", "value1");
// 獲取分布式鎖對(duì)象
RLock lock = redissonClient.getLock("myLock");
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
基于SETNX命令實(shí)現(xiàn)
通過(guò)使用Redis中的SETNX命令(即SET if Not eXists),可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分布式鎖。
SETNX命令是Redis中的一種原子性操作,用于將一個(gè)鍵值對(duì)(key-value)設(shè)置到Redis中,僅在鍵不存在時(shí)才會(huì)設(shè)置成功,否則設(shè)置失敗。利用SETNX命令的特性,可以實(shí)現(xiàn)分布式鎖的機(jī)制,具體步驟如下:
- 設(shè)置鎖:在Redis中設(shè)置一個(gè)鍵值對(duì),鍵為鎖名稱(chēng),值為一個(gè)隨機(jī)生成的字符串,同時(shí)設(shè)置過(guò)期時(shí)間(防止鎖一直存在,導(dǎo)致死鎖)。可以使用以下Redis命令:
SETNX lock_name random_value
EXPIRE lock_name expire_time
- 獲取鎖:如果SETNX命令返回1,則說(shuō)明鎖設(shè)置成功,此時(shí)獲取到了鎖;如果返回0,則說(shuō)明鎖已經(jīng)被其他節(jié)點(diǎn)持有,此時(shí)需要等待一段時(shí)間后重試獲取鎖。
- 釋放鎖:釋放鎖時(shí),需要先判斷當(dāng)前線程持有的鎖是否與之前設(shè)置的鎖名稱(chēng)和值相同,如果相同,則通過(guò)DEL命令刪除該鍵值對(duì),釋放鎖。
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
基于RedLock實(shí)現(xiàn)
RedLock是一個(gè)多節(jié)點(diǎn)分布式鎖算法,它基于Redis和一些簡(jiǎn)單的算法來(lái)實(shí)現(xiàn)高可用的分布式鎖。
與傳統(tǒng)的Redis分布式鎖方案相比,RedLock可以更好地應(yīng)對(duì)網(wǎng)絡(luò)故障和硬件故障等異常情況,提高系統(tǒng)的可用性和穩(wěn)定性。
RedLock算法的基本思想是:將鎖的持有和釋放過(guò)程轉(zhuǎn)化為一個(gè)競(jìng)爭(zhēng)資源的問(wèn)題,通過(guò)多節(jié)點(diǎn)協(xié)作的方式來(lái)實(shí)現(xiàn)鎖的分配和釋放。
具體步驟如下:
- 對(duì)于要加鎖的資源,計(jì)算出一個(gè)唯一的標(biāo)識(shí)(比如使用hash函數(shù)將資源名稱(chēng)轉(zhuǎn)化為一個(gè)32位整數(shù)),作為鎖的名稱(chēng)。
- 獲取多個(gè)Redis節(jié)點(diǎn)的當(dāng)前時(shí)間戳,并計(jì)算出一個(gè)時(shí)鐘偏差(clock drift)。時(shí)鐘偏差可以通過(guò)取多個(gè)Redis節(jié)點(diǎn)的時(shí)間戳的平均值來(lái)計(jì)算。這樣可以避免不同Redis節(jié)點(diǎn)之間的時(shí)間不同步而導(dǎo)致的鎖沖突問(wèn)題。
- 獲取鎖:對(duì)于每個(gè)Redis節(jié)點(diǎn),嘗試通過(guò)SET命令獲取鎖。如果獲取鎖成功,則記錄鎖的名稱(chēng)、鎖的值(一個(gè)隨機(jī)字符串)、過(guò)期時(shí)間以及Redis節(jié)點(diǎn)的標(biāo)識(shí)信息(比如IP地址和端口號(hào))。如果獲取鎖失敗,則記錄失敗的節(jié)點(diǎn)信息。
- 判斷獲取鎖的結(jié)果:統(tǒng)計(jì)獲取鎖成功的節(jié)點(diǎn)數(shù),并根據(jù)Quorum算法(投票算法)來(lái)判斷是否獲取鎖成功。如果獲取鎖成功的節(jié)點(diǎn)數(shù)大于等于N/2+1(其中N為Redis節(jié)點(diǎn)數(shù)),則表示鎖獲取成功;否則,表示鎖獲取失敗。
- 執(zhí)行結(jié)果:如果鎖獲取成功,則執(zhí)行相應(yīng)的業(yè)務(wù)邏輯;如果鎖獲取失敗,則需要嘗試在所有失敗的節(jié)點(diǎn)中找到一個(gè)最新的鎖并釋放它,以避免死鎖問(wèn)題。
- 釋放鎖:釋放鎖時(shí),需要根據(jù)鎖的名稱(chēng)和值來(lái)判斷當(dāng)前節(jié)點(diǎn)是否持有該鎖。如果當(dāng)前節(jié)點(diǎn)持有該鎖,則通過(guò)DEL命令刪除該鍵值對(duì),釋放鎖。
需要注意的是,RedLock算法并不能保證絕對(duì)的可用性和正確性,仍然可能存在某些特殊情況下的鎖沖突問(wèn)題。
因此,在實(shí)際應(yīng)用中,需要根據(jù)具體業(yè)務(wù)場(chǎng)景和需求來(lái)選擇適合的分布式鎖方案,并進(jìn)行充分的測(cè)試和優(yōu)化。
基于Lua腳本實(shí)現(xiàn)
在Redis中可以使用Lua腳本來(lái)實(shí)現(xiàn)分布式鎖,其基本思想是通過(guò)原子操作將鎖的獲取和釋放過(guò)程合并為一個(gè)操作,保證鎖的原子性和一致性。
使用Lua腳本可以在Redis中實(shí)現(xiàn)一個(gè)基于SET命令的分布式鎖,具體實(shí)現(xiàn)步驟如下:
- 生成一個(gè)隨機(jī)字符串作為鎖的值,以確保不同的客戶(hù)端使用的鎖值不同。
- 使用SET命令將鎖名作為key,鎖值作為value,過(guò)期時(shí)間作為expire參數(shù)來(lái)設(shè)置鎖,加上NX(Not eXist)選項(xiàng),只有當(dāng)key不存在時(shí)才設(shè)置成功。
- 在Lua腳本中使用eval命令執(zhí)行以下腳本:
if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
return 1
else
return 0
end
其中,KEYS[1]表示鎖的名稱(chēng),ARGV[1]表示鎖的值,ARGV[2]表示鎖的過(guò)期時(shí)間。
- 結(jié)果:如果eval命令返回1,則表示獲取鎖成功;如果返回0,則表示獲取鎖失敗。
- 釋放鎖時(shí),可以使用DEL命令刪除鎖的名稱(chēng)即可。
下面是一個(gè)完整的Lua例子:
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
return 0
end
-- 釋放鎖
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
上面的代碼包括兩個(gè)部分:獲取鎖和釋放鎖。
- 獲取鎖:使用setnx命令來(lái)嘗試獲取鎖。如果獲取成功,則設(shè)置鎖的過(guò)期時(shí)間,并返回1表示獲取鎖成功;否則,返回0表示獲取鎖失敗。
- 釋放鎖:先通過(guò)get命令獲取鎖的值,判斷當(dāng)前節(jié)點(diǎn)是否持有該鎖。如果持有,則使用del命令刪除該鍵值對(duì)并返回1表示釋放鎖成功;否則,返回0表示釋放鎖失敗。
總結(jié)
上面提到的通過(guò)Redis實(shí)現(xiàn)的分布式鎖幾種方案,在高并發(fā)的情況下,可能存在鎖沖突的問(wèn)題,因此需要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景來(lái)選擇適合的鎖方案,并進(jìn)行充分的測(cè)試和優(yōu)化。






