Redisson提供了分布式和可擴展的Java數據結構,包括分布式鎖的實現。
1. 添加依賴
在pom.xml
中添加Redisson依賴:
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.4</version> <!-- 根據需要選擇最新穩定版本 -->
</dependency>
2. 配置Redisson客戶端
創建Redisson配置類:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {// 創建單機模式的配置Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379").setPassword("yourPassword") // 如果有密碼.setDatabase(0);// 集群模式配置示例/*config.useClusterServers().addNodeAddress("redis://node1:7000", "redis://node2:7001").setPassword("password").setScanInterval(2000); // 集群狀態掃描間隔*/return Redisson.create(config);}
}
3. 使用Redisson分布式鎖
以下是幾種常見的鎖使用方式:
3.1 可重入鎖(Reentrant Lock)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
public class OrderService {@Autowiredprivate RedissonClient redissonClient;public void createOrder(String orderId) {String lockName = "order-lock:" + orderId;RLock lock = redissonClient.getLock(lockName);try {// 方式1:阻塞式獲取鎖(默認30秒自動釋放)lock.lock();// 方式2:帶超時的獲取鎖(等待100秒,自動釋放時間30秒)// boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);// if (isLocked) { ... }// 執行業務邏輯System.out.println("獲取鎖成功,處理訂單: " + orderId);Thread.sleep(2000);} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {// 釋放鎖lock.unlock();}}
}
3.2 公平鎖(Fair Lock)
public void createOrderFair(String orderId) {String lockName = "fair-order-lock:" + orderId;// 獲取公平鎖RLock fairLock = redissonClient.getFairLock(lockName);try {fairLock.lock();// 執行業務邏輯} finally {fairLock.unlock();}
}
3.3 讀寫鎖(ReadWriteLock)
import org.redisson.api.RReadWriteLock;public void updateProductStock(String productId) {String lockName = "product-rw-lock:" + productId;RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockName);// 寫鎖(互斥)try {rwLock.writeLock().lock();// 執行寫操作,如更新庫存} finally {rwLock.writeLock().unlock();}// 讀鎖(共享)try {rwLock.readLock().lock();// 執行讀操作,如查詢庫存} finally {rwLock.readLock().unlock();}
}
4. Redisson分布式鎖的特性
-
自動續約(WatchDog機制)
當獲取鎖時未指定釋放時間,Redisson會自動延長鎖的過期時間,防止業務執行時間過長導致鎖提前釋放。 -
可重入性
同一線程可以多次獲取同一把鎖而不會死鎖,需要相應次數的釋放。 -
異常處理
Redisson的鎖釋放操作在finally塊中執行,確保即使發生異常也能釋放鎖。 -
異步和響應式支持
支持異步獲取鎖和響應式編程模型:// 異步獲取鎖 CompletableFuture<Boolean> future = lock.tryLockAsync(100, 30, TimeUnit.SECONDS);
5. RedLock算法實現(多節點模式)
RedLock用于在多個Redis節點上實現更高可靠性的分布式鎖:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisClient;public void redLockExample() {// 創建多個Redisson客戶端連接不同節點RedissonClient client1 = Redisson.create(...);RedissonClient client2 = Redisson.create(...);RedissonClient client3 = Redisson.create(...);RLock lock1 = client1.getLock("lock1");RLock lock2 = client2.getLock("lock2");RLock lock3 = client3.getLock("lock3");// 創建RedLockorg.redisson.api.RedLock redLock = new org.redisson.api.RedLock(lock1, lock2, lock3);try {// 嘗試獲取鎖(多數節點成功才算成功)boolean isLocked = redLock.tryLock(100, 30, TimeUnit.SECONDS);if (isLocked) {// 執行業務邏輯}} finally {redLock.unlock();}
}
6.可重入鎖示例
6.1示例背景及需求
在很多實際業務場景中,我們執行的操作可能由于各種臨時原因(比如網絡抖動、資源暫時不可用等)而失敗,此時往往希望能夠進行重試,以增加操作最終成功的概率。同時,為了保證在重試過程中對共享資源的訪問是互斥的(避免多個線程同時操作造成數據不一致等問題),就需要使用可重入鎖來進行控制。
6.2詳細代碼示例
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
public class RetryableOperationService {@Autowiredprivate RedissonClient redissonClient;// 定義最大重試次數private static final int MAX_ATTEMPTS = 3;// 定義重試間隔時間(單位:毫秒)private static final long RETRY_INTERVAL = 100;public void retryableOperation(String key) {String lockName = "retry-lock:" + key;RLock lock = redissonClient.getLock(lockName);lock.lock();try {int attempts = 0;while (attempts < MAX_ATTEMPTS) {try {// 這里是具體要執行的可能失敗的業務操作,比如調用外部接口獲取數據、更新數據庫等System.out.println("正在執行第 " + (attempts + 1) + " 次嘗試操作...");// 模擬業務操作可能出現的異常情況,這里簡單地通過拋異常來表示操作失敗if (attempts == 1) {throw new RuntimeException("模擬操作失敗");}// 如果操作成功,直接返回,不再進行重試return;} catch (Exception e) {attempts++;System.out.println("操作失敗,第 " + attempts + " 次重試,等待 " + RETRY_INTERVAL + " 毫秒后再次嘗試...");// 線程休眠一段時間,等待后再次嘗試Thread.sleep(RETRY_INTERVAL);}}System.out.println("達到最大重試次數,操作最終失敗");} catch (InterruptedException ex) {Thread.currentThread().interrupt();System.out.println("線程被中斷,操作終止");} finally {// 無論操作最終是否成功,都要釋放鎖,確保其他線程可以獲取鎖來執行操作lock.unlock();}}
}
6.3代碼執行流程說明
-
獲取鎖:
首先,根據傳入的key
生成對應的鎖名稱(retry-lock:具體的key值
),然后通過RedissonClient
獲取可重入鎖lock
,并調用lock()
方法獲取鎖,這一步保證了在同一時刻只有一個線程能夠執行后續的重試操作,避免并發沖突。 -
執行操作與重試邏輯:
進入while
循環,開始嘗試執行具體的業務操作。在每次循環中:- 嘗試執行業務操作:這里只是簡單地模擬了業務操作,通過拋異常來表示操作失敗(實際應用中可能是調用外部接口返回錯誤碼、數據庫更新失敗等情況)。如果操作成功(沒有拋出異常),則直接通過
return
語句結束方法,不再進行重試。 - 處理操作失敗情況:當操作拋出異常時,表示此次嘗試失敗。
attempts
變量記錄當前的重試次數,每次失敗后將其加1,并輸出相應的重試提示信息,然后讓線程休眠RETRY_INTERVAL
(100毫秒)時間,等待一段時間后再次進行下一次嘗試。
- 嘗試執行業務操作:這里只是簡單地模擬了業務操作,通過拋異常來表示操作失敗(實際應用中可能是調用外部接口返回錯誤碼、數據庫更新失敗等情況)。如果操作成功(沒有拋出異常),則直接通過
-
重試次數限制與最終結果處理:
while
循環的條件是attempts < MAX_ATTEMPTS
,也就是最多只會重試MAX_ATTEMPTS
(定義為3次)次。當達到最大重試次數后,如果操作還是沒有成功,就會輸出操作最終失敗的提示信息。 -
釋放鎖:
無論操作最終是成功還是達到最大重試次數失敗,都會進入finally
塊釋放鎖。這一步非常關鍵,確保了鎖資源能夠被正確釋放,使得其他等待獲取該鎖的線程有機會執行相應的操作。
6.4適用場景舉例
-
網絡請求重試:
比如在一個微服務架構中,服務A需要調用服務B的接口獲取數據來更新本地數據庫。由于網絡環境不穩定,可能出現調用失敗的情況。此時,服務A就可以使用上述帶有可重入鎖的重試機制,在獲取到鎖后,多次嘗試調用服務B的接口,保證在重試過程中不會有其他線程同時進行相同的調用操作,避免數據不一致等問題。 -
數據庫更新重試:
當向數據庫插入或更新一條重要記錄時,可能因為數據庫臨時負載過高、事務沖突等原因導致操作失敗。通過使用這種帶可重入鎖的重試機制,可以在一定次數內反復嘗試更新操作,同時保證在整個重試期間,其他線程不會干擾該記錄的更新,確保數據的完整性和一致性。
6.5總結
可重入鎖主要用于以下方面:
- 確保操作的互斥性
在整個 retryableOperation 方法執行期間,從獲取鎖開始到最終釋放鎖,只有獲取到鎖的那個線程能夠執行后續的業務操作以及重試邏輯。例如,假設有多個線程同時調用 retryableOperation 方法并傳入相同的 key,由于鎖的存在,只有最先成功獲取到鎖(鎖名為 retry-lock:具體的key值)的線程能夠進入到方法內部開始執行可能失敗的業務操作以及重試流程,其他線程會被阻塞等待鎖釋放,從而保證了針對同一 key 對應的操作在同一時刻是互斥執行的,避免了多個線程同時操作造成的數據不一致、資源競爭等問題。 - 支持重試過程中的再次進入
因為是可重入鎖,在重試邏輯中(while 循環內),當業務操作失敗后線程進行休眠,然后再次嘗試執行業務操作時,同一線程能夠再次獲取到已經持有的這把鎖(無需等待鎖釋放后重新競爭獲取),可以順利進入循環體繼續執行后續的操作,不會出現因為鎖的限制而導致自己阻塞自己的情況。如果使用的是不可重入鎖,那么在重試時線程再次嘗試獲取鎖就會被阻塞,因為它自己已經持有這把鎖了,這樣就無法完成重試邏輯,會陷入死鎖狀態。