JAVA實(shí)現(xiàn)redis分布式雙重加鎖的示例代碼
背景:在日常開發(fā)過程中,遇到了一個(gè)需求,比如有一個(gè)對(duì)象User(name,age、sex)有三個(gè)屬性,現(xiàn)在需要用戶新增接口中,防止此接口被多人同時(shí)請(qǐng)求訪問,產(chǎn)生了姓名&年齡相同的,還有年齡&性別相同的數(shù)據(jù);
此問題的考慮思路
如果一個(gè)線程調(diào)用用戶新增接口的時(shí)候,在業(yè)務(wù)中通過查詢數(shù)據(jù)庫中是否已有相關(guān)數(shù)據(jù),從而不拋出異常提示,不讓做保存到數(shù)據(jù)庫的操作;這種考慮是我們最常見的考慮內(nèi)容。還有個(gè)問題,如果是外部系統(tǒng),涉及的操作并發(fā)量特別的大,那調(diào)用這個(gè)接口的并發(fā)量也很大的話,單純?cè)谕ㄟ^校驗(yàn)庫中是否有重復(fù)的數(shù)據(jù)防止重復(fù)數(shù)據(jù)插入只能阻止一部分問題數(shù)據(jù)的入庫。如果同時(shí)有兩個(gè)用戶甲乙,填寫的姓名和年齡
(年齡和性別是同樣的考慮方法);此時(shí)從庫中查了,沒有已有的數(shù)據(jù),此時(shí)為了防止這兩個(gè)用戶甲乙操作的重復(fù)數(shù)據(jù)同時(shí)入庫的情況,我們就得加上一個(gè)分布式鎖了(如果在單體應(yīng)用中可以使用synchronized),分布式架構(gòu)中需要使用Redis分布式鎖或者Redission分布式鎖來實(shí)現(xiàn)相應(yīng)的控制了;
在設(shè)計(jì)分布式Redis鎖以避免在新增User時(shí)出現(xiàn)同name和age組合,或者同age和sex組合的情況,你需要構(gòu)建一個(gè)能夠唯一標(biāo)識(shí)這些條件的key。由于Redis鎖通常用于確保操作的原子性,而你的需求是檢查并避免重復(fù)數(shù)據(jù),這里實(shí)際上可能更偏向于使用Redis的其它數(shù)據(jù)結(jié)構(gòu)(如集合、有序集合或哈希表)來輔助實(shí)現(xiàn),而不是僅僅使用單獨(dú)的key-value鎖
使用Redis的key-value鎖的基本思路
1.定義鎖的key:鎖的key應(yīng)該能夠唯一標(biāo)識(shí)你想要保護(hù)的資源或操作。在你的場(chǎng)景中,由于涉及到多個(gè)字段的組合檢查,你可以考慮將這些
字段組合成一個(gè)字符串作為key。例如:
對(duì)于name和age的組合,可以使用user??name:{name}:age:{age}。
對(duì)于age和sex的組合,可以使用user??age:{age}:sex:{sex}。
2.設(shè)置鎖:在嘗試新增User之前,先嘗試設(shè)置這個(gè)鎖。如果鎖設(shè)置成功(即沒有其他進(jìn)程或線程持有這個(gè)鎖),則繼續(xù)執(zhí)行檢查邏輯。
3.檢查并插入:在鎖的保護(hù)下,檢查數(shù)據(jù)庫中是否已經(jīng)存在具有相同name和age或age和sex組合的User。如果不存在,則執(zhí)行插入操作。
4.釋放鎖:無論操作成功還是失敗,最后都要釋放鎖,以便其他進(jìn)程或線程可以獲取鎖并執(zhí)行操作。
結(jié)合Redis數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)避免重復(fù)
然而,更有效的方法可能是使用Redis的集合(Set)或有序集合(Sorted Set)來存儲(chǔ)已經(jīng)存在的組合,并檢查新組合是否已存在。
1.使用集合:
- 對(duì)于name和age的組合,可以創(chuàng)建一個(gè)集合user:name_age,其中每個(gè)元素都是{name}:{age}的字符串。
- 對(duì)于age和sex的組合,可以創(chuàng)建另一個(gè)集合user:age_sex,其中每個(gè)元素都是{age}:{sex}的字符串。
- 在新增User時(shí),先檢查相應(yīng)的集合中是否已經(jīng)存在該組合。如果不存在,則添加到集合中,并執(zhí)行數(shù)據(jù)庫插入操作。
2.使用有序集合(如果需要按某種順序排序): - 類似于集合,但你可以為元素指定一個(gè)分?jǐn)?shù)(score),以便按特定順序存儲(chǔ)和檢索元素。
注意事項(xiàng)
性能考慮:隨著集合中元素的增加,檢查操作可能會(huì)變慢。因此,你可能需要考慮使用哈希表或其他數(shù)據(jù)結(jié)構(gòu)來優(yōu)化查找性能。
事務(wù)性:確保檢查集合和插入數(shù)據(jù)庫的操作是原子性的,以防止在檢查之后但在插入之前發(fā)生數(shù)據(jù)變化。
鎖的超時(shí):設(shè)置鎖的超時(shí)時(shí)間以防止死鎖。
鎖的粒度:根據(jù)你的應(yīng)用場(chǎng)景,你可能需要調(diào)整鎖的粒度。例如,如果操作非常頻繁,并且可以接受一定程度的重復(fù)檢查,則可以考慮放寬鎖的粒度或使用更輕量級(jí)的同步機(jī)制。
實(shí)現(xiàn)代碼
只避免 name和age的重復(fù)
下面的實(shí)現(xiàn)的一些代碼:希望能幫到大家理解思路。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class UserService { @Autowired private RedisTemplate<String, String> redisTemplate; // 假設(shè)的鎖過期時(shí)間 private static final long LOCK_EXPIRATION_TIME = 10L; // 10秒 // 嘗試獲取鎖 private boolean tryLock(String key) { ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); // 嘗試設(shè)置鎖,如果鍵不存在則設(shè)置成功,并設(shè)置過期時(shí)間 return opsForValue.setIfAbsent(key, "locked", LOCK_EXPIRATION_TIME, TimeUnit.SECONDS); } // 釋放鎖 private void releaseLock(String key) { redisTemplate.delete(key); } // 新增User的邏輯 public void addUserIfNotExists(User user) { String nameAgeLockKey = "user:lock:name:" + user.getName() + ":age:" + user.getAge(); // 嘗試獲取鎖 ,獲取鎖成功才會(huì)繼續(xù)執(zhí)行下面業(yè)務(wù)上的校驗(yàn) if (tryLock(nameAgeLockKey)) { try { // 在這里執(zhí)行數(shù)據(jù)庫檢查(是否已存在同name和age) // 如果不存在,則執(zhí)行插入操作 // 假設(shè)檢查通過,執(zhí)行插入操作(這里省略了具體的數(shù)據(jù)庫操作) System.out.println("User added successfully"); } finally { // 釋放鎖 releaseLock(nameAgeLockKey); } } else { // 未能獲取鎖,可能是其他進(jìn)程正在處理相同的組合 System.out.println("Failed to acquire lock(s), user addition may be in progress"); } } // ... 其他代碼 ... }
避免 name和age的和age和sex重復(fù):使用雙重的分布式鎖實(shí)現(xiàn):
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class UserService { @Autowired private RedisTemplate<String, String> redisTemplate; // 假設(shè)的鎖過期時(shí)間 private static final long LOCK_EXPIRATION_TIME = 10L; // 10秒 // 嘗試獲取鎖 private boolean tryLock(String key) { ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); // 嘗試設(shè)置鎖,如果鍵不存在則設(shè)置成功,并設(shè)置過期時(shí)間 return opsForValue.setIfAbsent(key, "locked", LOCK_EXPIRATION_TIME, TimeUnit.SECONDS); } // 釋放鎖 private void releaseLock(String key) { redisTemplate.delete(key); } // 新增User的邏輯 public void addUserIfNotExists(User user) { //加兩次鎖 String nameAgeLockKey = "user:lock:name:" + user.getName() + ":age:" + user.getAge(); String ageSexLockKey = "user:lock:age:" + user.getAge() + ":sex:" + user.getSex(); // 嘗試獲取兩個(gè)鎖 if (tryLock(nameAgeLockKey) && tryLock(ageSexLockKey)) { try { // 在這里執(zhí)行數(shù)據(jù)庫檢查(是否已存在同name和age或同age和sex的User) // 如果不存在,則執(zhí)行插入操作 // 假設(shè)檢查通過,執(zhí)行插入操作(這里省略了具體的數(shù)據(jù)庫操作) System.out.println("User added successfully"); } finally { // 釋放鎖 釋放兩次 releaseLock(nameAgeLockKey); releaseLock(ageSexLockKey); } } else { // 未能獲取鎖,可能是其他進(jìn)程正在處理相同的組合 System.out.println("Failed to acquire lock(s), user addition may be in progress"); } } // ... 其他代碼 ... }
雙重加鎖的 注意點(diǎn)上面的代碼示例簡(jiǎn)化了錯(cuò)誤處理和重試邏輯。在實(shí)際應(yīng)用中,你可能需要處理各種異常情況,例如Redis服務(wù)器不可用、鎖被意外刪除或過期等。此外,如果業(yè)務(wù)邏輯復(fù)雜或執(zhí)行時(shí)間較長(zhǎng),你可能需要考慮使用更高級(jí)的鎖機(jī)制,如Redis的發(fā)布/訂閱模式、Lua腳本或Redis的RedLock算法來確保鎖的安全性和可靠性。
另外,請(qǐng)注意,tryLock 方法中的 setIfAbsent 操作是原子的,這意味著它會(huì)在單個(gè)Redis命令中完成檢查和設(shè)置操作,從而避免了競(jìng)態(tài)條件。但是,由于網(wǎng)絡(luò)延遲、Redis服務(wù)器性能等因素,多個(gè)客戶端仍可能幾乎同時(shí)嘗試獲取相同的鎖。因此,即使使用了鎖,也需要謹(jǐn)慎地設(shè)計(jì)你的業(yè)務(wù)邏輯和錯(cuò)誤處理策略。
到此這篇關(guān)于JAVA實(shí)現(xiàn)redis分布式雙重加鎖的示例代碼的文章就介紹到這了,更多相關(guān)JAVA redis分布式雙重加鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot Actuator監(jiān)控器配置及使用解析
這篇文章主要介紹了Spring Boot Actuator監(jiān)控器配置及使用解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07SpringBoot集成RocketMQ發(fā)送事務(wù)消息的原理解析
RocketMQ 的事務(wù)消息提供類似 X/Open XA 的分布事務(wù)功能,通過事務(wù)消息能達(dá)到分布式事務(wù)的最終一致,這篇文章主要介紹了SpringBoot集成RocketMQ發(fā)送事務(wù)消息,需要的朋友可以參考下2022-06-06springboot+EHcache 實(shí)現(xiàn)文章瀏覽量的緩存和超時(shí)更新
這篇文章主要介紹了springboot+EHcache 實(shí)現(xiàn)文章瀏覽量的緩存和超時(shí)更新,問題描述和解決思路給大家介紹的非常詳細(xì),需要的朋友可以參考下2017-04-04深入學(xué)習(xí)JavaWeb中監(jiān)聽器(Listener)的使用方法
這篇文章主要為大家詳細(xì)介紹了深入學(xué)習(xí)JavaWeb中監(jiān)聽器(Listener)的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09Springboot中@RequestParam和@PathVariable的用法與區(qū)別詳解
這篇文章主要介紹了Springboot中@RequestParam和@PathVariable的用法與區(qū)別詳解,RESTful API設(shè)計(jì)的最佳實(shí)踐是使用路徑參數(shù)來標(biāo)識(shí)一個(gè)或多個(gè)特定資源,而使用查詢參數(shù)來對(duì)這些資源進(jìn)行排序/過濾,需要的朋友可以參考下2024-01-01Java創(chuàng)建內(nèi)部類對(duì)象實(shí)例詳解
這篇文章主要介紹了Java創(chuàng)建內(nèi)部類對(duì)象實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05spring boot整合scurity做簡(jiǎn)單的登錄校驗(yàn)的實(shí)現(xiàn)
這篇文章主要介紹了spring boot整合scurity做簡(jiǎn)單的登錄校驗(yàn)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04