詳解RedisTemplate下Redis分佈式鎖引發的系列問題
自己的項目因為會一直抓取某些信息,但是本地會和線上經常一起跑,造成沖突。這其實就是我們常說的分佈式集群的問題瞭,本地和線上的服務器構成瞭集群以及QPS為2的小並發(其實也不叫並發,不知道拿什麼詞形容?)。
首先,分佈式集群的問題大傢都知道,會造成數據庫的插入重復問題,會造成一系列的並發性問題。
解決的方式呢也大概如下幾點,百度以及谷歌上都能搜到的解決方式:
1:數據庫添加唯一索引
2:設計接口冪等性
3:依靠中間件使用分佈式鎖,而分佈式鎖又分為Redis和Zookeeper
由於Zookeeper我沒怎麼接觸過,並且我項目中本來就引用瞭Redis,所以就想著用Redis來做分佈式鎖,也高端洋氣上檔次點。
首先基於Redis的操作,我們必須要保證其原子性,也就是要麼全部成功,要麼全部失敗,先從Redis的客戶端入手。
就Redis客戶端而言,我們通過的操作是先使用setnx指令,如果成功則返回1,失敗則返回0
可是就分佈鎖鎖而言,一個常用的問題就是如果一個服務setnx成功瞭,但是在解鎖的時候如果發生瞭宕機或者一些特殊因素,導致無法解鎖,那麼其他服務將陷入死鎖的狀態。所以,我們在用 setnx 的同時想著去用 expire 指令對鎖進行一個過期操作
從指令可以看出 setnx 和 expire 指令是分開的,如果在這中間的空隙過程中如果有特殊因素導致指令無法繼續,也會導致死鎖的產生。
以下參考自老錢的 Redis 深度歷險:核心原理與應用實踐
為瞭解決這個疑難,Redis 開源社區湧現瞭一堆分佈式鎖的 library,專門用來解決這個問題。實現方法極為復雜,小白用戶一般要費很大的精力才可以搞懂。如果你需要使用分佈式鎖,意味著你不能僅僅使用 Jedis 或者 redis-py 就行瞭,還得引入分佈式鎖的 library。
為瞭治理這個亂象,Redis 2.8 版本中作者加入瞭 set 指令的擴展參數,使得 setnx 和 expire 指令可以一起執行,徹底解決瞭分佈式鎖的亂象。從此以後所有的第三方分佈式鎖 library 可以休息瞭。
以上都是基於Redis的操作,但是我們在JAVA中如何去運用分佈式鎖呢。
首先在Redis方面我用的是RedisTemplate對Redis進行操作的 ,而RedisTemplate在目前情況下如果不借助於是無法保證其原子性的,所以我們需要借助於Redis的Lua腳本。
先上Lua腳本的代碼
// 加鎖 if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end // 解鎖 redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0
Java調用腳本有兩種方式
1。新建一個腳本文件,在代碼中調用其絕對路徑地址
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(地址)));
2。在Java代碼中以字符串的方式傳入
redisScript.setScriptText(腳本);
我是用的第二種方式實現的,下面是JAVA代碼
/** * 獲取鎖 * @param lockKey * @param value * @param expireTime:單位-秒 * @return */ public boolean getLock(String lockKey, String value, int expireTime){ boolean ret = false; try{ String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Object result = redisTemplate.execute(redisScript,new StringRedisSerializer(),new StringRedisSerializer(), Collections.singletonList(lockKey),value,expireTime + ""); System.out.println(result + "-----------"); //Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),value,expireTime + ""); if(SUCCESS.equals(result)){ return true; } }catch(Exception e){ e.printStackTrace(); } return ret; } /** * 釋放鎖 * @param lockKey * @param value * @return */ public boolean releaseLock(String lockKey, String value){ String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Object result = redisTemplate.execute(redisScript,new StringRedisSerializer(),new StringRedisSerializer(), Collections.singletonList(lockKey),value); if(SUCCESS.equals(result)) { return true; } return false; }
以上代碼已經在我的項目中確切可以使用瞭。但是在使用的過程中遇到瞭許多問題。
1:java.lang.IllegalStateException
在返回值方面,會經常報IllegalStateException。
RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
用String類型時候,經常會報類型轉換異常。我在代碼中使用的Long類型接收該類型,在命令行中我們也看到命令行結果返回的是數字0或者1,保險起見我們也可以用Object對象來接收結果集。
2:ERR value is not an integer or out of range
這個問題糾結瞭我一個下午至少,Redis報的異常都是很深的,從跟蹤源碼的時候看到,我們在調用redisTemplate.execute的方法時候,如果不傳序列化的參數的時候,代碼默認調用的是 Jdkserializationredisserializer 來進行序列化和反序列化操作,這是jdk自帶的序列化操作,使用該序列化的對象必須要實現Serializable接口。所以該序列化接口是用於對實體類的序列化。
所以在進行 execute 操作的時候,我們傳入 Stringredisserializer,該序列化接口是專用於對字符串類型的序列化操作。具體的區別可以去這兩個類的源碼中看下他們的加密方式。
因為時間以及個人能力的問題,對部分源碼有點未理解,所以沒有做到全方位的解讀這些異常的原因,以後有機會會將源碼細讀並分析其異常原因。
到此這篇關於詳解RedisTemplate下Redis分佈式鎖引發的系列問題的文章就介紹到這瞭,更多相關RedisTemplate 分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 關於SpringBoot 使用 Redis 分佈式鎖解決並發問題
- Redis分佈式鎖如何實現續期
- C#實現Redis的分佈式鎖
- SpringBoot RedisTemplate分佈式鎖的項目實戰
- 分佈式面試分佈式鎖實現及應用場景