SpringBoot項(xiàng)目中實(shí)現(xiàn)Redis分布式鎖的方法與最佳實(shí)踐
引言
在 Spring Boot 項(xiàng)目中實(shí)現(xiàn) Redis 分布式鎖,主要有手動(dòng)實(shí)現(xiàn)和使用 ?Redisson? 兩種主流方式。下面我來為你詳細(xì)解釋這兩種方式的原理、實(shí)現(xiàn)方法和最佳實(shí)踐。
| 特性維度 | 手動(dòng)實(shí)現(xiàn) (RedisTemplate) | Redisson (推薦) |
|---|---|---|
| ?實(shí)現(xiàn)復(fù)雜度? | 較高,需處理所有細(xì)節(jié) | 低,開箱即用,API 簡潔 |
| ?可重入性? | 需自行實(shí)現(xiàn) | ?原生支持? |
| ?鎖續(xù)期(Watchdog)?? | 需自行實(shí)現(xiàn) | ?原生支持? |
| ?超時(shí)與等待? | 需自行實(shí)現(xiàn) | ?原生支持多種加鎖策略 |
| ?公平鎖/讀寫鎖? | 不支持 | ?支持? |
| ?集群支持(RedLock)?? | 實(shí)現(xiàn)復(fù)雜 | ?原生支持? |
| ?可靠性? | 一般,需自行規(guī)避各種邊界問題 | ?高,久經(jīng)考驗(yàn) |
手動(dòng)實(shí)現(xiàn)基于 RedisTemplate
這種方式讓你更接近底層原理,但需要自己處理鎖續(xù)期、可重入等復(fù)雜問題。
?添加依賴?
在 pom.xml中添加 Spring Data Redis 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
?配置 Redis 連接?
在 application.yml中配置:
spring:
redis:
host: localhost
port: 6379
# password: yourpassword # 如果設(shè)置了密碼
?分布式鎖工具類?
核心是使用 SET key value NX EX seconds命令原子性地獲取鎖,并用 Lua 腳本保證釋放鎖的原子性(判斷值再刪除)。
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
/**
* 嘗試獲取分布式鎖
* @param lockKey 鎖的業(yè)務(wù)Key,如 "order:123"
* @param requestId 請(qǐng)求標(biāo)識(shí)(可用UUID),用于安全釋放鎖
* @param expireTime 鎖的過期時(shí)間(毫秒)
* @param waitTime 獲取鎖的最大等待時(shí)間(毫秒)
* @return 是否獲取成功
*/
public boolean tryLock(String lockKey, String requestId, long expireTime, long waitTime) throws InterruptedException {
String fullKey = LOCK_PREFIX + lockKey;
long end = System.currentTimeMillis() + waitTime;
while (System.currentTimeMillis() < end) { // 在等待時(shí)間內(nèi)循環(huán)嘗試
// 使用 SET NX EX 命令原子性地嘗試獲取鎖
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(fullKey, requestId, expireTime, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(success)) {
return true; // 獲取鎖成功
}
Thread.sleep(50); // 短暫休眠,避免活鎖和CPU空轉(zhuǎn)
}
return false; // 超時(shí)仍未獲取到鎖
}
/**
* 釋放分布式鎖 (使用Lua腳本保證原子性)
* @param lockKey 鎖的業(yè)務(wù)Key
* @param requestId 請(qǐng)求標(biāo)識(shí),必須與加鎖時(shí)一致
* @return 是否釋放成功
*/
public boolean unlock(String lockKey, String requestId) {
String fullKey = LOCK_PREFIX + lockKey;
// Lua腳本:如果鍵存在且值與預(yù)期相符,則刪除它
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(fullKey), requestId);
return result != null && result == 1;
}
}
?關(guān)鍵點(diǎn):??
- ?NX (Not eXists)??:確保只有鎖不存在時(shí)才能設(shè)置成功,實(shí)現(xiàn)互斥。
- ?EX (Expire)??:設(shè)置過期時(shí)間,?這是防止死鎖的關(guān)鍵。即使客戶端崩潰,鎖也會(huì)自動(dòng)釋放。
- ?唯一值 (requestId)??:使用唯一值(如UUID)標(biāo)識(shí)鎖的持有者,確保只能由加鎖的客戶端來解鎖,?防止誤釋鎖。
- ?Lua 腳本?:釋放鎖時(shí),判斷值和刪除必須是原子操作,否則可能誤刪其他客戶端的鎖。
?在業(yè)務(wù)中使用?
@Service
public class OrderService {
@Autowired
private RedisDistributedLock redisLock;
public void createOrder(String orderId) {
String lockKey = "order_create:" + orderId;
String requestId = UUID.randomUUID().toString(); // 生成唯一請(qǐng)求ID
boolean locked = false;
try {
// 嘗試獲取鎖,等待5秒,鎖過期時(shí)間10秒
locked = redisLock.tryLock(lockKey, requestId, 10000, 5000);
if (locked) {
// 成功獲取鎖,執(zhí)行核心業(yè)務(wù)邏輯
System.out.println("成功獲取鎖,執(zhí)行訂單創(chuàng)建邏輯...");
// ... (你的業(yè)務(wù)代碼)
} else {
// 獲取鎖失敗,處理異常或重試
throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)稍后再試");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("獲取鎖被中斷", e);
} finally {
// 釋放鎖,務(wù)必放在finally塊中確保執(zhí)行
if (locked) {
redisLock.unlock(lockKey, requestId);
}
}
}
}
使用 Redisson(推薦)
Redisson 是一個(gè)強(qiáng)大的 Redis Java 客戶端,它提供了直接可用的分布式鎖實(shí)現(xiàn),解決了手動(dòng)實(shí)現(xiàn)的所有痛點(diǎn),如可重入鎖、鎖續(xù)期、等待機(jī)制等。
?添加依賴?
在 pom.xml中添加 Redisson 依賴:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version> <!-- 請(qǐng)使用最新穩(wěn)定版本 -->
</dependency>
?基本配置 (application.yml)??
spring:
redis:
host: localhost
port: 6379
# Redisson 通常會(huì)自動(dòng)使用Spring Redis的配置
?使用 Redisson 的 RLock?
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 OrderServiceRedisson {
@Autowired
private RedissonClient redissonClient; // 直接注入RedissonClient
public void createOrder(String orderId) {
String lockKey = "order:lock:" + orderId;
RLock lock = redissonClient.getLock(lockKey); // 獲取RLock對(duì)象
try {
// 嘗試加鎖,最多等待5秒,鎖持有時(shí)間10秒后自動(dòng)解鎖
// boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
// 更常用的方式是設(shè)置等待時(shí)間,但讓看門狗自動(dòng)續(xù)期(lockTime傳入-1或使用無參lock())
boolean isLocked = lock.tryLock(5, TimeUnit.SECONDS);
if (isLocked) {
try {
// 成功獲取鎖,執(zhí)行業(yè)務(wù)邏輯
System.out.println("Redisson鎖獲取成功,執(zhí)行邏輯...");
// ... (你的業(yè)務(wù)邏輯,看門狗會(huì)為沒有顯式設(shè)置leaseTime的鎖自動(dòng)續(xù)期)
} finally {
// 釋放鎖
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)稍后再試");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("獲取鎖被中斷", e);
}
}
}
?Redisson 核心優(yōu)勢(shì):??
- ?看門狗機(jī)制 (Watchdog)??:如果你沒有顯式指定
leaseTime(鎖持有時(shí)間),Redisson 會(huì)啟動(dòng)一個(gè)看門狗線程,在業(yè)務(wù)執(zhí)行期間自動(dòng)定期續(xù)期鎖,防止業(yè)務(wù)執(zhí)行時(shí)間超時(shí)導(dǎo)致鎖意外釋放。這是手動(dòng)實(shí)現(xiàn)非常容易忽略且難以正確實(shí)現(xiàn)的一點(diǎn)。 - ?可重入?:同一個(gè)線程可以多次獲取同一把鎖。
- ?豐富的鎖類型?:支持可重入鎖、公平鎖、聯(lián)鎖、紅鎖(RedLock)、讀寫鎖等。
注意事項(xiàng)與最佳實(shí)踐
?鎖的粒度?:鎖的 Key 要精確,例如 "order:123"比 "order"更好,減少競爭,提升并發(fā)度。
?超時(shí)時(shí)間?:手動(dòng)實(shí)現(xiàn)時(shí),過期時(shí)間不宜過短或過長。過短可能導(dǎo)致業(yè)務(wù)未完成鎖就釋放;過長則故障恢復(fù)慢。Redisson 的看門狗機(jī)制能很好地解決這個(gè)問題。
?務(wù)必釋放鎖?:釋放鎖必須放在 finally代碼塊中執(zhí)行,確保即使業(yè)務(wù)異常也能釋放鎖。
?Redis 模式?:
- 單機(jī)/主從模式:上述方法適用,但主從異步復(fù)制下,主節(jié)點(diǎn)宕機(jī)可能導(dǎo)致鎖失效(從節(jié)點(diǎn)可能無鎖數(shù)據(jù))。
- ?紅鎖 (RedLock)??:為解決上述問題,可在多個(gè)獨(dú)立的 Redis 主節(jié)點(diǎn)上嘗試獲取鎖,超過半數(shù)成功才算真正獲取。Redisson 提供了紅鎖實(shí)現(xiàn),但更復(fù)雜,通常用于對(duì)一致性要求極高的場景。
總結(jié)
對(duì)于大多數(shù) Spring Boot 項(xiàng)目,?我強(qiáng)烈推薦直接使用 Redisson。它功能完善、久經(jīng)考驗(yàn),能讓你遠(yuǎn)離分布式鎖的各種復(fù)雜細(xì)節(jié),更專注于業(yè)務(wù)邏輯開發(fā)。如果你是為了學(xué)習(xí)底層原理或?qū)崿F(xiàn)一個(gè)非常簡單的鎖,那么可以嘗試手動(dòng)實(shí)現(xiàn)的方式。
以上就是SpringBoot項(xiàng)目中實(shí)現(xiàn)Redis分布式鎖的方法與最佳實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot實(shí)現(xiàn)Redis分布式鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Maven中Profile基礎(chǔ)知識(shí)與激活機(jī)制詳解
這篇文章將深入解析Maven?Profile的底層實(shí)現(xiàn)機(jī)制,從Profile標(biāo)識(shí)符的作用域劃分到多維度激活條件的組合應(yīng)用,全面剖析其在不同規(guī)模項(xiàng)目中的最佳實(shí)踐,需要的可以了解下2025-05-05
java獲取文件擴(kuò)展名的方法小結(jié)【正則與字符串截取】
這篇文章主要介紹了java獲取文件擴(kuò)展名的方法,結(jié)合實(shí)例形式分析了使用正則與字符串截取兩種獲取擴(kuò)展名的操作技巧,需要的朋友可以參考下2017-01-01
Java因項(xiàng)目配置不當(dāng)而引發(fā)的數(shù)據(jù)泄露
這篇文章主要介紹了Java因項(xiàng)目配置不當(dāng)而引發(fā)的數(shù)據(jù)泄露解決辦法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09
Java的volatile和sychronized底層實(shí)現(xiàn)原理解析
文章詳細(xì)介紹了Java中的synchronized和volatile關(guān)鍵字的底層實(shí)現(xiàn)原理,包括字節(jié)碼層面、JVM層面的實(shí)現(xiàn)細(xì)節(jié),以及鎖的類型和MESI協(xié)議在多核處理器中的作用,文章還探討了synchronized和volatile的區(qū)別,以及如何通過Atomic類來實(shí)現(xiàn)更細(xì)粒度的原子操作,感興趣的朋友一起看看吧2025-03-03
SpringCloud分布式鏈路追蹤組件Sleuth配置詳解
這篇文章主要介紹了SpringCloud鏈路追蹤組件Sleuth配置方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-11-11
Java中List集合去除重復(fù)數(shù)據(jù)的方法匯總
這篇文章主要給大家介紹了關(guān)于Java中List集合去除重復(fù)數(shù)據(jù)的方法,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
如何使用Spring MVC的消息轉(zhuǎn)換器設(shè)置日期格式
這篇文章主要介紹了如何使用Spring MVC的消息轉(zhuǎn)換器設(shè)置日期格式,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07

