做者:菜蚜node
my.oschina.net/wnjustdoit/blog/1606215git
注意: 因爲SET命令加上選項已經能夠徹底取代SETNX, SETEX, PSETEX的功能,因此在未來的版本中,redis可能會不推薦使用而且最終拋棄這幾個命令。
關於Martin Kleppmann的Redlock的分析
SET resource_name my_random_value NX PX 30000 手動刪除鎖(Lua腳本): if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
爲了保證在儘量短的時間內獲取到(N/2)+1個節點的鎖,能夠並行去獲取各個節點的鎖(固然,並行可能須要消耗更多的資源,由於串行只須要count到足夠數量的鎖就能夠中止獲取了);github
另外,怎麼動態實時統一獲取redis master nodes須要更進一步去思考了。web
QA,補充一下說明(如下爲我與朋友溝通的狀況,以說明文中你們可能不夠明白的地方):redis
一、在關鍵問題2.1中,刪除就刪除了,會形成什麼問題?算法
線程A超時,準備刪除鎖;但此時的鎖屬於線程B;線程B還沒執行完,線程A把鎖刪除了,這時線程C獲取到鎖,同時執行程序;因此不能亂刪。數據庫
二、在關鍵問題2.2中,只要在key生成時,跟線程相關就不用考慮這個問題了嗎?
不一樣的線程執行程序,線程之間肯雖然有差別呀,而後在redis鎖的value設置有線程信息,好比線程id或線程名稱,是分佈式環境的話加個機器id前綴咯(相似於twitter的snowflake算法!),可是在del命令只會涉及到key,不會再次檢查value,因此仍是須要lua腳本控制if(condition){xxx}的原子性。
三、那要不要考慮鎖的重入性?
不須要重入;try…finally 沒得重入的場景;對於單個線程來講,執行是串行的,獲取鎖以後一定會釋放,由於finally的代碼一定會執行啊(只要進入了try塊,finally一定會執行)。
四、爲何兩個線程都會去刪除鎖?(貌似重複的問題。無論怎樣,仍是耐心解答吧)
每一個線程只能管理本身的鎖,不能管理別人線程的鎖啊。這裏能夠聯想一下ThreadLocal。
五、若是加鎖的線程掛了怎麼辦?只能等待自動超時?
看你怎麼寫程序的了,一種是問題3的回答;另外,那就自動超時咯。這種狀況也適用於網絡over了。
六、時間太長,程序異常就會蛋疼,時間過短,就會出現程序尚未處理完就超時了,這豈不是很尷尬?
是呀,因此須要更好的衡量這個超時時間的設置。
實踐部分主要代碼:
package com.caiya.cms.web.component; import com.caiya.cache.CacheException; import com.caiya.cache.redis.JedisCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * redis實現分佈式鎖 * 可實現特性: * 一、使多線程無序排隊獲取和釋放鎖; * 二、丟棄未成功得到鎖的線程處理; * 三、只釋放線程自己加持的鎖; * 四、避免死鎖 * * @author wangnan * @since 1.0 */ public final class RedisLock { private static final Logger logger = LoggerFactory.getLogger(RedisLock.class); /** * 嘗試加鎖(僅一次) * * @param lockKey 鎖key * @param lockValue 鎖value * @param expireSeconds 鎖超時時間(秒) * @return 是否加鎖成功 * @throws CacheException */ public static boolean tryLock(String lockKey, String lockValue, long expireSeconds) throws CacheException { JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache(); try { String response = jedisCache.set(lockKey, lockValue, "nx", "ex", expireSeconds); return Objects.equals(response, "OK"); } finally { jedisCache.close(); } } /** * 加鎖(指定最大嘗試次數範圍內) * * @param lockKey 鎖key * @param lockValue 鎖value * @param expireSeconds 鎖超時時間(秒) * @param tryTimes 最大嘗試次數 * @param sleepMillis 每兩次嘗試之間休眠時間(毫秒) * @return 是否加鎖成功 * @throws CacheException */ public static boolean lock(String lockKey, String lockValue, long expireSeconds, int tryTimes, long sleepMillis) throws CacheException { boolean result; int count = 0; do { count++; result = tryLock(lockKey, lockValue, expireSeconds); try { TimeUnit.MILLISECONDS.sleep(sleepMillis); } catch (InterruptedException e) { logger.error(e.getMessage(), e); } } while (!result && count <= tryTimes); return result; } /** * 釋放鎖 * * @param lockKey 鎖key * @param lockValue 鎖value */ public static void unlock(String lockKey, String lockValue) { JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache(); try { String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; Object result = jedisCache.eval(luaScript, 1, lockKey, lockValue); // Objects.equals(result, 1L); } catch (Exception e) { logger.error(e.getMessage(), e); } finally { jedisCache.close(); } // return false; } private RedisLock() { } }
... String lockKey = Constant.DEFAULT_CACHE_NAME + ":addItemApply:" + applyPriceDTO.getItemId() + "_" + applyPriceDTO.getSupplierId();// 跟業務相關的惟一拼接鍵 String lockValue = Constant.DEFAULT_CACHE_NAME + ":" + System.getProperty("JvmId") + ":" + Thread.currentThread().getName() + ":" + System.currentTimeMillis();// 生成集羣環境中的惟一值 boolean locked = RedisLock.tryLock(lockKey, lockValue, 100);// 只嘗試一次,在本次處理過程當中直接拒絕其餘線程的請求 if (!locked) { throw new IllegalAccessException("您的操做太頻繁了,休息一下再來吧~"); } try { // 開始處理核心業務邏輯 Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId()); ... ... } finally { RedisLock.unlock(lockKey, lockValue);// 在finally塊中釋放鎖 }
... String lockKey = Constant.DEFAULT_CACHE_NAME + ":addItemApply:" + applyPriceDTO.getItemId() + "_" + applyPriceDTO.getSupplierId(); String lockValue = Constant.DEFAULT_CACHE_NAME + ":機器編號:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis(); boolean locked = RedisLock.lock(lockKey, lockValue, 100, 20, 100);// 非公平鎖,無序競爭(這裏須要合理根據業務處理狀況設置最大嘗試次數和每次休眠時間) if (!locked) { throw new IllegalAccessException("系統太忙,本次操做失敗");// 通常來講,不會走到這一步;若是真的有這種狀況,而且在合理設置鎖嘗試次數和等待響應時間以後仍然處理不過來,可能須要考慮優化程序響應時間或者用消息隊列排隊執行了 } try { // 開始處理核心業務邏輯 Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId()); ... ... } finally { RedisLock.unlock(lockKey, lockValue); } ...
基於redis的分佈式鎖實現客戶端Redisson:
https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
基於zookeeper的分佈式鎖實現:
http://curator.apache.org/curator-recipes/shared-reentrant-lock.html
- END -
關注Java技術棧微信公衆號,在後臺回覆關鍵字:Java,能夠獲取一份棧長整理的 Java 最新技術乾貨。
最近乾貨分享
點擊「閱讀原文」加入棧長的戰隊~