Java實(shí)現(xiàn)redis分布式鎖的三種方式
一、引入原因
在分布式服務(wù)中,常常有如定時(shí)任務(wù)、庫(kù)存更新這樣的場(chǎng)景。
在定時(shí)任務(wù)中,如果不使用quartz這樣的分布式定時(shí)工具,只是簡(jiǎn)單的使用定時(shí)器來(lái)進(jìn)行定時(shí)任務(wù),在服務(wù)分布式部署中,就有可能存在定時(shí)任務(wù)并發(fā)執(zhí)行,造成一些問(wèn)題。
在庫(kù)存更新這樣的場(chǎng)景中,我們服務(wù)對(duì)數(shù)據(jù)庫(kù)同一條記錄進(jìn)行更新,并記錄。對(duì)記錄更新可以使用分布式鎖,但對(duì)操作進(jìn)行記錄時(shí),可能造成讀未提交,造成記錄錯(cuò)亂的情況。
在以上的場(chǎng)景中,我們引入了分布式事務(wù)鎖。
二、分布式鎖實(shí)現(xiàn)過(guò)程中的問(wèn)題
問(wèn)題一:異常導(dǎo)致鎖沒(méi)有釋放
這個(gè)問(wèn)題形成的原因就是程序在獲取到鎖之后,執(zhí)行業(yè)務(wù)的過(guò)程中出現(xiàn)了異常,導(dǎo)致鎖沒(méi)有被釋放。通俗的話(huà)說(shuō):上廁所的人死在了廁所里面,導(dǎo)致“坑位”資源死鎖無(wú)法被釋放。(當(dāng)然這種情況出現(xiàn)的概率很小,但概率小不等于不存在。)
解決方案: 為redis的key設(shè)置過(guò)期時(shí)間,程序異常導(dǎo)致的死鎖,在到達(dá)過(guò)期時(shí)間之后鎖自動(dòng)釋放。也就說(shuō)廁所門(mén)是電子鎖,鎖定的最長(zhǎng)時(shí)間是有限制的,超過(guò)時(shí)長(zhǎng)鎖就會(huì)自動(dòng)打開(kāi)釋放"坑位"資源。
問(wèn)題二:獲取鎖與設(shè)置過(guò)期時(shí)間操作不是原子性的
上文中我們雖然獲取到鎖,也設(shè)置了過(guò)期時(shí)間,看似完美。但是在高并發(fā)的場(chǎng)景下仍然會(huì)出問(wèn)題,因?yàn)?ldquo;獲取鎖”與“設(shè)置過(guò)期時(shí)間”是兩個(gè)redis操作,兩個(gè)redis操作不是原子性的。
可能出現(xiàn)這種情況:就在獲取鎖之后,設(shè)置過(guò)期時(shí)間之前程序宕機(jī)了。鎖被獲取到了但沒(méi)有設(shè)置過(guò)期時(shí)間,最后又成為死鎖。
解決方案: 獲取鎖的同時(shí)設(shè)置過(guò)期時(shí)間
問(wèn)題三:鎖過(guò)期之后被別的線(xiàn)程重新獲取與釋放
這個(gè)問(wèn)題出現(xiàn)的場(chǎng)景是:假如某個(gè)應(yīng)用集群化部署存在多個(gè)進(jìn)程實(shí)例,實(shí)例A、實(shí)例B。實(shí)例A獲取到鎖,但是執(zhí)行過(guò)程超時(shí)了(數(shù)據(jù)庫(kù)層面或其他層面導(dǎo)致操作執(zhí)行超時(shí))。超時(shí)之后鎖被自動(dòng)釋放了,實(shí)例B獲取到鎖,并執(zhí)行業(yè)務(wù)程序,執(zhí)行完成之后把鎖刪除了。
解決方案: 在釋放鎖之前判斷一下,這把鎖是不是自己的那一把,如果是別人的鎖你就不要?jiǎng)印T趺磁袛噙@把鎖是不是自己的?加鎖時(shí)為value賦隨機(jī)值,加鎖的隨機(jī)值等于解鎖時(shí)的獲取到的值,才能證明這把鎖是你的。
問(wèn)題四:鎖的釋放不是原子性的
大家仔細(xì)看代碼,鎖的釋放時(shí)三個(gè)操作,這三個(gè)操作不是原子性的。也就是說(shuō)在高并發(fā)的場(chǎng)景下,你剛get到的redis key有可能也被別的線(xiàn)程get了,你剛要?jiǎng)h除別的線(xiàn)程可能已經(jīng)把這個(gè)key刪除了。
解決方案: 我們可以使用redis lua腳本(lua腳本是在一個(gè)事務(wù)里面執(zhí)行的,可以保證原子性)。在Java代碼中可以以字符串的形式存在。如下:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
問(wèn)題五:其他的問(wèn)題?
上面我們分析了很多使用redis實(shí)現(xiàn)分布式鎖可能出現(xiàn)的問(wèn)題及解決方案,其實(shí)在實(shí)際的開(kāi)發(fā)應(yīng)用中還會(huì)有更多的問(wèn)題。比如:
- 目前我們的程序獲取不到鎖,就無(wú)限的重試,是不是應(yīng)該在重試一定的次數(shù)之后就拋出異常?在有限的時(shí)間內(nèi)通過(guò)異常給用戶(hù)一個(gè)友好的響應(yīng)。比如:程序太忙,請(qǐng)您稍后再試!
- 程序A沒(méi)有執(zhí)行完成,鎖定的key就過(guò)期了。雖然過(guò)期之后會(huì)自動(dòng)釋放鎖,但是我的程序A的確沒(méi)有執(zhí)行完成啊,也沒(méi)有異常拋出,就是執(zhí)行的時(shí)間比較長(zhǎng),這個(gè)時(shí)候是不是應(yīng)該對(duì)鎖定的key進(jìn)行續(xù)期?
這些問(wèn)題在高并發(fā)場(chǎng)景下會(huì)出現(xiàn),實(shí)際上分布式鎖的細(xì)節(jié)實(shí)踐有很多的現(xiàn)成的解決方案,不用我們?nèi)プ约簩?shí)現(xiàn)。比較完整優(yōu)秀的分布式鎖實(shí)現(xiàn)包括:
RedisLockRegistry是spring-integration-redis中提供redis分布式鎖實(shí)現(xiàn)類(lèi)
基于Redisson實(shí)現(xiàn)分布式鎖原理(Redission是一個(gè)獨(dú)立的redis客戶(hù)端,是與Jedis、Lettuce同級(jí)別的存在)
三、具體實(shí)現(xiàn)
1. RedisTemplate
RedisTemplate<String, String> redisTemplate; public void updateUserWithRedisLock(SysUser sysUser) throws InterruptedException { // 占分布式鎖,去redis占坑 // 1. 分布式鎖占坑 Boolean lock = redisTemplate.opsForValue().setIfAbsent("SysUserLock" + sysUser.getId(), "value", 30, TimeUnit.SECONDS); if(lock) { //加鎖成功... // todo business redisTemplate.delete("SysUserLock" + sysUser.getId()); //刪除key,釋放鎖 } else { Thread.sleep(100); // 加鎖失敗,重試 updateUserWithRedisLock(sysUser); } }
setIfAbsent方法的作用是在某一個(gè)lock key不存在的時(shí)候,才能返回true;如果這個(gè)key已經(jīng)存在了就返回false,返回false就是獲取鎖失敗。setIfAbsent函數(shù)功能類(lèi)似于redis命令行setnx。
2. RedisLockRegistry
集成spring-integration-redis
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-redis</artifactId> </dependency>
注冊(cè)RedisLockRegistry
@Configuration public class RedisLockConfig { @Bean public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) { //第一個(gè)參數(shù)redisConnectionFactory //第二個(gè)參數(shù)registryKey,分布式鎖前綴,設(shè)置為項(xiàng)目名稱(chēng)會(huì)好些 //該構(gòu)造方法對(duì)應(yīng)的分布式鎖,默認(rèn)有效期是60秒.可以自定義 return new RedisLockRegistry(redisConnectionFactory, "boot-launch"); //return new RedisLockRegistry(redisConnectionFactory, "boot-launch",60); } }
使用RedisLockRegistry
代碼中實(shí)現(xiàn)
@Resource private RedisLockRegistry redisLockRegistry; public void updateUser(String userId) { String lockKey = “config” + userId; Lock lock = redisLockRegistry.obtain(lockKey); //獲取鎖資源 try { lock.lock(); //加鎖 //這里寫(xiě)需要處理業(yè)務(wù)的業(yè)務(wù)代碼 } finally { lock.unlock(); //釋放鎖 } }
注解實(shí)現(xiàn)
@RedisLock("lock-key") public void save(){ }
3. 使用redisson實(shí)現(xiàn)分布式鎖
集成redisson
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.15.0</version> <exclusions> <exclusion> <groupId>org.redisson</groupId> <!-- 默認(rèn)是 Spring Data Redis v.2.3.x ,所以排除掉--> <artifactId>redisson-spring-data-23</artifactId> </exclusion> </exclusions> </dependency>
配置
在配置文件中加
spring: redis: redisson: file: classpath:redisson.yaml
然后新建一個(gè)redisson.yaml文件,也放在resouce目錄下
singleServerConfig: idleConnectionTimeout: 10000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 password: 123456 subscriptionsPerConnection: 5 clientName: null address: "redis://192.168.161.3:6379" subscriptionConnectionMinimumIdleSize: 1 subscriptionConnectionPoolSize: 50 connectionMinimumIdleSize: 32 connectionPoolSize: 64 database: 0 dnsMonitoringInterval: 5000 threads: 0 nettyThreads: 0 codec: !<org.redisson.codec.JsonJacksonCodec> {} transportMode: "NIO"
實(shí)現(xiàn)
@Resource private RedissonClient redissonClient; public void updateUser(String userId) { String lockKey = "config" + userId; RLock lock = redissonClient.getLock(lockKey); //獲取鎖資源 try { lock.lock(10, TimeUnit.SECONDS); //加鎖,可以指定鎖定時(shí)間 //這里寫(xiě)需要處理業(yè)務(wù)的業(yè)務(wù)代碼 } finally { lock.unlock(); //釋放鎖 } }
- 相對(duì)于RedisLockRegistry另一個(gè)小優(yōu)點(diǎn)是:我們可以為每一個(gè)鎖指定鎖定的超時(shí)時(shí)間。RedisLockRegistry目前只能針對(duì)所有的鎖設(shè)定統(tǒng)一的超時(shí)時(shí)間
- 如果業(yè)務(wù)執(zhí)行超時(shí)之后,再去unlock會(huì)拋出java.lang.IllegalMonitorStateException
到此這篇關(guān)于Java實(shí)現(xiàn)redis分布式鎖的三種方式的文章就介紹到這了,更多相關(guān)Java redis分布式鎖 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于ElasticSearch的常用增刪改查DSL和代碼
這篇文章主要介紹了關(guān)于ElasticSearch的常用增刪改查DSL和代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之二手書(shū)商城系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開(kāi)發(fā)的二手書(shū)商城系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有在線(xiàn)書(shū)城該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01SpringBoot整合EasyExcel的完整過(guò)程記錄
easyexcel是阿里巴巴旗下開(kāi)源項(xiàng)目,主要用于Excel文件的導(dǎo)入和導(dǎo)出處理,下面這篇文章主要給大家介紹了關(guān)于SpringBoot整合EasyExcel的完整過(guò)程,需要的朋友可以參考下2021-12-12超細(xì)致講解Spring框架 JdbcTemplate的使用
在之前的Javaweb學(xué)習(xí)中,學(xué)習(xí)了手動(dòng)封裝JdbcTemplate,其好處是通過(guò)(sql語(yǔ)句+參數(shù))模板化了編程。而真正的JdbcTemplate類(lèi),是Spring框架為我們寫(xiě)好的。它是 Spring 框架中提供的一個(gè)對(duì)象,是對(duì)原始 Jdbc API 對(duì)象的簡(jiǎn)單封裝。2021-09-09Mybatis-Plus實(shí)現(xiàn)公共字段自動(dòng)填充的項(xiàng)目實(shí)踐
本文主要介紹了Mybatis-Plus實(shí)現(xiàn)公共字段自動(dòng)填充的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07SpringBoot默認(rèn)使用HikariDataSource數(shù)據(jù)源方式
這篇文章主要介紹了SpringBoot默認(rèn)使用HikariDataSource數(shù)據(jù)源方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10