目錄
一、為什么需要分布式鎖?
二、Redis分布式鎖核心特性
三、實現方案與代碼詳解
方案1:基礎版 SETNX + EXPIRE
原理
代碼示例
問題
方案2:Redisson框架(生產推薦)
核心特性
代碼示例
優勢
方案3:RedLock算法(Redis集群)
適用場景
實現步驟
代碼片段
方案4:Lua腳本原子化操作
解決基礎版問題
Lua腳本示例
Java調用方式
四、高級場景與解決方案
場景1:公平鎖(按順序獲取)
實現思路
代碼邏輯
場景2:可重入鎖
實現機制
Redisson實現
五、避坑指南與最佳實踐
1. 鎖誤刪問題
2. 鎖續期問題
3. 主從一致性問題
4. 性能優化
六、總結與選型建議
一、為什么需要分布式鎖?
在微服務、多機部署場景中,多個進程可能同時競爭同一資源(如庫存、訂單)。傳統JVM鎖僅作用于本地,無法保證分布式環境的互斥訪問。例如:
- 電商庫存超賣:兩個服務實例同時查詢庫存>0,均執行扣減操作。
- 定時任務重疊:多節點觸發相同任務(如對賬),導致數據混亂。
Redis憑借高性能、原子操作和豐富數據結構,成為分布式鎖的首選方案。
二、Redis分布式鎖核心特性
- 互斥性:僅一個客戶端能持有鎖
- 防死鎖:鎖需自動過期(如SET EX)
- 容錯性:節點故障不影響鎖機制
- 可重入性(可選):同一線程可多次加鎖
- 公平性(可選):按申請順序分配鎖
三、實現方案與代碼詳解
方案1:基礎版 SETNX + EXPIRE
原理
SET key value NX EX seconds
:原子設置鍵值并設置過期時間- 若返回?
true
?則獲取鎖,否則重試
代碼示例
String lockKey = "product_stock_lock";
String lockValue = UUID.randomUUID().toString(); // 唯一標識// 嘗試加鎖
Boolean success = jedis.set(lockKey, lockValue, "NX", "EX", 30);
if (success != null && success) {// 獲取鎖成功,執行業務邏輯try {// 扣減庫存操作} finally {// 釋放鎖if (lockValue.equals(jedis.get(lockKey))) {jedis.del(lockKey);}}
} else {// 獲取鎖失敗,重試或返回
}
問題
- 鎖誤刪:業務未完成時鎖過期,其他線程可能刪除當前線程的鎖
- 非原子操作:
set
?和?get
?存在競態條件
方案2:Redisson框架(生產推薦)
核心特性
- 可重入鎖:同一線程可多次加鎖,計數器管理
- 看門狗機制:默認每10秒續期鎖至30秒,防止業務超時
- 異步兼容:支持?
tryLock()
?阻塞等待和?isLocked()
?狀態檢查
代碼示例
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);RLock lock = redisson.getLock("order_lock");
try {// 嘗試加鎖(最多等待10秒,鎖過期30秒)if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {// 處理訂單邏輯} else {log.warn("獲取鎖失敗");}
} catch (InterruptedException e) {Thread.currentThread().interrupt();
} finally {lock.unlock(); // 必須在finally中釋放
}
優勢
- 自動處理鎖續期、可重入、跨進程兼容
- 支持主從切換(通過Redis主從復制)
方案3:RedLock算法(Redis集群)
適用場景
- Redis集群環境(至少3個獨立節點)
- 需容忍半數節點故障仍保持鎖可用
實現步驟
- 向多數節點(N/2+1)發送加鎖請求
- 所有節點設置相同鍵值和過期時間
- 成功加鎖后,鎖有效時間為?
T - 網絡延遲
代碼片段
// 配置多個Redisson客戶端連接不同節點
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://node1:6379");
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://node2:6380");
// 創建RedLock對象
RedissonRedLock redLock = new RedissonRedLock(Redisson.create(config1),Redisson.create(config2)
);// 加鎖邏輯
try {boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);if (locked) {// 業務邏輯}
} finally {redLock.unlock();
}
方案4:Lua腳本原子化操作
解決基礎版問題
- 原子驗證+刪除:通過Lua腳本確保操作原子性
- 唯一標識:用UUID區分鎖歸屬
Lua腳本示例
-- 釋放鎖腳本(判斷鍵值是否匹配)
if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])
elsereturn 0
end
Java調用方式
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, 1, lockKey, lockValue);
四、高級場景與解決方案
場景1:公平鎖(按順序獲取)
實現思路
- 使用?
List
?存儲等待隊列 - 通過?
ZSet
?記錄申請時間戳,優先分配最早請求
代碼邏輯
// 申請鎖時加入隊列
jedis.lpush("lock_queue", clientId);
jedis.zadd("lock_timestamp", System.currentTimeMillis(), clientId);// 檢查隊列頭部是否是自己
String head = jedis.lrange("lock_queue", 0, 0).get(0);
if (head.equals(clientId) && currentTimeMatch(jedis.zscore("lock_timestamp", head))) {// 獲取鎖成功
}
場景2:可重入鎖
實現機制
- 使用?
Hash
?存儲線程ID和重入次數 - 加鎖時遞增計數器,解鎖時遞減(歸零后刪除鎖)
Redisson實現
RLock lock = redisson.getLock("reentrant_lock");
lock.lock(); // 第一次加鎖
// 嵌套調用同一鎖
lock.lock(); // 重入計數+1
...
lock.unlock(); // 計數-1,歸零后刪除鎖
五、避坑指南與最佳實踐
1. 鎖誤刪問題
- 原因:鎖過期后,其他線程刪除了當前線程的鎖
- 解決方案:
- 使用唯一標識(如UUID+線程ID)作為鎖值
- 釋放鎖前通過Lua腳本校驗歸屬
2. 鎖續期問題
- 看門狗機制:Redisson自動續期,業務長時間運行時需顯式指定超時時間
- 手動續期:調用?
lock.expire(seconds)
?延長鎖時間
3. 主從一致性問題
- 癥狀:主節點寫鎖后宕機,從節點未同步鎖信息
- 解決方案:
- 使用RedLock算法(需多數節點加鎖成功)
- 開啟Redis主從復制的
WAIT
命令(Redis 7+)
4. 性能優化
- 減少鎖粒度:按業務ID(如訂單號)細化鎖范圍
- 避免嵌套鎖:同一線程內多次加鎖需謹慎處理重入
- 監控指標:鎖獲取成功率、平均等待時間、超時次數
六、總結與選型建議
方案 | 適用場景 | 優點 | 缺點 |
---|---|---|---|
SETNX + EXPIRE | 快速原型、單節點場景 | 簡單高效 | 需手動處理細節 |
Redisson | Java項目、生產環境 | 功能完善,自動續期 | 依賴第三方庫 |
RedLock | Redis集群、高容錯需求 | 強一致性,容錯性強 | 性能較低,實現復雜 |
Lua腳本 | 需嚴格原子操作的場景 | 徹底解決非原子問題 | 需維護腳本 |
發布/訂閱 | 高并發減少輪詢 | 節省資源 | 消息可靠性需保障 |
最佳實踐:
- 優先使用Redisson框架(生產環境)
- 單節點快速實現可選SETNX+Lua腳本
- 集群環境采用RedLock算法
- 鎖名稱統一規范(如
resource_type:id
) - 超時時間設為業務平均耗時的2倍