lua腳本+Redission實現分布式鎖

實現分布式鎖最簡單的一種方式:基于Redis

不論是本地鎖還是分布式鎖,核心都在于“互斥”。

在 Redis 中, SETNX 命令是可以幫助我們實現互斥。SETNXset if not exists?(對應 Java 中的 setIfAbsent 方法),如果 key 不存在的話,才會設置 key 的值。如果 key 已經存在, SETNX 啥也不做。

SETNX lockKey uniqueValue
(integer) 1
SETNX lockKey uniqueValue
(integer) 0

釋放鎖的話,直接通過?DEL?命令刪除對應的 key 即可。

DEL lockKey
(integer) 1

為了防止誤刪到其他的鎖,這里我們建議使用 Lua 腳本通過 key 對應的 value(唯一值)來判斷。

選用 Lua 腳本是為了保證解鎖操作的原子性。因為 Redis 在執行 Lua 腳本時,可以以原子性的方式執行,從而保證了鎖釋放操作的原子性。

// 釋放鎖時,先比較鎖對應的 value 值是否相等,避免鎖的誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

這是一種最簡易的 Redis 分布式鎖實現,實現方式比較簡單,性能也很高效。不過,這種方式實現分布式鎖存在一些問題。就比如應用程序遇到一些問題比如釋放鎖的邏輯突然掛掉,可能會導致鎖無法被釋放,進而造成共享資源無法再被其他線程/進程訪問。

為了避免鎖無法被釋放,我們可以想到的一個解決辦法就是:給這個 key(也就是鎖) 設置一個過期時間?。

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
  • lockKey:加鎖的鎖名;
  • uniqueValue:能夠唯一標識鎖的隨機字符串;
  • NX:只有當 lockKey 對應的 key 值不存在的時候才能 SET 成功;
  • EX:過期時間設置(秒為單位)EX 3 標示這個鎖有一個 3 秒的自動過期時間。與 EX 對應的是 PX(毫秒為單位),這兩個都是過期時間設置。

這樣確實可以解決問題,不過,這種解決辦法同樣存在漏洞:如果操作共享資源的時間大于過期時間,就會出現鎖提前過期的問題,進而導致分布式鎖直接失效。如果鎖的超時時間設置過長,又會影響到性能。

你或許在想:如果操作共享資源的操作還未完成,鎖過期時間能夠自己續期就好了!

好 它來了

Redission+lua腳本實現互斥鎖

Redisson 是一個開源的 Java 語言 Redis 客戶端,提供了很多開箱即用的功能,不僅僅包括多種分布式鎖的實現。并且,Redisson 還支持 Redis 單機、Redis Sentinel、Redis Cluster 等多種部署架構。

Redisson 中的分布式鎖自帶自動續期機制,使用起來非常簡單,原理也比較簡單。在Redisson中需要手動加鎖,并且可以控制鎖的失效時間和等待時間。當鎖住的一個業務還沒有執行完成時,Redisson會引入一個看門狗機制,每隔一段時間檢查當前業務是否還持有鎖。如果持有,就增加加鎖的持有時間。

實踐一下:優惠券秒殺一人一單防止超賣實現步驟

1 引入依賴
<dependencies><!-- Redisson --><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.1</version></dependency><!-- Spring Boot Data Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Spring Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
2. Redisson 配置
@Configuration
public class RedissonConfig {@Value("${spring.redis.host}")private String host;@Value("${spring.redis.port}")private String port;@Value("${spring.redis.password}")private String password;@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password == null || password.isEmpty() ? null : password).setDatabase(0);return Redisson.create(config);}
}
3. 常量類
public interface RedisConstants {String SECKILL_STOCK_KEY = "seckill:stock:";String SECKILL_ORDER_KEY = "seckill:order:";String LOCK_COUPON_KEY = "lock:coupon:";long LOCK_TIMEOUT = 30; // 鎖超時時間(秒)
}
4. service
@Service
public class CouponService {@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate CouponMapper couponMapper;@Autowiredprivate OrderMapper orderMapper;// 秒殺優惠券public Result seckillCoupon(Long couponId, Long userId) {// 1. 生成鎖keyString lockKey = RedisConstants.LOCK_COUPON_KEY + couponId;// 2. 獲取Redisson鎖RLock lock = redissonClient.getLock(lockKey);// 3. 嘗試獲取鎖,等待10秒,自動釋放時間30秒 這里沒有啟用看門狗 因為設置了自動30s超時釋放 boolean isLocked = false;try {isLocked = lock.tryLock(10, RedisConstants.LOCK_TIMEOUT, TimeUnit.SECONDS);if (!isLocked) {return Result.fail("搶購失敗,請稍后再試");}// 4. 執行Lua腳本校驗庫存和用戶訂單String script = buildSeckillScript();List<String> keys = Arrays.asList(couponId.toString());List<String> args = Arrays.asList(userId.toString());Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),keys, args);// 5. 處理腳本返回結果if (result == null) {return Result.fail("系統異常");}if (result == 0) {return Result.fail("庫存不足");}if (result == -1) {return Result.fail("每個用戶限購一次");}// 6. 創建訂單(這里簡化處理,實際項目可能需要更復雜的訂單創建邏輯)createOrder(couponId, userId);return Result.ok("搶購成功");} catch (InterruptedException e) {Thread.currentThread().interrupt();return Result.fail("系統異常");} finally {// 7. 釋放鎖if (isLocked && lock.isHeldByCurrentThread()) {lock.unlock();}}}// 構建秒殺Lua腳本private String buildSeckillScript() {return "local stockKey = 'seckill:stock:' .. KEYS[1] " +"local orderKey = 'seckill:order:' .. KEYS[1] " +"local userId = ARGV[1] " +"local stock = tonumber(redis.call('get', stockKey) or 0) " +"if stock <= 0 then return 0 end " +"if redis.call('sismember', orderKey, userId) == 1 then return -1 end " +"redis.call('decr', stockKey) " +"redis.call('sadd', orderKey, userId) " +"return 1";}// 創建訂單private void createOrder(Long couponId, Long userId) {// 查詢優惠券信息Coupon coupon = couponMapper.selectById(couponId);// 創建訂單Order order = new Order();order.setUserId(userId);order.setCouponId(couponId);order.setPayAmount(coupon.getPrice());// 設置其他訂單字段...// 保存訂單orderMapper.insert(order);}
}
5. controller
@RestController
@RequestMapping("/api/coupon")
public class CouponController {@Autowiredprivate CouponService couponService;@PostMapping("/seckill/{couponId}")public Result seckillCoupon(@PathVariable Long couponId, @RequestHeader("userId") Long userId) {return couponService.seckillCoupon(couponId, userId);}
}
6. 初始化庫存和優惠券
@Service
public class InitService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate CouponMapper couponMapper;// 初始化優惠券庫存到Redis@PostConstructpublic void initCouponStock() {// 查詢所有可用優惠券List<Coupon> coupons = couponMapper.selectList(new QueryWrapper<Coupon>().eq("status", 1).gt("stock", 0).lt("start_time", LocalDateTime.now()).gt("end_time", LocalDateTime.now()));// 將優惠券庫存加載到Redisfor (Coupon coupon : coupons) {stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + coupon.getId(), coupon.getStock().toString());}}
}

lua腳本詳解

-- 獲取庫存鍵和訂單鍵
local stockKey = 'seckill:stock:' .. KEYS[1] 
local orderKey = 'seckill:order:' .. KEYS[1] -- 獲取用戶ID參數
local userId = ARGV[1] -- 獲取當前庫存(如果不存在則為0)
local stock = tonumber(redis.call('get', stockKey) or 0) -- 檢查庫存是否不足
if stock <= 0 then return 0 end -- 檢查用戶是否已購買過
if redis.call('sismember', orderKey, userId) == 1 then return -1 end -- 扣減庫存
redis.call('decr', stockKey) -- 記錄用戶已購買
redis.call('sadd', orderKey, userId) -- 返回成功標識
return 1

看門狗機制在哪體現捏??

當你調用tryLock()方法沒有顯式指定鎖的持有時間(即只傳等待時間,不傳釋放時間)時,看門狗機制會自動生效。例如:

// 啟用看門狗:不指定leaseTime,使用默認續期時間(默認30秒)
lock.tryLock(10, null, TimeUnit.SECONDS);// 禁用看門狗:顯式指定leaseTime,鎖到期后不會續期
lock.tryLock(10, 30, TimeUnit.SECONDS); // 你提供的代碼使用這種方式

如果啟用dog 建議增加配置來調整看門狗的默認續期時間:?

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setLockWatchdogTimeout(60 * 1000); // 設置看門狗續期時間為60秒

Redisson實現的分布式鎖是可重入的嗎?它是怎么實現的?

是的,Redisson 實現的分布式鎖是可重入的。可重入鎖允許同一個線程多次獲取同一把鎖而不會被阻塞,這可以有效避免死鎖問題,同時讓代碼邏輯更清晰。

Redisson 如何實現可重入鎖

Redisson 是基于 Redis 的哈希結構來存儲鎖信息的。打個比方,我們有個叫 “myLock” 的鎖,這就是鎖的唯一標識,相當于哈希結構里的 Key。而每個嘗試獲取鎖的線程都有自己唯一的標識,像線程 ID 或者 UUID,這就是哈希結構里的 Field。線程獲取鎖的次數,也就是重入次數,就是哈希結構里的 Value。

加鎖過程

當一個線程想去獲取鎖的時候,Redisson 首先會檢查這個鎖對應的 Key 存不存在。要是不存在,那就說明當前沒有線程持有這把鎖,Redisson 就會創建這個鎖,把 Field 設成當前線程的標識,Value 設為 1,同時給鎖設置一個過期時間。要是鎖已經存在,Redisson 就會去檢查 Field 是不是和當前線程的標識一樣。如果一樣,那就意味著當前線程已經持有這把鎖了,Redisson 就把 Value 加 1,并且刷新鎖的過期時間。要是不一樣,那就表示鎖被其他線程占著,當前線程就得等著鎖被釋放。

釋放鎖過程

釋放鎖的時候,Redisson 會先看看鎖的 Field 和當前線程標識是不是一致。如果一致,就把 Value 減 1。要是減完之后 Value 變成 0 了,那就說明當前線程已經完全釋放了這把鎖,Redisson 就把鎖對應的 Key 刪除。要是 Value 還大于 0,說明當前線程還有重入的情況,還持有鎖,Redisson 就刷新一下鎖的過期時間。

防止死鎖

Redisson 在防止死鎖方面也有很實用的機制。一方面,加鎖的時候會給鎖設置過期時間,就算某個線程出問題了,一直不釋放鎖,到時間了鎖也會自動被刪除。另一方面,它的可重入機制也能避免因為線程嵌套調用導致的死鎖。

可重入鎖

Redisson 的可重入鎖優勢也很明顯。從線程安全角度看,只有持有鎖的線程才能釋放鎖,這就保證了不會出現線程安全問題。在性能上,它通過 Lua 腳本確保加鎖和釋放鎖的操作是原子性的,避免了競態條件,效率很高。

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

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

相關文章

設計模式之工廠模式(二):實際案例

設計模式之工廠模式(一) 在閱讀Qt網絡部分源碼時候&#xff0c;發現在某處運用了工廠模式&#xff0c;而且編程技巧也用的好&#xff0c;于是就想分享出來&#xff0c;供大家參考&#xff0c;理解的不對的地方請多多指點。 以下是我整理出來的類圖&#xff1a; 關鍵說明&#x…

MultiTTS 1.7.6 | 最強離線語音引擎,提供多音色無障礙朗讀功能,附帶語音包

MultiTTS是一款免費且支持離線使用的文本轉語音&#xff08;TTS&#xff09;工具&#xff0c;旨在為用戶提供豐富的語音包選項&#xff0c;實現多音色無障礙朗讀功能。這款應用程序特別適合用于閱讀軟件中的離線聽書體驗&#xff0c;提供了多樣化的語音選擇&#xff0c;使得聽書…

歌曲《忘塵谷》基于C語言的歌曲調性檢測技術解析

引言 在音樂分析與數字信號處理領域&#xff0c;自動檢測歌曲調性是一項基礎且關鍵的任務。本文以C語言為核心&#xff0c;結合音頻處理庫&#xff08;libsndfile&#xff09;和快速傅里葉變換庫&#xff08;FFTW&#xff09;&#xff0c;探討如何實現調性檢測&#xff0c;并通…

大某麥演唱會門票如何自動搶

引言 僅供學習研究&#xff0c;歡迎交流 搶票難&#xff0c;難于上青天&#xff01;無論是演唱會、話劇還是體育賽事&#xff0c;大麥網的票總是秒光。大麥網是國內知名的票務平臺&#xff0c;熱門演出票往往一票難求。手動搶票不僅耗時&#xff0c;還容易錯過機會。作為一名…

1.3.3 tinyalsa詳細介紹

一、TinyALSA 的背景與設計目標 1. 誕生背景 Android 音頻需求的演變&#xff1a;早期 Android 系統使用標準 ALSA&#xff08;Advanced Linux Sound Architecture&#xff09;的用戶空間庫 alsa-lib&#xff0c;但因其復雜性&#xff08;代碼龐大、依賴較多&#xff09;和資…

超越合并速度(merge speed):AI如何重塑開發者協作

李升偉 編譯 AI 關于現代開發的討論通常圍繞著單一指標&#xff1a;合并速度&#xff08;merge speed&#xff09;。但在這一表面測量之下&#xff0c;隱藏著開發團隊工作方式的一種更深刻的變革。讓我們探討開發者協作的微妙演變方式以及為什么傳統生產力指標只講述了一部分故…

如何找正常運行虛擬機

1.新建虛擬機。Linux centos7&#xff0c;給虛擬機改個名字不要放在c盤 2.安裝操作系統。cd/dvd->2009.iso 啟動虛擬機

深度學習:系統性學習策略(二)

深度學習的系統性學習策略 基于《認知覺醒》與《認知驅動》的核心方法論,結合深度學習的研究實踐,從認知與技能雙重維度總結以下系統性學習策略: 一、認知覺醒:構建深度學習的思維操作系統 三重腦區協同法則 遵循**本能腦(舒適區)-情緒腦(拉伸區)-理智腦(困難區)**的…

如何使用CSS解決一行有三個元素,前兩個元素靠左排列,第三個元素靠右排列的問題

如圖所示&#xff0c;我要把左邊的場館和區域信息靠左排列&#xff0c;價格信息靠右排列。如何使用CSS實現這種效果&#xff1f; 在這里&#xff0c;我使用了flexbox彈性布局&#xff0c;以下是我的實現代碼 .name-info {display: flex;gap: 2px;justify-content: space-betwee…

USB傳輸模式

USB有四種傳輸模式: 控制傳輸, 中斷傳輸, 同步傳輸, 批量傳輸 1. 中斷傳輸 中斷傳輸一般用于小批量, 非連續的傳輸. 對實時性要求較高. 常見的使用此傳輸模式的設備有: 鼠標, 鍵盤等. 要注意的是, 這里的 “中斷” 和我們常見的中斷概念有差異. Linux中的中斷是設備主動發起的…

【Python 變量類型】

Python 是一種動態類型語言&#xff0c;變量類型在運行時自動確定&#xff0c;無需顯式聲明。以下是 Python 中核心變量類型的分類與用法詳解&#xff1a; 一、基本數據類型 1. 數值類型 整數 (int) 支持正負數、零和二進制/八進制/十六進制表示&#xff1a; a 42 b 0o52 #…

Python基礎:類的深拷貝與淺拷貝-->with語句的使用及三個庫:matplotlib基本畫圖-->pandas之Series創建

一.類的深拷貝與淺拷貝 class CPU():pass class Disk():passclass Computer():#計算機由CPU和硬盤組成def __init__(self):self.cpu CPU()self.disk Disk()cpu CPU()#創建一個CPU對象 disk Disk()#創建一個硬盤對象#創建一個計算機對象 com Computer(cpu,disk) #變量&…

【SSM-SpringMVC(二)】Spring接入Web環境!本篇開始研究SpringMVC的使用!SpringMVC數據響應和獲取請求數據

SpringMVC的數據響應方式 頁面跳轉 直接返回字符串通過ModelAndView對象返回 回寫數據 直接返回字符串返回對象或集合 頁面跳轉&#xff1a; 返回字符串方式 直接返回字符串&#xff1a;此種方式會將返回的字符串與視圖解析器的前后綴拼接后跳轉 RequestMapping("/con&…

閱文集團C++面試題及參考答案

目錄 能否不使用鎖保證多線程安全? 面向對象的三個特性是什么?請分別解釋。 構造函數和析構函數能否被繼承? C++ 中函數重載是如何實現的? C 語言中是否支持函數重載? 什么是左值和右值?請舉例說明。 C++ 中子類的構造和析構順序是怎樣的? C++ 中虛函數表的變化過…

【親測有效】如何清空但不刪除GitHub倉庫中的所有文件(main分支)

如何清空但不刪除GitHub倉庫中的所有文件&#xff08;main分支&#xff09; 在項目開發過程中&#xff0c;有時我們需要清空GitHub倉庫中的所有文件&#xff0c;同時保留倉庫本身。這種情況常見于項目重構、代碼重寫或者需要重新開始一個項目時。本文將介紹一種有效的方法來清…

前端EXCEL插件,智表ZCELL產品V3.0 版本發布,底層采用canvas全部重構,功能大幅擴展,性能極致提升,滿足千萬級單元格加載

本次更新是底層全部重構&#xff0c;按照現代瀏覽器要求&#xff0c;采用canvas方式進行了重構&#xff0c;預留了將來擴展空間&#xff0c;特別是在大數據量性能提升方面有了較大提升&#xff0c;可以滿足千萬級單元格加載&#xff0c;歡迎大家體驗使用。 體驗地址&#xff1…

3DGS-to-PC:3DGS模型一鍵絲滑轉 點云 or Mesh 【Ubuntu 20.04】【2025最新版!!】

一、引言 3D高斯潑濺(3DGS)是一種新興的三維場景表示方法&#xff0c;可以生成高質量的場景重建結果。然而&#xff0c;要查看這些重建場景&#xff0c;需要特殊的高斯渲染器。大多數3D處理軟件并不兼容3D高斯分布模型&#xff0c;但它們通常都兼容點云文件。 3DGS-to-PC項目提…

OpenHarmony 以太網卡熱插拔事件接口無效

目錄 1.背景 2.解決方案 1.背景 在OpenHarmony中調用以太網熱插拔時間,發現熱插拔沒有任何回調,如下接口 import { ethernet } from @kit.NetworkKit;ethernet.on(interfaceStateChange, (data: object) => {console.log(on interfaceSharingStateChange: + JSON.…

C++ 跨平臺開發挑戰與深度解決方案:從架構設計到實戰優化

C 憑借其高性能與底層控制能力&#xff0c;在游戲引擎、嵌入式系統、工業軟件等領域占據核心地位。然而&#xff0c;跨平臺開發過程中需應對硬件架構多樣性、操作系統差異性、編譯工具鏈碎片化等復雜問題。本文將從底層架構到上層應用&#xff0c;系統性剖析 C 跨平臺開發的核心…

什么是 ANR 如何避免它

一、什么是 ANR&#xff1f; ANR&#xff08;Application Not Responding&#xff09; 是 Android 系統在應用程序主線程&#xff08;UI 線程&#xff09;被阻塞超過一定時間后觸發的錯誤機制。此時系統會彈出一個對話框提示用戶“應用無響應”&#xff0c;用戶可以選擇等待或強…