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