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è)簡(jiǎn)單的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 json字符串轉(zhuǎn)JSONObject和JSONArray以及取值的實(shí)例
Java Email郵件發(fā)送簡(jiǎn)單實(shí)現(xiàn)介紹
Eclipse中查看android工程代碼出現(xiàn)"android.jar has no source attachment
maven打包時(shí)候修改包名稱帶上git版本號(hào)和打包時(shí)間方式
基于JavaMail實(shí)現(xiàn)簡(jiǎn)單郵件發(fā)送
Spring實(shí)現(xiàn)IoC的多種方式小結(jié)

