SpringBoot基于Redis的分布式鎖實(shí)現(xiàn)過程記錄
一、概述
什么是分布式鎖
在單機(jī)環(huán)境中,一般在多并發(fā)多線程場(chǎng)景下,出現(xiàn)多個(gè)線程去搶占一個(gè)資源,這個(gè)時(shí)候會(huì)出現(xiàn)線程同步問題,造成執(zhí)行的結(jié)果沒有達(dá)到預(yù)期。我們會(huì)用線程間加鎖的方式,比如synchronized,lock,volatile,以及JVM并發(fā)包中提供的其他工具類去處理此問題。
但是隨著技術(shù)的發(fā)展,分布式系統(tǒng)的出現(xiàn),各個(gè)應(yīng)用服務(wù)都部署在不同節(jié)點(diǎn),由各自的JVM去操控,資源已經(jīng)不是在 線程 之間的共享,而是變成了 進(jìn)程 之間的共享,以上解決線程同步問題的辦法已經(jīng)無法滿足。
因此,引入了分布式鎖的概念。
分布式鎖,既在分布式部署的環(huán)境下,通過在外部設(shè)置鎖,讓客戶端之間互斥,當(dāng)多應(yīng)用發(fā)生對(duì)共享資源的搶占時(shí),該資源同一時(shí)刻只能一個(gè)應(yīng)用訪問,從而保證數(shù)據(jù)一致性
分布式鎖滿足條件
- 互斥性:即同一時(shí)刻只能一個(gè)客戶端獲得鎖,其他客戶端必須等待獲取鎖的客戶端主動(dòng)釋放鎖或鎖超時(shí)后再次對(duì)資源進(jìn)行搶占。
- 避免死鎖:這把鎖在一段有限的時(shí)間之后,一定會(huì)被釋放(正常釋放或異常釋放),否則當(dāng)一個(gè)客戶端線程獲得鎖后沒有主動(dòng)釋放,也沒有設(shè)置超時(shí)時(shí)間,中途宕機(jī)等原因,其他線程就會(huì)一直獲取不了鎖
- 具備可重入特性:同一個(gè)線程可重復(fù)可遞歸調(diào)用的鎖,在外層使用鎖之后,在內(nèi)層仍然可以使用,如果沒有可重入鎖的支持,在第二次嘗試獲得鎖時(shí)將會(huì)進(jìn)入死鎖狀態(tài)。
- 高可用:獲取或釋放鎖的機(jī)制必須高可用且性能佳
分布式鎖的重要性不言而喻,原因不在贅述,每一位菜鳥都有理由掌握它。提到分布式鎖,解決方案更是烏泱烏泱的,如:
- 直接通過關(guān)系型數(shù)據(jù)庫實(shí)現(xiàn)
- 基于Redission實(shí)現(xiàn)
- 基于Apache Curator實(shí)現(xiàn)
- …
本文暫時(shí)先介紹一種,基于Redission實(shí)現(xiàn)的方式
二、環(huán)境搭建
有一個(gè)簡單的SpringBoot環(huán)境即可,便于測(cè)試:
依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
配置
server: port: 7077 spring: redis: host: 192.144.228.170 database: 0
啟動(dòng)及配置類
package com.ideax.distributed; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class DistributedApplication { public static void main(String[] args) { SpringApplication.run(DistributedApplication.class,args); } /** * 配置redisson客戶端 * @return org.redisson.Redisson * @author zhangxs * @date 2022-01-06 10:01 */ @Bean public Redisson redisson(){ // 單機(jī)模式 Config config = new Config(); config.useSingleServer().setAddress("redis://192.144.228.170:6379").setDatabase(0); return (Redisson) Redisson.create(config); } }
三、模擬一個(gè)庫存扣減的場(chǎng)景
package com.ideax.distributed.controller; import org.redisson.Redisson; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * 庫存 前端控制器 * @author zhangxs * @date 2022-01-06 09:46 */ @RequestMapping("/inventory") @RestController public class InventoryController { @Autowired private StringRedisTemplate redisTemplate; @Autowired private Redisson redisson; @GetMapping("/minus") public ResponseEntity<String> minusInventory(){ // 分布式高并發(fā)場(chǎng)景下,這樣肯定不行 synchronized (this) { int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get("stock"))); if (stock > 0) { int currentStock = stock - 1; redisTemplate.opsForValue().set("stock", currentStock + ""); System.out.println("扣減成功,當(dāng)前庫存為" + currentStock); } else { System.out.println("庫存不足,扣減失?。?); } } return ResponseEntity.ok("success"); } @GetMapping("/minus1") public ResponseEntity<String> minusInventory1(){ // 相當(dāng)于setnx命令 String lockKey = "lockKey"; // 務(wù)必加try-finally,因?yàn)槿绻?wù)掛了,鎖還得釋放 String clientId = UUID.randomUUID().toString(); try { // 相當(dāng)于加鎖 // Boolean absent = redisTemplate.opsForValue().setIfAbsent(lockKey, "zxs"); // 上下兩行不能分開寫,如果這中間報(bào)異常了,依然出現(xiàn)死鎖 // redisTemplate.expire(lockKey,10, TimeUnit.SECONDS); Boolean absent = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId,30,TimeUnit.SECONDS); if (!absent) { return ResponseEntity.notFound().build(); } int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get("stock"))); if (stock > 0) { int currentStock = stock - 1; redisTemplate.opsForValue().set("stock", currentStock + ""); System.out.println("扣減成功,當(dāng)前庫存為" + currentStock); } else { System.out.println("庫存不足,扣減失??!"); } } finally { // 如果系統(tǒng)掛了呢,finally也不起作用了,因此還需要設(shè)置超時(shí)時(shí)間 // 釋放鎖之前,判斷一下,務(wù)必釋放的鎖是自己持有的鎖 if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } return ResponseEntity.ok("success"); } /** * 終極方案 */ @GetMapping("/minus2") public ResponseEntity<String> minusInventory2(){ // redisson解決方案 String lockKey = "lockKey"; RLock lock = redisson.getLock(lockKey); try { // 加鎖 lock.lock(); int stock = Integer.parseInt(Objects.requireNonNull(redisTemplate.opsForValue().get("stock"))); if (stock > 0) { int currentStock = stock - 1; redisTemplate.opsForValue().set("stock", currentStock + ""); System.out.println("扣減成功,當(dāng)前庫存為" + currentStock); } else { System.out.println("庫存不足,扣減失??!"); } } finally { // 釋放鎖 lock.unlock(); } return ResponseEntity.ok("success"); } }
四、總結(jié)
- @GetMapping("/minus") public ResponseEntity<String> minusInventory():初步實(shí)現(xiàn)方式,在單線程環(huán)境下可以使用,但是在分布式高并發(fā)場(chǎng)景下,毫無疑問是肯定不行的
- @GetMapping("/minus1") public ResponseEntity<String> minusInventory1():進(jìn)階實(shí)現(xiàn)方式,在一般的不拘小節(jié)的公司,勉強(qiáng)夠用,但是你需要考慮一下,鎖過期時(shí)間,到底設(shè)置多少才能完美呢?
- @GetMapping("/minus2") public ResponseEntity<String> minusInventory2():終極實(shí)現(xiàn)方式,redisson幫我們解決了上面的實(shí)現(xiàn)方式出現(xiàn)的尷尬情況
到此這篇關(guān)于SpringBoot基于Redis的分布式鎖實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot基于Redis的分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+Redis執(zhí)行l(wèi)ua腳本的5種方式總結(jié)
- Springboot+Redis執(zhí)行l(wèi)ua腳本的項(xiàng)目實(shí)踐
- springboot使用redisTemplate操作lua腳本
- springboot中使用redis并且執(zhí)行調(diào)試lua腳本
- SpringBoot通過redisTemplate調(diào)用lua腳本并打印調(diào)試信息到redis log(方法步驟詳解)
- SpringBoot通過RedisTemplate執(zhí)行Lua腳本的方法步驟
- SpringBoot+Redis執(zhí)行l(wèi)ua腳本的方法步驟
- SpringBoot利用注解來實(shí)現(xiàn)Redis分布式鎖
- 關(guān)于SpringBoot 使用 Redis 分布式鎖解決并發(fā)問題
- springboot+redis+lua實(shí)現(xiàn)分布式鎖的腳本
相關(guān)文章
JAVA多線程實(shí)現(xiàn)生產(chǎn)者消費(fèi)者的實(shí)例詳解
這篇文章主要介紹了JAVA多線程實(shí)現(xiàn)生產(chǎn)者消費(fèi)者的實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-06-06java實(shí)現(xiàn)簡單的給sql語句賦值的示例
這篇文章主要介紹了java實(shí)現(xiàn)簡單的給sql語句賦值的示例,需要的朋友可以參考下2014-05-05SpringBoot2.動(dòng)態(tài)@Value的實(shí)現(xiàn)方式
這篇文章主要介紹了SpringBoot2.動(dòng)態(tài)@Value的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07java實(shí)現(xiàn)幸運(yùn)抽獎(jiǎng)功能
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)幸運(yùn)抽獎(jiǎng)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03