查看了很多關於redis實現分佈式鎖的文章,無疑要設計一個靠譜的分佈式並不太容易,總會出現各類鬼畜的問題;如今就來小述一下,在設計一個分佈式鎖的過程當中,會遇到一些什麼問題java
藉助redis來實現分佈式鎖(咱們先考慮單機redis的模式),首先有必要了解下如下幾點:redis
獲取鎖:安全
釋放鎖:服務器
網上一種常見的case,主要思路以下網絡
實現代碼以下併發
public class DistributeLock { private static final Long OUT_TIME = 30L; public String tryLock(Jedis jedis, String key) { while (true) { String value = UUID.randomUUID().toString() + "_" + System.currentTimeMillis(); Long ans = jedis.setnx(key, value); if (ans != null && ans == 1) { // 獲取鎖成功 return value; } // 鎖獲取失敗, 判斷是否超時 String oldLock = jedis.get(key); if (oldLock == null) { continue; } long oldTime = Long.parseLong(oldLock.substring(oldLock.lastIndexOf("_") + 1)); long now = System.currentTimeMillis(); if (now - oldTime < OUT_TIME) { // 沒有超時 continue; } String getsetOldVal = jedis.getSet(key, value); if (Objects.equals(oldLock, getsetOldVal)) { // 返回的正好是上次的值,表示鎖獲取成功 return value; } else { // 表示返回的是其餘業務設置的鎖,趕忙的設置回去 jedis.set(key, getsetOldVal); } } } public void tryUnLock(Jedis jedis, String key, String uuid) { String ov = jedis.get(key); if (uuid.equals(ov)) { // 只釋放本身的鎖 jedis.del(key); } } }
觀察獲取鎖的邏輯,特別是獲取超時鎖的邏輯,很容易想到有一個問題 getSet
方法會不會致使寫數據混亂的問題,簡單來講就是多個線程同時判斷鎖超時時,執行 getSet
設置鎖時,最終獲取鎖的線程,可否保證和redis中的鎖的value相同app
上面的實現方式,一個混亂的case以下:dom
實際驗證分佈式
在上面的代碼中,配合測試case,加上一些日誌輸出測試
public static String tryLock(Jedis jedis, String key) throws InterruptedException { String threadName = Thread.currentThread().getName(); while (true) { String value = threadName + "_" + UUID.randomUUID().toString() + "_" + System.currentTimeMillis(); Long ans = jedis.setnx(key, value); if (ans != null && ans == 1) { // 獲取鎖成功 return value; } // 鎖獲取失敗, 判斷是否超時 String oldLock = jedis.get(key); if (oldLock == null) { continue; } long oldTime = Long.parseLong(oldLock.substring(oldLock.lastIndexOf("_") + 1)); long now = System.currentTimeMillis(); if (now - oldTime < OUT_TIME) { // 沒有超時 continue; } // 強制使全部的線程均可以到這一步 Thread.sleep(50); System.out.println(threadName + " in getSet!"); // 人工接入,確保t1 獲取到鎖, t2 獲取的是t1設置的內容, t3獲取的是t2設置的內容 if ("t2".equalsIgnoreCase(threadName)) { Thread.sleep(20); } else if ("t3".equalsIgnoreCase(threadName)) { Thread.sleep(40); } String getsetOldVal = jedis.getSet(key, value); System.out.println(threadName + " set redis value: " + value); if (Objects.equals(oldLock, getsetOldVal)) { // 返回的正好是上次的值,表示鎖獲取成功 System.out.println(threadName + " get lock!"); if ("t1".equalsIgnoreCase(threadName)) { // t1獲取到鎖,強制sleep40ms, 確保線t2,t3也進入了 getSet邏輯 Thread.sleep(40); } return value; } else { // 表示返回的是其餘業務設置的鎖,趕忙的設置回去 // 人肉介入,確保t2優先執行,並設置回t1設置的值, t3後執行設置的是t2設置的值 if ("t3".equalsIgnoreCase(threadName)) { Thread.sleep(40); } else if ("t2".equalsIgnoreCase(threadName)){ Thread.sleep(20); } jedis.set(key, getsetOldVal); System.out.println(threadName + " recover redis value: " + getsetOldVal); } } }
測試case
@Test public void testLock() throws InterruptedException { // 先無視獲取jedis的方式 JedisPool jedisPool = cacheWrapper.getJedisPool(0); Jedis jedis = jedisPool.getResource(); String lockKey = "lock_test"; String old = DistributeLock.tryLock(jedis, lockKey); System.out.println("old lock: " + old); // 確保鎖超時 Thread.sleep(40); // 建立三個線程 Thread t1 = new Thread(() -> { try { Jedis j =jedisPool.getResource(); DistributeLock.tryLock(j, lockKey); System.out.println("t1 >>>> " + j.get(lockKey)); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1"); Thread t2 = new Thread(() -> { try { Jedis j =jedisPool.getResource(); DistributeLock.tryLock(j, lockKey); System.out.println("t2 >>>>> " + j.get(lockKey)); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2"); Thread t3 = new Thread(() -> { try { Jedis j =jedisPool.getResource(); DistributeLock.tryLock(j, lockKey); System.out.println("t3 >>>>> " + j.get(lockKey)); } catch (InterruptedException e) { e.printStackTrace(); } }, "t3"); t1.start(); t2.start(); t3.start(); Thread.sleep(10000); };
部分輸出結果:
main in getSet! main set redis value: main_d4cc5d69-5027-4550-abe1-10126f057779_1515643763130 main get lock! old lock: main_d4cc5d69-5027-4550-abe1-10126f057779_1515643763130 t1 in getSet! t2 in getSet! t1 set redis value: t1_105974db-7d89-48bf-9669-6f122a3f9fb6_1515643763341 t1 get lock! t3 in getSet! t2 set redis value: t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341 t1 >>>> t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341 t3 set redis value: t3_9aa5d755-43b2-43bd-9a0b-2bad13fa31f6_1515643763345 t2 recover redis value: t1_105974db-7d89-48bf-9669-6f122a3f9fb6_1515643763341 t3 recover redis value: t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341
重點關注 t1 >>>> t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341
,表示t1線程過去了鎖,可是鎖的內容不是其value,即使t2去恢復,也會被t3給覆蓋
如何解決上面這個問題呢?
上面是典型的併發致使的問題,固然能夠考慮從解決併發問題的角度出發來考慮,一個常見的方式就是加鎖了,思路以下:(不詳細展開了)
這種實現方式,會有如下的問題:
相比於前面一種直接將value設置爲時間戳,而後來比對的方法,這裏則直接藉助redis自己的expire方式來實現超時設置,主要實現邏輯相差無幾
public class DistributeExpireLock { private static final Integer OUT_TIME = 3; public static String tryLock(Jedis jedis, String key) { String value = UUID.randomUUID().toString(); while(true) { Long ans = jedis.setnx(key, value); if (ans != null && ans == 1) { // 獲取鎖成功 jedis.expire(key, OUT_TIME); // 主動設置超時時間爲3s return value; } // 獲取失敗,先確認下是否有設置國超是時間 // 防止鎖的超時時間設置失效,致使一直競爭不到 if(jedis.ttl(key) < 0) { jedis.expire(key, OUT_TIME); } } } public static void tryUnLock(Jedis jedis, String key, String uuid) { String ov = jedis.get(key); if (uuid.equals(ov)) { // 只釋放本身的鎖 jedis.del(key); System.out.println(Thread.currentThread() +" del lock success!"); } else { System.out.println(Thread.currentThread() +" del lock fail!"); } } }
獲取鎖的邏輯相比以前的,就簡單不少了,接下來則須要簡單的分析下,上面這種實現方式,會不會有坑呢?咱們主要看一下獲取鎖失敗的場景
從上面這個邏輯來看問題不大,可是有個問題,case :
想基於redis實現一個相對靠譜的分佈式鎖,須要考慮的東西仍是比較多的,並且這種鎖並不太適用於業務要求特別嚴格的地方,如
盡信書則不如,已上內容,純屬一家之言,因本人能力通常,看法不全,若有問題,歡迎批評指正