【Java后端】【可直接落地的 Redis 分布式鎖實現】

可直接落地的 Redis 分布式鎖實現:包含最小可用版、生產可用版(帶 Lua 原子解鎖、續期“看門狗”、自旋等待、可重入)、以及基于注解+AOP 的無侵入用法,最后還給出 Redisson 方案對比與踩坑清單。


一、設計目標與約束

  • 獲取鎖:SET key value NX PX ttl(原子、帶過期)
  • 釋放鎖:Lua 校驗 value(token)后再 DEL,避免誤刪他人鎖
  • 等待策略:可設置總體等待時長 + 抖動退避,避免驚群
  • 續期(看門狗):長耗時任務自動延長鎖過期,避免任務未完成鎖先過期
  • 可重入:同一線程/請求二次進入同一鎖,計數 +1,退出時計數 -1
  • 可觀測性:日志、指標(命中/失敗/續期次數等)

二、最小可用實現(入門示例)

// MinimalLockService.java
@Service
public class MinimalLockService {private final StringRedisTemplate redis;public MinimalLockService(StringRedisTemplate redis) {this.redis = redis;}/** 獲取鎖,返回 token(uuid),失敗返回 null */public String tryLock(String key, long ttlMs) {String token = UUID.randomUUID().toString();Boolean ok = redis.opsForValue().setIfAbsent(key, token, Duration.ofMillis(ttlMs));return Boolean.TRUE.equals(ok) ? token : null;}/** 釋放鎖(Lua):只有持有相同 token 才能刪除鎖 */public boolean unlock(String key, String token) {String script = """if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])elsereturn 0end""";Long res = redis.execute(new DefaultRedisScript<>(script, Long.class), List.of(key), token);return res != null && res > 0;}
}

適合“單次短任務、不等待”的場景;生產建議使用下文增強版。


三、生產可用鎖客戶端(可重入 + 等待 + 續期)

1)核心實現

// RedisDistributedLock.java
@Component
public class RedisDistributedLock implements InitializingBean, DisposableBean {private final StringRedisTemplate redis;private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);private final DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>();private final DefaultRedisScript<Long> renewScript  = new DefaultRedisScript<>();// 線程內可重入計數:key -> (token, count)private final ThreadLocal<Map<String, ReentryState>> reentry = ThreadLocal.withInitial(HashMap::new);public RedisDistributedLock(StringRedisTemplate redis) {this.redis = redis;}@Override public void afterPropertiesSet() {unlockScript.setResultType(Long.class);unlockScript.setScriptText("""if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('del', KEYS[1])elsereturn 0end""");renewScript.setResultType(Long.class);renewScript.setScriptText("""if redis.call('get', KEYS[1]) == ARGV[1] thenreturn redis.call('pexpire', KEYS[1], ARGV[2])elsereturn 0end""");}@Override public void destroy() { scheduler.shutdownNow(); }public static class LockHandle implements AutoCloseable {private final RedisDistributedLock client;private final String key;private final String token;private final long ttlMs;private final ScheduledFuture<?> watchdogTask;private boolean closed = false;private LockHandle(RedisDistributedLock c, String key, String token, long ttlMs, ScheduledFuture<?> task) {this.client = c; this.key = key; this.token = token; this.ttlMs = ttlMs; this.watchdogTask = task;}@Override public void close() {if (closed) return;closed = true;if (watchdogTask != null) watchdogTask.cancel(false);client.release(key, token);}public String key() { return key; }public String token() { return token; }}private record ReentryState(String token, AtomicInteger count) {}/** 嘗試在 waitMs 內獲取鎖;持有 ttlMs;支持可重入與退避等待;啟用自動續期(watchdog=true) */public Optional<LockHandle> acquire(String key, long ttlMs, long waitMs, boolean watchdog) {Map<String, ReentryState> map = reentry.get();// 可重入:當前線程已持有同一 keyif (map.containsKey(key)) {map.get(key).count().incrementAndGet();return Optional.of(new LockHandle(this, key, map.get(key).token(), ttlMs, null));}final String token = UUID.randomUUID().toString();final long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(waitMs);while (true) {Boolean ok = redis.opsForValue().setIfAbsent(key, token, Duration.ofMillis(ttlMs));if (Boolean.TRUE.equals(ok)) {map.put(key, new ReentryState(token, new AtomicInteger(1)));ScheduledFuture<?> task = null;if (watchdog) {// 續期間隔:ttl 的 1/2(保守 <= 2/3 均可)long interval = Math.max(500, ttlMs / 2);task = scheduler.scheduleAtFixedRate(() -> renew(key, token, ttlMs),interval, interval, TimeUnit.MILLISECONDS);}return Optional.of(new LockHandle(this, key, token, ttlMs, task));}if (System.nanoTime() > deadline) break;// 抖動退避:50~150mstry { Thread.sleep(50 + ThreadLocalRandom.current().nextInt(100)); }catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; }}return Optional.empty();}private void renew(String key, String token, long ttlMs) {try {Long r = redis.execute(renewScript, List.of(key), token, String.valueOf(ttlMs));// 失敗說明鎖已不在或被他人占有,停止續期} catch (Exception ignore) {}}private void release(String key, String token) {Map<String, ReentryState> map = reentry.get();ReentryState state = map.get(key);if (state == null) return; // 非當前線程,無操作(冪等)if (state.count().decrementAndGet() > 0) return; // 仍有重入層級map.remove(key);try {redis.execute(unlockScript, List.of(key), token);} catch (Exception e) {// 記錄日志/指標}}
}

2)使用范例(try-with-resources 自動釋放)

@Service
public class OrderService {private final RedisDistributedLock lock;public OrderService(RedisDistributedLock lock) { this.lock = lock; }public void deductStock(String skuId) {String key = "lock:stock:" + skuId;Optional<RedisDistributedLock.LockHandle> h =lock.acquire(key, /*ttlMs*/ 10_000, /*waitMs*/ 3_000, /*watchdog*/ true);if (h.isEmpty()) {throw new IllegalStateException("系統繁忙,請稍后重試");}try (RedisDistributedLock.LockHandle ignored = h.get()) {// 業務邏輯:查詢庫存 -> 校驗 -> 扣減 -> 持久化// ...(這里可再次重入同鎖,例如調用內部方法)}}
}

四、注解 + AOP:無侵入加鎖(支持 SpEL 動態 key)

1)定義注解

// RedisLock.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {/** 鎖名(前綴) */String name();/** 業務 key 的 SpEL,例如 "#skuId" 或 "#req.userId + ':' + #req.orderId" */String key();/** 過期毫秒 */long ttlMs() default 10_000;/** 最長等待毫秒 */long waitMs() default 3_000;/** 是否自動續期 */boolean watchdog() default true;/** 獲取失敗是否拋異常;false 則直接跳過執行業務 */boolean failFast() default true;
}

2)AOP 切面

// RedisLockAspect.java
@Aspect
@Component
public class RedisLockAspect {private final RedisDistributedLock locker;private final SpelExpressionParser parser = new SpelExpressionParser();private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();public RedisLockAspect(RedisDistributedLock locker) { this.locker = locker; }@Around("@annotation(anno)")public Object around(ProceedingJoinPoint pjp, RedisLock anno) throws Throwable {MethodSignature sig = (MethodSignature) pjp.getSignature();Method method = sig.getMethod();String spel = anno.key();EvaluationContext ctx = new StandardEvaluationContext();String[] paramNames = nameDiscoverer.getParameterNames(method);Object[] args = pjp.getArgs();if (paramNames != null) {for (int i = 0; i < paramNames.length; i++) {ctx.setVariable(paramNames[i], args[i]);}}String bizKey = parser.parseExpression(spel).getValue(ctx, String.class);String lockKey = "lock:" + anno.name() + ":" + bizKey;Optional<RedisDistributedLock.LockHandle> h =locker.acquire(lockKey, anno.ttlMs(), anno.waitMs(), anno.watchdog());if (h.isEmpty()) {if (anno.failFast()) {throw new IllegalStateException("并發過高,稍后再試");} else {return null; // 或者返回自定義“占用中”結果}}try (RedisDistributedLock.LockHandle ignored = h.get()) {return pjp.proceed();}}
}

3)業務使用

@Service
public class CheckoutService {@RedisLock(name = "pay", key = "#orderId", ttlMs = 15000, waitMs = 5000)public String pay(Long orderId) {// 冪等校驗、扣款、記賬、改狀態...return "OK";}
}

五、和 Redisson 的取舍

  • 自己實現(本文方案)
    輕量、可控、無第三方依賴;需要你自己維護續期、統計、容錯。
  • Redisson
    功能齊全(公平鎖、信號量、讀寫鎖、鎖續期看門狗、聯鎖/紅鎖等),配置簡單,實戰成熟。
    👉 建議對鎖模型復雜、需要多數據結構協作的場景直接上 Redisson。

示例(Redisson):

<!-- pom -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.38.0</version>
</dependency>
@Autowired private RedissonClient redisson;public void doWork() {RLock lock = redisson.getLock("lock:demo");// 默認看門狗 30s,自動續期if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {try { /* 業務 */ }finally { lock.unlock(); }}
}

六、生產實踐與踩坑清單

  1. 務必用 Lua 校驗 token 再解鎖:防止誤刪他人鎖。
  2. TTL 要合理:不能太短(業務未完成鎖已過期),也不能太長(死鎖恢復慢)。一般結合看門狗更穩。
  3. 等待 + 退避:避免 CPU 自旋和驚群;可以配合“排隊提示”。
  4. 可重入只是“線程內”語義:跨線程/跨進程不可重入,需要更復雜的標識管理;盡量避免跨線程使用同一鎖。
  5. 冪等設計:即使拿到鎖也可能重復執行(重試、網絡抖動);寫操作要有冪等鍵。
  6. 多節點/主從復制延遲:強一致要求下盡量連接主節點;或降低讀從庫。
  7. 集群模式 key tag:使用 {} 包裹哈希標簽,確保同一鍵路由到同槽位(適用于 Redisson 等場景)。
  8. 監控指標:加鎖成功率、平均等待、續期失敗次數、異常堆棧等,配合告警。
  9. 故障演練:kill -9 模擬進程崩潰,驗證鎖自動過期與業務補償是否生效。

七、完整配置(參考)

<!-- pom.xml 關鍵依賴 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
# application.yml
spring:redis:host: localhostport: 6379# password: yourpasslettuce:pool:max-active: 8max-idle: 8min-idle: 0
// Redis 序列化(可選,鎖用不到復雜序列化,這里保證 key=String 即可)
@Configuration
public class RedisConfig {@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory f) {return new StringRedisTemplate(f);}
}

八、如何驗證

  • 并發壓測兩個請求同時調用 @RedisLock 方法,觀察只有一個進入臨界區;另一個要么等待成功、要么超時失敗。
  • 人為延長業務耗時(Thread.sleep),觀察續期是否發生:在 Redis 中 PTTL lock:... 始終大于 0。
  • 殺掉進程:確認鎖會在 TTL 到期后自動釋放。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/94251.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/94251.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/94251.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

數據結構 -- 鏈表--雙向鏈表的特點、操作函數

雙向鏈表的操作函數DouLink.c#include "DouLink.h" #include <stdio.h> #include <stdlib.h> #include <string.h>/*** brief 創建一個空的雙向鏈表* * 動態分配雙向鏈表管理結構的內存&#xff0c;并初始化頭指針和節點計數* * return 成功返回指…

Wireshark獲取數據傳輸的碼元速率

一、Wireshark的物理層參數 Wireshark主界面可以看到數據發送時刻和長度&#xff1a; 這個時刻是Wireshark完整獲取數據包的時刻&#xff0c;實際上就是結束時刻。 需要知道的是&#xff1a; Wireshark工作在數據鏈路層及以上&#xff0c;它能解碼 以太網幀 / IP 包 / TCP…

11.1.3 完善注冊登錄,實現文件上傳和展示

1、完善注冊/登錄 1. 涉及的數據庫表單&#xff1a;user_info 2. 引用MySQL線程池&#xff0c;Redis線程池 3. 完善注冊功能 4. 完善登錄功能 2.1 涉及的數據庫表單&#xff1a;user_info 重新創建數據庫 #創建數據庫 DROP DATABASE IF EXISTS 0voice_tuchuang;CREATE D…

【Linux文件系統】目錄結構

有沒有剛進入Linux世界時&#xff0c;對著黑乎乎的終端&#xff0c;輸入一個 ls / 后&#xff0c;看著蹦出來的一堆名字 like bin, etc, usr&#xff0c;感覺一頭霧水&#xff0c;像是在看天書&#xff1f; 別擔心&#xff0c;你不是一個人。Linux的文件系統就像一個超級有條理…

螺旋槽曲面方程的數學建模與偏導數求解

螺旋槽曲面的數學描述 在鉆頭設計和機械加工領域,螺旋槽的幾何建模至關重要。螺旋槽通常由徑向截形繞軸做螺旋運動形成,其數學模型可通過參數方程和隱函數方程兩種方式描述。 設螺旋槽的徑向截形方程為: y=f(z)y = f(z)y=f(z) x=xcx = x_cx=xc? 其中 xcx_cxc? 為常數,…

線性回歸:機器學習中的基石

在機器學習的眾多算法中&#xff0c;線性回歸無疑是最基礎也是最常被提及的一種。它不僅在統計學中占有重要地位&#xff0c;而且在預測分析和數據建模中也發揮著關鍵作用。本文將深入探討線性回歸的基本概念、評估指標以及在實際問題中的應用&#xff0c;并通過一個模擬的氣象…

編程刷題-資料分發1 圖論/DFS

P2097 資料分發 1 題目描述 有一些電腦&#xff0c;一部分電腦有雙向數據線連接。 如果一個電腦得到數據&#xff0c;它可以傳送到的電腦都可以得到數據。 現在&#xff0c;你有這個數據&#xff0c;問你至少將其輸入幾臺電腦&#xff0c;才能使所有電腦得到數據。 輸入格式 第…

RabbitMQ:延時消息(死信交換機、延遲消息插件)

目錄一、死信交換機【不推薦】二、延遲消息插件【推薦】2.1 安裝插件【Linux】2.2 安裝插件【Windows】2.3 如何使用延時消息&#xff1a;生產者發送消息時指定一個時間&#xff0c;消費者不會立刻收到消息&#xff0c;而是在指定時間之后才收到消息。 延時任務&#xff1a;設置…

動學學深度學習05-深度學習計算

動學學深度學習pytorch 參考地址&#xff1a;https://zh.d2l.ai/ 文章目錄動學學深度學習pytorch1-第05章-深度學習計算1. 層&#xff08;Layer&#xff09;與塊&#xff08;Block&#xff09;1.1 什么是深度學習中的“層”&#xff1f;1.2 什么是“塊”&#xff08;Block&…

智慧工廠煙霧檢測:全場景覆蓋與精準防控

智慧工廠煙霧檢測&#xff1a;構建工業安全的智能防線&#xff08;所有圖片均為真實項目案例&#xff09;在工業4.0時代&#xff0c;智慧工廠通過物聯網、人工智能與大數據技術的深度融合&#xff0c;實現了生產流程的數字化與智能化。然而&#xff0c;工廠環境中的火災隱患始終…

@JsonIgnoreProperties注解詳解

JsonIgnoreProperties是 Jackson 庫中的一個重要注解&#xff0c;用于在 JSON 序列化&#xff08;對象轉 JSON&#xff09;和反序列化&#xff08;JSON 轉對象&#xff09;過程中??控制屬性的可見性??。它提供了更高級別的屬性忽略能力&#xff0c;特別適合處理復雜場景。一…

紅酒數據集預處理實戰:缺失值處理的 5 種打開方式,從入門到進階一步到位

在數據分析與建模流程中&#xff0c;缺失值處理是數據預處理階段的關鍵步驟&#xff0c;直接影響后續模型的準確性與穩定性。本文以紅酒數據集為研究對象&#xff0c;詳細介紹如何通過基礎統計方法&#xff08;均值、中位數、眾數&#xff09;、完整案例分析&#xff08;CCA&am…

Node.js 開發 JavaScript SDK 包的完整指南(AI)

一、核心概念SDK 包定義 專為特定服務/平臺封裝的工具庫&#xff0c;提供標準化 API 調用、錯誤處理、類型聲明等功能。示例&#xff1a;支付寶 SDK、AWS SDK、微信小程序 SDK。技術棧選擇 語言&#xff1a;JavaScript/TypeScript&#xff08;推薦 TS&#xff0c;便于類型提示&…

Redis實戰-基于Session實現分布式登錄

1.流程分析1.1發送短信驗證碼提交手機號的時候要進行校驗手機號&#xff0c;校驗成功才會去生成驗證碼&#xff0c;將驗證碼保存到session&#xff0c;發生他把這部分那。1.2短信驗證碼登錄/注冊如果提交手機號和驗證碼之后&#xff0c;校驗一致才進行根據手機號查詢用戶&#…

瘋狂星期四文案網第47天運營日記

網站運營第47天&#xff0c;點擊觀站&#xff1a; 瘋狂星期四 crazy-thursday.com 全網最全的瘋狂星期四文案網站 運營報告 今日訪問量 今日搜索引擎收錄情況 必應現在是邊收錄邊k頁面 百度快倒閉 網站優化點 完善工作流&#xff0c;全面實現文案自動化采集&#xff0c;se…

Vue生命周期以及自定義鉤子和路由

Vue生命周期常用的onMounted掛載后執行和onUnmounted卸載前以及onupdated更新后實際上用react對比就是useEffect&#xff0c;而且掛載順序也是子組件先于父組件然后往外的棧結構&#xff0c;先進后出。1.Vue的生命周期<template><h2>當前求和為{{ sum }}</h2>…

探索Thompson Shell:Unix初代Shell的智慧

引言 在計算機科學的漫漫長河中&#xff0c;Thompson Shell 無疑占據著舉足輕重的開創性地位&#xff0c;它是 Unix 系統的第一個 shell&#xff0c;誕生于 1971 年&#xff0c;由計算機領域的傳奇人物 Ken Thompson 開發。在那個計算機技術剛剛起步、硬件資源極度匱乏的年代&a…

MySQL B+ 樹索引詳解:從原理到實戰優化

引言在現代數據庫應用中&#xff0c;查詢效率是影響系統性能的關鍵因素之一。而索引&#xff0c;尤其是 B 樹索引&#xff0c;是 MySQL 中最常用、最重要的性能優化手段。正確使用索引可以將查詢時間從毫秒級降低到微秒級&#xff0c;極大地提升應用響應速度。1. B 樹索引的重要…

計算機內存中的整型存儲奧秘、大小端字節序及其判斷方法

目錄 一、回顧與引入&#xff1a;整數在內存中的存儲方式 為什么要采用補碼存儲&#xff1f; 二、大小端字節序及其判斷方法 1、什么是大小端&#xff1f; 2、為什么存在大小端&#xff1f; 3、練習 練習1&#xff1a;簡述大小端概念并設計判斷程序&#xff08;百度面試…

Redis 最常用的 5 種數據類型

Redis 支持多種靈活的數據類型&#xff0c;每種類型針對特定場景優化。以下是 **Redis 最常用的 5 種數據類型**及其核心特點和應用場景&#xff1a;1. 字符串&#xff08;String&#xff09;描述&#xff1a;最基本的數據類型&#xff0c;可存儲文本、數字&#xff08;整數/浮…