Java實(shí)現(xiàn)Redis分布式鎖的三種方案匯總
序言
setnx、Redisson、RedLock 都可以實(shí)現(xiàn)分布式鎖,從易到難得排序?yàn)椋簊etnx < Redisson < RedLock。一般情況下,直接使用 Redisson 就可以啦,有很多邏輯框架的作者都已經(jīng)考慮到了。
方案一:setnx
1.1、簡(jiǎn)單實(shí)現(xiàn)
下面的鎖實(shí)現(xiàn)可以用在測(cè)試或者簡(jiǎn)單場(chǎng)景,但是它存在以下問題,使其不適合用在正式環(huán)境。
- 鎖可能被誤刪: 在解鎖操作中,如果一個(gè)線程的鎖已經(jīng)因?yàn)槌瑫r(shí)而被自動(dòng)釋放,然后又被其他線程獲取到,這時(shí)原線程再來解鎖就會(huì)誤刪其他線程的鎖。
- 臨界區(qū)代碼不安全: 線程 A 還沒有執(zhí)行完臨界區(qū)代碼,鎖就過期釋放掉了。線程 B 此時(shí)又能獲取到鎖,進(jìn)入臨界區(qū)代碼,導(dǎo)致了臨界區(qū)代碼非串行執(zhí)行,帶來了線程不安全的問題。
public class RedisLock { ? @Autowired private StringRedisTemplate redisTemplate; ? /** * 加鎖 */ private boolean tryLock(String key) { Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } ? /** * 解鎖 */ private void unlock(String key) { redisTemplate.delete(key); } }
1.2、使用 lua 腳本加鎖、解鎖
lua 腳本是原子的,不管寫多少 lua 腳本代碼,redis 都是通過一條命令去執(zhí)行的。
下述代碼使用了 lua 腳本進(jìn)行加鎖/解鎖,保證了加鎖和解鎖的時(shí)候都是原子性的,是一種相對(duì)較好的 Redis 分布式鎖的實(shí)現(xiàn)方式。
它支持獲得鎖的線程才能釋放鎖,如果線程 1 因?yàn)殒i過期而丟掉了鎖,然后線程 2 拿到了鎖。此時(shí)線程 1 的業(yè)務(wù)代碼執(zhí)行完以后,也無法釋放掉線程 2 的鎖,解決了誤刪除的問題。
public class RedisLock { ? private final StringRedisTemplate redisTemplate; ? public RedisDistributedLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } ? public boolean tryLock(String lockKey, String lockValue, long expireTimeInSeconds) { try { //加鎖成功返回 true,加鎖失敗返回 fasle。效果等同于 redisTemplate.opsForValue().setIfAbsent String luaScript = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 else return 0 end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue, String.valueOf(expireTimeInSeconds)); ? return result != null && result == 1; } catch (Exception e) { // Handle exceptions return false; } } ? public void unlock(String lockKey, String lockValue) { try { //拿到鎖的線程才可以釋放鎖,lockValue 可以設(shè)置為 uuid。 String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue); } catch (Exception e) { // Handle exceptions } } }
方案二:Redisson
Redisson 是一個(gè)基于 Java 的客服端,通過 Redisson 我們可以快速安全的實(shí)現(xiàn)分布式鎖。Redisson 框架具有可重入鎖的支持、分布式鎖的實(shí)現(xiàn)、鎖的自動(dòng)續(xù)期、紅鎖支持等多種特點(diǎn),給我們開發(fā)過程中帶來了極大的便利。
@Component public class RedisLock { ? @Resource private RedissonClient redissonClient; ? /** * lock(), 拿不到lock就不罷休,不然線程就一直block */ public RLock lock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock.lock(); return lock; } ? /** * leaseTime為加鎖時(shí)間,單位為秒 */ public RLock lock(String lockKey, long leaseTime) { RLock lock = redissonClient.getLock(lockKey); lock.lock(leaseTime, TimeUnit.SECONDS); return null; } ? /** * timeout為加鎖時(shí)間,時(shí)間單位由unit確定 */ public RLock lock(String lockKey, TimeUnit unit, long timeout) { RLock lock = redissonClient.getLock(lockKey); lock.lock(timeout, unit); return lock; } ? /** * @param lockKey 鎖 key * @param unit 單位 * @param waitTime 等待時(shí)間 * @param leaseTime 鎖有效時(shí)間 * @return 加鎖成功? true:成功 false: 失敗 */ public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) { ? RLock lock = redissonClient.getLock(lockKey); try { return lock.tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { return false; } } ? /** * unlock */ public void unlock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } } ? /** * unlock * @param lock 鎖 */ public void unlock(RLock lock) { lock.unlock(); } }
方案三:RedLock
RedLock 又叫做紅鎖,是 Redis 官方提出的一種分布式鎖的算法,紅鎖的提出是為了解決集群部署中 Redis 鎖相關(guān)的問題。
比如當(dāng)線程 A 請(qǐng)求鎖成功了,這時(shí)候從節(jié)點(diǎn)還沒有復(fù)制鎖。此時(shí)主節(jié)點(diǎn)掛掉了,從節(jié)點(diǎn)成為了主節(jié)點(diǎn)。線程 B 請(qǐng)求加鎖,在原來的從節(jié)點(diǎn)(現(xiàn)在是主節(jié)點(diǎn))上加鎖成功。這時(shí)候就會(huì)出現(xiàn)線程安全問題。
下圖是紅鎖的簡(jiǎn)易思路。紅鎖認(rèn)為 (N / 2) + 1 個(gè)節(jié)點(diǎn)加鎖成功后,那么就認(rèn)為獲取到了鎖,通過這種算法減少線程安全問題。簡(jiǎn)單流程為:
- 順序向五個(gè)節(jié)點(diǎn)請(qǐng)求加鎖
- 根據(jù)一定的超時(shí)時(shí)間判斷是否跳過該節(jié)點(diǎn)
- (N / 2) + 1 個(gè)節(jié)點(diǎn)加鎖成功并且小于鎖的有效期
- 認(rèn)定加鎖成功
@Service public class MyService { ? private final RedissonClient redissonClient; ? @Autowired public MyService(RedissonClient redissonClient) { this.redissonClient = redissonClient; } ? public void doSomething() { RLock lock1 = redissonClient.getLock("lock1"); RLock lock2 = redissonClient.getLock("lock2"); RLock lock3 = redissonClient.getLock("lock3"); ? RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); redLock.lock(); try { // 業(yè)務(wù)邏輯 } finally { redLock.unlock(); } } }
總結(jié)
自己玩或者測(cè)試的時(shí)候使用方案一的簡(jiǎn)單實(shí)現(xiàn)。
單機(jī)版 Redis 使用方案二。
Redis 集群使用方案三。
以上就是Java實(shí)現(xiàn)Redis分布式鎖的三種方案匯總的詳細(xì)內(nèi)容,更多關(guān)于Redis分布式鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mybatis中${param}與#{param}的區(qū)別說明
這篇文章主要介紹了Mybatis中${param}與#{param}的區(qū)別說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06Spring大白話之三級(jí)緩存如何解決循環(huán)依賴問題
Spring通過三級(jí)緩存(singletonObjects、earlySingletonObjects、singletonFactories)解決單例循環(huán)依賴,三級(jí)緩存使用Lambda表達(dá)式提前暴露bean的早期引用,確保在遞歸調(diào)用時(shí)能夠正確獲取對(duì)象實(shí)例,避免死循環(huán)2025-02-02SpringBoot讀寫xml上傳到AWS存儲(chǔ)服務(wù)S3的示例
這篇文章主要介紹了SpringBoot讀寫xml上傳到S3的示例,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2020-10-10基于springboot 長(zhǎng)輪詢的實(shí)現(xiàn)操作
這篇文章主要介紹了基于springboot 長(zhǎng)輪詢的實(shí)現(xiàn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-01-01Java實(shí)戰(zhàn)之實(shí)現(xiàn)一個(gè)好用的MybatisPlus代碼生成器
這篇文章主要介紹了Java實(shí)戰(zhàn)之實(shí)現(xiàn)一個(gè)好用的MybatisPlus代碼生成器,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04Java連接MySQL數(shù)據(jù)庫(kù)增刪改查的通用方法(推薦)
下面小編就為大家?guī)硪黄狫ava連接MySQL數(shù)據(jù)庫(kù)增刪改查的通用方法(推薦)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08