基于Redis實(shí)現(xiàn)分布式鎖的三種方式
引言
在現(xiàn)代分布式系統(tǒng)中,多個(gè)節(jié)點(diǎn)同時(shí)操作共享資源是常見(jiàn)的情況。這種并發(fā)訪(fǎng)問(wèn)如果不加以控制,可能會(huì)導(dǎo)致數(shù)據(jù)不一致、業(yè)務(wù)異常等問(wèn)題。因此,分布式鎖成為了確保分布式系統(tǒng)中各節(jié)點(diǎn)協(xié)調(diào)一致、避免資源沖突的一個(gè)重要工具。本文將介紹三種常用的分布式鎖實(shí)現(xiàn)方式:基于 Redis 的 SETNX 鎖實(shí)現(xiàn)、基于 Redisson 實(shí)現(xiàn)的分布式鎖以及 使用 Redis Lua 腳本的分布式鎖實(shí)現(xiàn)。
什么是分布式鎖?
分布式鎖是一種在分布式系統(tǒng)中,確保同一時(shí)刻只有一個(gè)節(jié)點(diǎn)能夠訪(fǎng)問(wèn)共享資源的機(jī)制。與傳統(tǒng)的單機(jī)鎖(如 synchronized)不同,分布式鎖跨越多個(gè)機(jī)器、節(jié)點(diǎn),通過(guò)一個(gè)外部協(xié)調(diào)者來(lái)管理鎖。常見(jiàn)的分布式鎖實(shí)現(xiàn)工具包括 Redis、ZooKeeper、Consul 等。
分布式鎖廣泛應(yīng)用于以下場(chǎng)景:
- 限流控制:防止多個(gè)請(qǐng)求同時(shí)修改同一資源。
- 全局任務(wù)調(diào)度:在多個(gè)節(jié)點(diǎn)中確保只有一個(gè)節(jié)點(diǎn)執(zhí)行特定的任務(wù)。
- 防止重復(fù)處理:確保同一操作不會(huì)被多個(gè)節(jié)點(diǎn)重復(fù)執(zhí)行。
- 分布式唯一性保證:如生成全局唯一 ID。
1. 基于 RedisSETNX實(shí)現(xiàn)分布式鎖
Redis 是最常見(jiàn)的分布式鎖實(shí)現(xiàn)工具之一。我們可以通過(guò) Redis 的 SETNX 命令來(lái)實(shí)現(xiàn)一個(gè)基本的分布式鎖。SETNX 命令的作用是 只有在鍵不存在時(shí)設(shè)置該鍵的值,因此可以用來(lái)確保在同一時(shí)刻只有一個(gè)節(jié)點(diǎn)能夠獲取鎖。
實(shí)現(xiàn)步驟:
- 使用
SETNX命令嘗試獲取鎖(設(shè)置某個(gè)鍵值對(duì))。 - 如果獲取成功,表示該節(jié)點(diǎn)獲得了鎖,可以進(jìn)行后續(xù)操作。
- 如果獲取失敗,表示鎖已被其他節(jié)點(diǎn)占用,當(dāng)前節(jié)點(diǎn)需要等待或重試。
- 設(shè)置鎖的過(guò)期時(shí)間,防止死鎖的發(fā)生。
基于SETNX實(shí)現(xiàn)代碼(Java)
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisDistributedLock {
private static final String LOCK_KEY = "lock:resource"; // 鎖的唯一標(biāo)識(shí)
private static final int EXPIRE_TIME = 10; // 鎖的超時(shí)時(shí)間,單位:秒
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private Jedis jedis;
public RedisDistributedLock() {
jedis = new Jedis(REDIS_HOST, REDIS_PORT);
}
// 獲取鎖
public boolean acquireLock() {
String lockValue = UUID.randomUUID().toString(); // 唯一的鎖值,防止鎖被誤釋放
// 使用 SETNX 命令獲取鎖
Long result = jedis.setnx(LOCK_KEY, lockValue);
if (result == 1) {
// 鎖獲取成功,設(shè)置過(guò)期時(shí)間
jedis.expire(LOCK_KEY, EXPIRE_TIME);
return true;
}
// 鎖未獲取成功
return false;
}
// 釋放鎖
public boolean releaseLock() {
String lockValue = jedis.get(LOCK_KEY);
if (lockValue != null && lockValue.equals(jedis.get(LOCK_KEY))) {
// 確保當(dāng)前鎖是自己持有的
jedis.del(LOCK_KEY);
return true;
}
return false;
}
// 關(guān)閉連接
public void close() {
jedis.close();
}
public static void main(String[] args) {
RedisDistributedLock lock = new RedisDistributedLock();
// 嘗試獲取鎖
if (lock.acquireLock()) {
try {
System.out.println("Lock acquired, performing task.");
// 執(zhí)行任務(wù)...
Thread.sleep(5000); // 模擬任務(wù)處理時(shí)間
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.releaseLock(); // 完成任務(wù)后釋放鎖
System.out.println("Lock released.");
}
} else {
System.out.println("Failed to acquire lock, try again later.");
}
lock.close();
}
}關(guān)鍵點(diǎn):
SETNX確保只有一個(gè)節(jié)點(diǎn)能夠獲取到鎖。- 鎖設(shè)置了過(guò)期時(shí)間,避免由于異常導(dǎo)致的死鎖。
del只有在確認(rèn)當(dāng)前持有鎖的客戶(hù)端才會(huì)釋放鎖。
2. 使用 Redisson 實(shí)現(xiàn)分布式鎖
Redisson 是基于 Redis 提供的高層次 Java 客戶(hù)端,它簡(jiǎn)化了分布式鎖的實(shí)現(xiàn)。Redisson 提供了 RLock 接口來(lái)管理分布式鎖,使得開(kāi)發(fā)者無(wú)需手動(dòng)處理底層的細(xì)節(jié)。
Redisson 的優(yōu)勢(shì)在于其高效性和易用性,能夠自動(dòng)處理鎖的過(guò)期、重試、續(xù)期等功能。
Redisson 分布式鎖代碼示例
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.Redisson;
import org.redisson.config.Config;
public class RedissonLockExample {
public static void main(String[] args) {
// 配置 Redisson 客戶(hù)端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 設(shè)置 Redis 地址
RedissonClient redisson = Redisson.create(config); // 創(chuàng)建 Redisson 客戶(hù)端
// 獲取分布式鎖
RLock lock = redisson.getLock("lock:resource");
try {
// 嘗試獲取鎖
if (lock.tryLock()) {
System.out.println("Lock acquired, performing task.");
// 執(zhí)行任務(wù)...
Thread.sleep(5000); // 模擬任務(wù)處理
} else {
System.out.println("Failed to acquire lock, try again later.");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
redisson.shutdown(); // 關(guān)閉 Redisson 客戶(hù)端
System.out.println("Lock released.");
}
}
}關(guān)鍵點(diǎn):
RLock是 Redisson 提供的分布式鎖對(duì)象。tryLock()方法可以用來(lái)嘗試獲取鎖,獲取成功返回true,否則返回false。- Redisson 自動(dòng)處理了鎖的超時(shí)和重試等邏輯,避免手動(dòng)管理鎖狀態(tài)。
3. 使用 Lua 腳本實(shí)現(xiàn)分布式鎖
在 Redis 中,Lua 腳本能夠確保多條 Redis 命令的原子性執(zhí)行。通過(guò) Lua 腳本,我們可以把獲取鎖、設(shè)置過(guò)期時(shí)間和釋放鎖等操作合并成一個(gè)原子操作,避免了競(jìng)態(tài)條件問(wèn)題。
import redis.clients.jedis.Jedis;
public class RedisDistributedLockWithLua {
private static final String LOCK_KEY = "lock:resource";
private static final int EXPIRE_TIME = 10; // 鎖的超時(shí)時(shí)間,單位:秒
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private Jedis jedis;
public RedisDistributedLockWithLua() {
jedis = new Jedis(REDIS_HOST, REDIS_PORT);
}
// 獲取鎖
public boolean acquireLock(String lockValue) {
// Lua 腳本:嘗試獲取鎖,如果成功則設(shè)置鎖的過(guò)期時(shí)間
String script =
"if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then " +
" redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
// 使用 EVAL 命令執(zhí)行 Lua 腳本
Object result = jedis.eval(script, 1, LOCK_KEY, lockValue, String.valueOf(EXPIRE_TIME));
return "1".equals(result.toString());
}
// 釋放鎖
public boolean releaseLock(String lockValue) {
// Lua 腳本:確保當(dāng)前鎖的持有者才會(huì)釋放鎖
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
// 使用 EVAL 命令執(zhí)行 Lua 腳本
Object result = jedis.eval(script, 1, LOCK_KEY, lockValue);
return "1".equals(result.toString());
}
// 關(guān)閉連接
public void close() {
jedis.close();
}
public static void main(String[] args) {
RedisDistributedLockWithLua lock = new RedisDistributedLockWithLua();
String lockValue = "unique-lock-value"; // 唯一的鎖值,用于標(biāo)識(shí)當(dāng)前鎖的擁有者
// 嘗試獲取鎖
if (lock.acquireLock(lockValue)) {
try {
System.out.println("Lock acquired, performing task.");
// 執(zhí)行任務(wù)...
Thread.sleep(5000); // 模擬任務(wù)處理
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.releaseLock(lockValue); // 完成任務(wù)后釋放鎖
System.out.println("Lock released.");
}
} else {
System.out.println("Failed to acquire lock, try again later.");
}
lock.close();
}
}解釋 Lua 腳本:
- 獲取鎖:
SETNX命令保證只有一個(gè)客戶(hù)端能成功設(shè)置鎖,同時(shí)使用EXPIRE設(shè)置鎖的過(guò)期時(shí)間。 - 釋放鎖:通過(guò) Lua 腳本確認(rèn)當(dāng)前客戶(hù)端持有鎖,防止其他客戶(hù)端誤釋放鎖。
對(duì)比三種 Redis 分布式鎖實(shí)現(xiàn)方式
在分布式系統(tǒng)中,我們可以通過(guò) Redis 實(shí)現(xiàn)分布式鎖,保證多個(gè)節(jié)點(diǎn)在訪(fǎng)問(wèn)共享資源時(shí)的互斥性。本文介紹了三種常見(jiàn)的分布式鎖實(shí)現(xiàn)方式:基于 Redis SETNX 命令的鎖、基于 Redisson 實(shí)現(xiàn)的鎖、以及 基于 Redis Lua 腳本的鎖。每種實(shí)現(xiàn)方式有其優(yōu)缺點(diǎn),根據(jù)系統(tǒng)的不同需求,開(kāi)發(fā)者可以選擇合適的方式。接下來(lái),我們將對(duì)這三種方式進(jìn)行對(duì)比,幫助大家做出更明智的選擇。
1. 基于 RedisSETNX命令實(shí)現(xiàn)分布式鎖
實(shí)現(xiàn)原理:
使用 Redis 的 SETNX(SET if Not eXists)命令來(lái)設(shè)置一個(gè)唯一的鎖鍵,如果該鍵不存在,表示成功獲取鎖;如果該鍵已經(jīng)存在,表示鎖已被占用。然后,使用 EXPIRE 命令為鎖設(shè)置過(guò)期時(shí)間,防止死鎖。
優(yōu)點(diǎn):
- 簡(jiǎn)單易懂:實(shí)現(xiàn)思路簡(jiǎn)單,代碼量少。
- 高效:由于 Redis 是單線(xiàn)程的,
SETNX和EXPIRE操作是原子的,基本可以保證鎖的正確性。 - 無(wú)依賴(lài):只需要一個(gè)基礎(chǔ)的 Redis 客戶(hù)端(如 Jedis 或 Lettuce)即可,不需要引入額外的庫(kù)。
缺點(diǎn):
- 操作不原子:盡管
SETNX本身是原子操作,但獲取鎖和設(shè)置過(guò)期時(shí)間是兩次獨(dú)立的操作,存在被中斷的風(fēng)險(xiǎn)(例如網(wǎng)絡(luò)延遲、Redis 節(jié)點(diǎn)重啟等)。 - 不支持鎖續(xù)期:當(dāng)任務(wù)執(zhí)行時(shí)間長(zhǎng)時(shí),不支持鎖續(xù)期:當(dāng)任務(wù)執(zhí)行時(shí)間長(zhǎng)時(shí),鎖過(guò)期可能導(dǎo)致其他節(jié)點(diǎn)誤獲取鎖,進(jìn)而引發(fā)資源競(jìng)爭(zhēng)、數(shù)據(jù)不一致等問(wèn)題。沒(méi)有機(jī)制來(lái)自動(dòng)續(xù)期鎖。
- 可能的競(jìng)態(tài)條件:由于
SETNX和EXPIRE是分開(kāi)的操作,若在獲取鎖后未及時(shí)設(shè)置過(guò)期時(shí)間,可能導(dǎo)致鎖在任務(wù)執(zhí)行完之前過(guò)期,進(jìn)而導(dǎo)致其他客戶(hù)端誤獲取鎖,造成資源競(jìng)爭(zhēng)或數(shù)據(jù)不一致。
2. 使用 Redisson 實(shí)現(xiàn)分布式鎖
實(shí)現(xiàn)原理:
Redisson 是基于 Redis 提供的高層次 Java 客戶(hù)端,它通過(guò) RLock 接口來(lái)管理分布式鎖。Redisson 提供了更高層次的 API,自動(dòng)處理鎖的超時(shí)、重試、續(xù)期等問(wèn)題。
優(yōu)點(diǎn):
- 易用性高:Redisson 提供了豐富的分布式鎖接口,開(kāi)發(fā)者只需要關(guān)心鎖的獲取和釋放,而無(wú)需關(guān)心底層細(xì)節(jié)。
- 自動(dòng)續(xù)期:Redisson 支持自動(dòng)續(xù)期功能,如果任務(wù)執(zhí)行時(shí)間超過(guò)鎖的過(guò)期時(shí)間,Redisson 會(huì)自動(dòng)延長(zhǎng)鎖的生存時(shí)間,避免鎖過(guò)期導(dǎo)致的死鎖。
- 高效與可靠性:Redisson 通過(guò) Redis 實(shí)現(xiàn)高效的分布式鎖,且內(nèi)置了重試機(jī)制,適應(yīng)高并發(fā)場(chǎng)景。
- 功能豐富:除了鎖,Redisson 還提供了分布式集合、分布式隊(duì)列等數(shù)據(jù)結(jié)構(gòu),適用于更多場(chǎng)景。
缺點(diǎn):
- 依賴(lài) Redisson:需要額外引入 Redisson 庫(kù),增加了項(xiàng)目的復(fù)雜度和依賴(lài)。
- 性能損耗:雖然 Redisson 提供了很高的抽象,但在某些場(chǎng)景下,其封裝的操作可能會(huì)引入額外的性能損耗。
- 限制于 Java:Redisson 是為 Java 提供的客戶(hù)端,不適合其他語(yǔ)言的開(kāi)發(fā)者使用。
3. 使用 Redis Lua 腳本實(shí)現(xiàn)分布式鎖
實(shí)現(xiàn)原理:
Lua 腳本可以在 Redis 服務(wù)器端原子地執(zhí)行多個(gè)命令。通過(guò) Lua 腳本,我們將獲取鎖、設(shè)置過(guò)期時(shí)間和釋放鎖的操作合并為一個(gè)原子操作,避免了競(jìng)爭(zhēng)條件的發(fā)生。
優(yōu)點(diǎn):
- 原子性:Lua 腳本在 Redis 服務(wù)器端執(zhí)行,避免了在客戶(hù)端與 Redis 之間的多次往返操作,確保鎖的獲取和過(guò)期時(shí)間的設(shè)置是原子性的。
- 避免競(jìng)態(tài)條件:通過(guò) Lua 腳本,獲取鎖和設(shè)置過(guò)期時(shí)間操作合并為一個(gè)原子操作,避免了競(jìng)態(tài)條件問(wèn)題。
- 減少網(wǎng)絡(luò)開(kāi)銷(xiāo):通過(guò) Lua 腳本將多個(gè) Redis 操作合并為一個(gè)操作,減少了網(wǎng)絡(luò)延遲,提升了性能。
缺點(diǎn):
- 代碼復(fù)雜:相比于
SETNX命令和 Redisson,Lua 腳本需要編寫(xiě)和調(diào)試,開(kāi)發(fā)者需要了解 Lua 腳本的語(yǔ)法和 Redis 的命令。 - 可讀性差:Lua 腳本在 Redis 上執(zhí)行,不如 Redisson 這樣的高級(jí) API 直觀,調(diào)試和維護(hù)相對(duì)困難。
- 錯(cuò)誤處理復(fù)雜:Lua 腳本會(huì)在 Redis 服務(wù)器端執(zhí)行,錯(cuò)誤處理較為復(fù)雜。如果出現(xiàn)腳本執(zhí)行錯(cuò)誤,排查會(huì)更麻煩。
三者對(duì)比
| 特性 | SETNX 鎖實(shí)現(xiàn) | Redisson 鎖實(shí)現(xiàn) | Lua 腳本鎖實(shí)現(xiàn) |
|---|---|---|---|
| 實(shí)現(xiàn)復(fù)雜度 | 簡(jiǎn)單 | 簡(jiǎn)單,依賴(lài) Redisson | 稍復(fù)雜,需編寫(xiě) Lua 腳本 |
| 原子性 | 低(需要分兩步操作) | 高(自動(dòng)處理鎖的生命周期) | 高(所有操作在 Redis 上原子執(zhí)行) |
| 支持鎖續(xù)期 | 否 | 是 | 否(需手動(dòng)在腳本中實(shí)現(xiàn)續(xù)期) |
| 鎖釋放保障 | 需要手動(dòng)確認(rèn)鎖值 | 自動(dòng)釋放,且可靠性高 | 需要手動(dòng)確認(rèn)鎖值 |
| 性能 | 較高 | 較高,但有額外封裝性能開(kāi)銷(xiāo) | 非常高(減少了網(wǎng)絡(luò)延遲) |
| 依賴(lài) | 僅需 Redis 客戶(hù)端 | 需要引入 Redisson 庫(kù) | 僅需 Redis 客戶(hù)端,使用 Lua 腳本 |
| 適用場(chǎng)景 | 簡(jiǎn)單場(chǎng)景,低并發(fā)需求 | 高并發(fā),自動(dòng)續(xù)期,可靠性需求 | 高性能、低延遲需求,且能接受腳本復(fù)雜性 |
| 錯(cuò)誤處理 | 容易處理 | 易于使用且錯(cuò)誤處理簡(jiǎn)潔 | 錯(cuò)誤處理較為復(fù)雜 |
總結(jié)
根據(jù)實(shí)際需求,開(kāi)發(fā)者可以選擇不同的分布式鎖實(shí)現(xiàn)方式:
- 基于
SETNX的實(shí)現(xiàn)適合于簡(jiǎn)單的場(chǎng)景,且對(duì)性能要求較高時(shí),可以快速實(shí)現(xiàn)鎖功能,但存在較低的原子性和不支持續(xù)期的缺點(diǎn)。 - Redisson 實(shí)現(xiàn)適合需要高并發(fā)和高可用的應(yīng)用,能夠自動(dòng)續(xù)期、處理復(fù)雜的分布式鎖需求,且易于使用。但會(huì)增加外部依賴(lài)。
- Lua 腳本實(shí)現(xiàn)適合于對(duì)性能要求極高、需要保證原子性和減少網(wǎng)絡(luò)延遲的場(chǎng)景,且不依賴(lài)外部庫(kù),但需要一定的 Lua 腳本能力,開(kāi)發(fā)復(fù)雜度較高。
選擇合適的分布式鎖實(shí)現(xiàn)方式,能夠有效保證系統(tǒng)的一致性和高可用性。
以上就是基于Redis實(shí)現(xiàn)分布式鎖的三種方式的詳細(xì)內(nèi)容,更多關(guān)于Redis分布式鎖實(shí)現(xiàn)方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
window環(huán)境redis通過(guò)AOF恢復(fù)數(shù)據(jù)的方法
這篇文章主要介紹了window環(huán)境redis通過(guò)AOF恢復(fù)數(shù)據(jù)的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
redis分布式鎖的go-redis實(shí)現(xiàn)方法詳解
這篇文章主要介紹了redis分布式鎖的go-redis實(shí)現(xiàn)方法,本文給大家介紹的非常詳細(xì)對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12
Redis如何在項(xiàng)目中合理使用經(jīng)驗(yàn)分享
這篇文章主要給大家介紹了關(guān)于Redis如何在項(xiàng)目中合理使用的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Redis?key的過(guò)期時(shí)間和永久有效的實(shí)現(xiàn)
在Redis中,鍵可以設(shè)置過(guò)期時(shí)間或被永久保存,`EXPIRE`和`PEXPIRE`命令分別用于設(shè)置鍵的過(guò)期時(shí)間,具有一定的參考價(jià)值,感興趣的可以了解一下2024-09-09

