SpringBoot整合Redis正確的實(shí)現(xiàn)分布式鎖的示例代碼
前言
最近在做分塊上傳的業(yè)務(wù),使用到了Redis來維護(hù)上傳過程中的分塊編號。
每上傳完成一個(gè)分塊就獲取一下文件的分塊集合,加入新上傳的編號,手動(dòng)接口測試下是沒有問題的,前端通過并發(fā)上傳調(diào)用就出現(xiàn)問題了,并發(fā)的get再set,就會(huì)存在覆蓋寫現(xiàn)象,導(dǎo)致最后的分塊數(shù)據(jù)不對,不能觸發(fā)分塊合并請求。
遇到并發(fā)二話不說先上鎖,針對執(zhí)行代碼塊加了一個(gè)JVM鎖之后問題就解決了。
仔細(xì)一想還是不太對,項(xiàng)目是分布式部署的,做了負(fù)載均衡,一個(gè)節(jié)點(diǎn)的代碼被鎖住了,請求輪詢到其他節(jié)點(diǎn)還是可以進(jìn)行覆蓋寫,并沒有解決到問題啊
沒辦法,只有用上分布式鎖了。之前對于分布式鎖的理論還是很熟悉的,沒有比較好的應(yīng)用場景就沒寫過具體代碼,趁這個(gè)機(jī)會(huì)就學(xué)習(xí)使用一下分布式鎖。
理論
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。是為了解決分布式系統(tǒng)中,不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)共享同一個(gè)資源的問題,它通常會(huì)采用互斥來保證程序的一致性

通常的實(shí)現(xiàn)方式有三種:
- 基于 MySQL 的悲觀鎖來實(shí)現(xiàn)分布式鎖,這種方式使用的最少,這種實(shí)現(xiàn)方式的性能不好,且容易造成死鎖,并且MySQL本來業(yè)務(wù)壓力就很大了,再做鎖也不太合適
- 基于 Redis 實(shí)現(xiàn)分布式鎖,單機(jī)版可用setnx實(shí)現(xiàn),多機(jī)版建議用Radission
- 基于 ZooKeeper 實(shí)現(xiàn)分布式鎖,利用 ZooKeeper 順序臨時(shí)節(jié)點(diǎn)來實(shí)現(xiàn)
為了確保分布式鎖可用,我們至少要確保鎖的實(shí)現(xiàn)同時(shí)滿足以下四個(gè)條件:
- 互斥性。在任意時(shí)刻,只有一個(gè)客戶端能持有鎖。
- 不會(huì)發(fā)生死鎖。即使有一個(gè)客戶端在持有鎖的期間崩潰而沒有主動(dòng)解鎖,也能保證后續(xù)其他客戶端能加鎖。
- 具有容錯(cuò)性。只要大部分的Redis節(jié)點(diǎn)正常運(yùn)行,客戶端就可以加鎖和解鎖。
- 解鈴還須系鈴人。加鎖和解鎖必須是同一個(gè)客戶端,客戶端自己不能把別人加的鎖給解了。
本文就使用的是Redis的setnx實(shí)現(xiàn),如果Redis是多機(jī)版的可以去了解下Radssion,封裝的就特別的好,也是官方推薦的
代碼
1. 加依賴
引入Spring Boot和Redis整合的快速使用依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 加配置
application.properties中加入Redis連接相關(guān)配置
spring.redis.host=xxx spring.redis.port=6379 spring.redis.database=0 spring.redis.password=xxx spring.redis.timeout=10000 # 設(shè)置jedis連接池 spring.redis.jedis.pool.max-active=50 spring.redis.jedis.pool.min-idle=20
3. 重寫Redis的序列化規(guī)則
默認(rèn)使用的JDK的序列化,不自己設(shè)置一下Redis中的數(shù)據(jù)是看不懂的
/**
* @author Chkl
* @create 2020/6/7
* @since 1.0.0
*/
@Component
public class RedisConfig {
/**
* 改造RedisTemplate,重寫序列化規(guī)則,避免存入序列化內(nèi)容看不懂
* @param connectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
// 設(shè)置key和value的序列化規(guī)則
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
4. 如何正確的上鎖
直接上代碼
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private long timeout = 3000;
/**
* 上鎖
* @param key 鎖標(biāo)識
* @param value 線程標(biāo)識
* @return 上鎖狀態(tài)
*/
public boolean lock(String key, String value) {
long start = System.currentTimeMillis();
while (true) {
//檢測是否超時(shí)
if (System.currentTimeMillis() - start > timeout) {
return false;
}
//執(zhí)行set命令
Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);//1
//是否成功獲取鎖
if (absent) {
return true;
}
return false;
}
}
}
核心代碼就是
Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);
setIfAbsent方法就相當(dāng)于命令行下的Setnx方法,指定的 key 不存在時(shí),為 key 設(shè)置指定的值
參數(shù)分別是key、value、超時(shí)時(shí)間和時(shí)間單位
- key,表示針對于這段資源的唯一標(biāo)識
- value,表示針對于這個(gè)線程的唯一標(biāo)識。為什么有了key了還需要設(shè)置value呢,就是為了滿足四個(gè)條件的最后一個(gè):解鈴還須系鈴人。只有通過key和value的組合才能保證解鎖時(shí)是同一個(gè)線程來解鎖
- 超時(shí)時(shí)間,必須和setnx一起進(jìn)行操作,不能再setnx結(jié)束后再執(zhí)行。如果加鎖成功了,還沒有設(shè)置過期時(shí)間就宕機(jī)了,鎖就永遠(yuǎn)不會(huì)過期,變成死鎖
5. 如何正確解鎖
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private DefaultRedisScript<Long> redisScript;
private static final Long RELEASE_SUCCESS = 1L;
/**
* 解鎖
* @param key 鎖標(biāo)識
* @param value 線程標(biāo)識
* @return 解鎖狀態(tài)
*/
public boolean unlock(String key, String value) {
//使用Lua腳本:先判斷是否是自己設(shè)置的鎖,再執(zhí)行刪除
Long result = redisTemplate.execute(redisScript, Arrays.asList(key,value));
//返回最終結(jié)果
return RELEASE_SUCCESS.equals(result);
}
/**
* @return lua腳本
*/
@Bean
public DefaultRedisScript<Long> defaultRedisScript() {
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
return defaultRedisScript;
}
}
解鎖過程需要兩步操作
1.判斷操作線程是否是加鎖的線程
2.如果是加鎖線程,執(zhí)行解鎖操作
這兩步操作也需要原子的進(jìn)行操作,但是Redis不支持這兩步的合并的操作,所以,就只有使用lua腳本實(shí)現(xiàn)來保證原子性咯
如果在判斷是加鎖的線程之后,并且執(zhí)行解鎖之前,鎖到期了,被其他線程獲得鎖了,這時(shí)候再進(jìn)行解鎖就會(huì)解掉其他線程的鎖,使得不滿足解鈴還須系鈴人
6. 實(shí)際應(yīng)用
沒有使用分布式鎖時(shí)的保存文件分塊的代碼
/**
* 保存文件分塊編號到redis
* @param chunkNumber 分塊號
* @param identifier 文件唯一編號
* @return 文件分塊的大小
*/
@Override
public Integer saveChunk(Integer chunkNumber, String identifier) {
//從Redis獲取已經(jīng)存在的分塊編號集合
Set<Integer> oldChunkNumber = (Set<Integer>) JSON.parseObject(redisOperator.get("chunkNumberList_"+identifier),Set.class);
//如果不存在分塊集合,創(chuàng)建一個(gè)集合
if (Objects.isNull(oldChunkNumber)) {
Set<Integer> newChunkNumber = new HashSet<>();
newChunkNumber.add(chunkNumber);
redisOperator.set("chunkNumberList_"+identifier, JSON.toJSONString(newChunkNumber),36000);
return newChunkNumber.size();
//如果分塊集合已經(jīng)存在了,就添加一個(gè)編號
} else {
oldChunkNumber.add(chunkNumber);
redisOperator.set("chunkNumberList_"+identifier, JSON.toJSONString(oldChunkNumber),36000);
return oldChunkNumber.size();
}
}
存在的問題是:當(dāng)并發(fā)的請求進(jìn)來之后,可能獲取同一個(gè)狀態(tài)的集合進(jìn)行修改,修改后直接寫入,造成同一個(gè)狀態(tài)獲得的集合操作線程覆蓋寫的現(xiàn)象
使用分布式鎖保證同時(shí)只能有一個(gè)線程能獲取到集合并進(jìn)行修改,避免了覆蓋寫現(xiàn)象
使用分布式鎖代碼
/**
* 保存文件分塊編號到redis
* @param chunkNumber 分塊號
* @param identifier 文件唯一編號
* @return 文件分塊的大小
*/
@Override
public Integer saveChunk(Integer chunkNumber, String identifier) {
//通過UUID生成一個(gè)請求線程識別標(biāo)志作為鎖的value
String threadUUID = CoreUtil.getUUID();
//上鎖,以共享資源標(biāo)識:文件唯一編號,作為key,以線程標(biāo)識UUID作為value
redisLock.lock(identifier,threadUUID);
//從Redis獲取已經(jīng)存在的分塊編號集合
Set<Integer> oldChunkNumber = (Set<Integer>) JSON.parseObject(redisOperator.get("chunkNumberList_"+identifier),Set.class);
//如果不存在分塊集合,創(chuàng)建一個(gè)集合
if (Objects.isNull(oldChunkNumber)) {
Set<Integer> newChunkNumber = new HashSet<>();
newChunkNumber.add(chunkNumber);
redisOperator.set("chunkNumberList_"+identifier, JSON.toJSONString(newChunkNumber),36000);
//解鎖
redisLock.unlock(identifier,threadUUID);
return newChunkNumber.size();
//如果分塊集合已經(jīng)存在了,就添加一個(gè)編號
} else {
oldChunkNumber.add(chunkNumber);
redisOperator.set("chunkNumberList_"+identifier, JSON.toJSONString(oldChunkNumber),36000);
//解鎖
redisLock.unlock(identifier,threadUUID);
return oldChunkNumber.size();
}
}
代碼中使用的共享資源標(biāo)識是文件唯一編號identifier,它能標(biāo)識加鎖代碼段中的唯一資源,即key為"chunkNumberList_"+identifier的集合
代碼中使用的線程唯一標(biāo)識是UUID,能保證加鎖和解鎖時(shí)獲取的標(biāo)識不會(huì)重復(fù)
到此這篇關(guān)于SpringBoot整合Redis正確的實(shí)現(xiàn)分布式鎖的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot整合Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot利用注解來實(shí)現(xiàn)Redis分布式鎖
- Spring?Boot?集成Redisson實(shí)現(xiàn)分布式鎖詳細(xì)案例
- springboot 集成redission 以及分布式鎖的使用詳解
- SpringBoot之使用Redis實(shí)現(xiàn)分布式鎖(秒殺系統(tǒng))
- Redis分布式鎖升級版RedLock及SpringBoot實(shí)現(xiàn)方法
- SpringBoot使用Redis實(shí)現(xiàn)分布式鎖
- SpringBoot使用Redisson實(shí)現(xiàn)分布式鎖(秒殺系統(tǒng))
- SpringBoot集成Redisson實(shí)現(xiàn)分布式鎖的方法示例
- springboot+redis分布式鎖實(shí)現(xiàn)模擬搶單
- Spring?Boot?3.0x的Redis?分布式鎖的概念和原理
相關(guān)文章
java中hashmap容量的初始化實(shí)現(xiàn)
這篇文章主要介紹了java中hashmap容量的初始化實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
通過FeignClient調(diào)用微服務(wù)提供的分頁對象IPage報(bào)錯(cuò)的解決
這篇文章主要介紹了通過FeignClient調(diào)用微服務(wù)提供的分頁對象IPage報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
淺析Java.IO輸入輸出流 過濾流 buffer流和data流
這篇文章主要介紹了Java.IO輸入輸出流 過濾流 buffer流和data流的相關(guān)資料,本文給大家介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10
mybatis?log4j2打印sql+日志實(shí)例代碼
在學(xué)習(xí)mybatis的時(shí)候,如果用log4j2來協(xié)助查看調(diào)試信息,則會(huì)大大提高學(xué)習(xí)的效率,加快debug速度,下面這篇文章主要給大家介紹了關(guān)于mybatis?log4j2打印sql+日志的相關(guān)資料,需要的朋友可以參考下2022-08-08
Spring學(xué)習(xí)筆記2之表單數(shù)據(jù)驗(yàn)證、文件上傳實(shí)例代碼
這篇文章主要介紹了Spring學(xué)習(xí)筆記2之表單數(shù)據(jù)驗(yàn)證、文件上傳 的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
Java策略模式的簡單應(yīng)用實(shí)現(xiàn)方法
這篇文章主要介紹了Java策略模式的簡單應(yīng)用實(shí)現(xiàn)方法,需要的朋友可以參考下2014-02-02
Spring boot項(xiàng)目redisTemplate實(shí)現(xiàn)輕量級消息隊(duì)列的方法
這篇文章主要給大家介紹了關(guān)于Spring boot項(xiàng)目redisTemplate實(shí)現(xiàn)輕量級消息隊(duì)列的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Spring boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
JFreeChart動(dòng)態(tài)畫折線圖的方法
這篇文章主要為大家詳細(xì)介紹了JFreeChart動(dòng)態(tài)畫折線圖的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06

