一、分布式鎖的必要性
在分布式系統中,當多個節點需要對共享資源進行讀寫操作時,傳統的本地鎖(如Java的synchronized或ReentrantLock)無法跨節點生效。此時,必須引入分布式鎖來保證操作的原子性和一致性。分布式鎖需滿足以下核心特性:
- 互斥性:任意時刻僅一個客戶端持有鎖
- 防死鎖:即使持有鎖的客戶端崩潰,鎖仍可被釋放
- 可重入性:同一客戶端可多次獲取同一把鎖
- 一致性:解鎖操作必須由鎖的持有者執行
二、Redis分布式鎖實現
Redis實現分布式鎖主要利用Redis的setnx命令。
SETNX key value 是 Redis 的原子性命令,用于設置鍵值對,僅當鍵不存在時生效,否則不執行任何操作。
1. 基礎實現(SETNX+EXPIRE)
// 加鎖(非原子操作)
if (redisTemplate.execute((RedisCallback<Long>) connection -> connection.setNX(lockKey.getBytes(), UUID.randomUUID().toString().getBytes())) == 1) {redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);return true;
}
return false;
- 問題:SETNX與EXPIRE的非原子性導致可能存在鎖未設置過期時間的風險
2. 原子化實現(SET命令增強)
// 原子化加鎖
Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), UUID.randomUUID().toString().getBytes(), Expiration.seconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT));
return success != null && success;
- 優勢:通過SET命令的NX選項實現原子性加鎖與過期時間設置
3. 解鎖實現
// 解鎖(需驗證鎖歸屬)
if (UUID.fromString(redisTemplate.opsForValue().get(lockKey)).equals(currentUuid)) {redisTemplate.delete(lockKey);
}
三、Redis鎖的缺陷與優化
1. 鎖過期問題
- 現象:業務執行時間超過鎖過期時間,導致鎖提前釋放
- 解決方案:
- 動態調整過期時間
- 使用Redisson的看門狗機制
2. 鎖誤刪風險
- 現象:客戶端A的鎖過期后,客戶端B獲取鎖并被客戶端A誤刪
- 解決方案:
// 使用Lua腳本保證原子性 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockKey), currentUuid);
四、Redisson分布式鎖進階
1. 加鎖過程
當一個線程嘗試獲取分布式鎖時,Redisson 會執行以下操作:
- 原子操作:使用 Redis 的
SETNX
(SET if Not eXists)命令來嘗試設置一個鍵值對,鍵表示鎖的名稱,值表示持有鎖的線程標識(通常是一個 UUID)。如果設置成功,說明該線程成功獲取到鎖;如果設置失敗,說明鎖已經被其他線程持有。 - 設置過期時間:為了避免鎖被永久持有(例如持有鎖的線程崩潰),Redisson 會為鎖設置一個過期時間。可以使用
SET
命令的NX
和EX
選項來原子性地完成設置鍵值對和過期時間的操作。 - 鎖重入:Redisson 支持鎖重入,即同一個線程可以多次獲取同一把鎖而不會死鎖。為了實現鎖重入,Redisson 在 Redis 中存儲的鍵值對的值是一個計數器,每次線程獲取鎖時計數器加 1,釋放鎖時計數器減 1,當計數器為 0 時才真正釋放鎖。
2. 鎖續期機制
為了防止在業務邏輯執行期間鎖過期,Redisson 引入了鎖續期機制,也稱為“看門狗”機制:
- 定時任務:當線程成功獲取鎖后,Redisson 會啟動一個定時任務,該任務會在鎖過期時間的三分之一處執行,嘗試對鎖進行續期。
- Lua 腳本:續期操作使用 Lua 腳本來保證原子性,腳本會檢查鎖的持有者是否還是當前線程,如果是則更新鎖的過期時間。
3. 釋放鎖過程
當線程完成業務邏輯后,需要釋放鎖,Redisson 會執行以下操作:
- 計數器減 1:如果是鎖重入的情況,先將計數器減 1。
- 釋放鎖:當計數器為 0 時,使用 Lua 腳本來刪除 Redis 中的鍵值對,從而釋放鎖。Lua 腳本可以保證刪除操作的原子性,避免在刪除過程中出現并發問題。
示例代碼
以下是一個簡單的 Java 代碼示例,展示了如何使用 Redisson 獲取和釋放分布式鎖:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;import java.util.concurrent.TimeUnit;public class RedissonLockExample {public static void main(String[] args) {// 創建 Redisson 客戶端Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);// 獲取鎖RLock lock = redisson.getLock("myLock");try {// 嘗試獲取鎖,等待 10 秒,鎖的過期時間為 30 秒boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);if (isLocked) {// 模擬業務邏輯System.out.println("獲取到鎖,開始執行業務邏輯");Thread.sleep(5000);System.out.println("業務邏輯執行完畢");}} catch (InterruptedException e) {e.printStackTrace();} finally {// 釋放鎖if (lock.isHeldByCurrentThread()) {lock.unlock();System.out.println("鎖已釋放");}}// 關閉 Redisson 客戶端redisson.shutdown();}
}
4. 鎖模式對比
鎖類型 | 特性 | 適用場景 |
---|---|---|
公平鎖 | 按等待順序分配鎖 | 高并發有序場景 |
聯鎖 | 多個獨立Redis節點的組合鎖 | 金融級安全場景 |
紅鎖 | 多數節點達成共識的鎖機制 | 分布式系統強一致性要求 |
五、最佳實踐建議
- 鎖粒度控制:避免粗粒度鎖,優先使用細粒度鎖
- 過期時間設置:根據業務耗時合理設置,建議30-60秒
- 異常處理:所有加鎖操作必須包含finally塊釋放鎖
- 監控報警:對鎖競爭、鎖超時等異常情況進行監控
- 降級策略:鎖獲取失敗時要有回退機制,避免系統雪崩
六、總結
Redis原生鎖與Redisson框架為分布式鎖提供了不同層級的解決方案:
- Redis原生方案適用于輕量級場景,需關注原子性與鎖過期問題
- Redisson框架通過自動續期、多種鎖模式等特性,提供了企業級的分布式鎖解決方案
在實際應用中,應根據系統規模、一致性要求和業務特性選擇合適的實現方式,同時結合監控和報警機制保障系統的穩定性。