大牛舉例教你如何用Redis構建高性能鎖

前言redis

在這裏粗略的說一下,zk鎖性能比redis低的緣由:zk中的角色分爲leader,flower,每次寫請求只能請求leader,leader會把寫請求廣播到全部flower,若是flower都成功纔會提交給leader,其實這裏至關於一個2PC的過程。在加鎖的時候是一個寫請求,當寫請求不少時,zk會有很大的壓力,最後致使服務器響應很慢。安全

正題:性能優化

什麼狀況下須要加鎖?bash

當多個線程、用戶同時競爭同一個資源時,須要加鎖。好比,下訂單減庫存,搶票,選課,搶紅包等。若是在此處沒有鎖的控制,會致使很嚴重的問題,下訂單減庫存的時候不加鎖,會致使商品超賣;搶票的時候不加鎖,會致使兩我的搶到同一個位置;選課的時候沒有鎖的控制,致使選課成功的人數大於教室的座位數;搶紅包時沒有鎖的控制,搶到紅包的金額大於紅包的實際金額。服務器

什麼是分佈式鎖?網絡

學過JAVA多線程的朋友都知道,爲了防止多個線程同時執行同一段代碼,能夠用synchronized關鍵字或JAVA API中ReentrantLock類來控制。多線程

可是目前幾乎任何一個系統都每每部署多臺機器的,單機部署的應用不多,synchronized和ReentrantLock發揮不出任何做用,此時就須要一把全局的鎖,來代替JAVA中的synchronized和ReentrantLock。架構

當Thread1線程獲取到鎖,執行鎖中的代碼,其餘線程或其餘機器再次請求該鎖,發現鎖被Thread1佔用,加鎖失敗。當Thread1釋放鎖,其餘線程則能夠獲取到鎖並執行相應的操做。併發

咱們能夠用Jedis中是setnx命令來構建這把鎖,首先,我列舉一些錯誤的構建鎖的方式:分佈式

錯誤例子1

Long lock= jedis.setnx(key,value); 
if(lock>0){ 
//執行業務邏輯 
} 
複製代碼

經過setnx命令建立一個key、value,若是key不存在,則加鎖成功。這樣作有什麼問題呢?若是執行加鎖操做成功,在釋放鎖的時候,系統宕機,致使這個key永遠不會被del掉,也就是說其餘線程一直獲取不到鎖,

致使死鎖發生。爲了不這種狀況,請看下面的代碼

錯誤例子2

Long lock= jedis.setnx(key,value); 
if(lock>0){ 
 jedis.expire(key,expireTime); 
} 
複製代碼

和上面的例子相似,惟一不一樣的是這裏多了一步設置key過時時間的操做。若是在del的時候系統宕機,等過時時間一到,Redis會刪除這個key。

其餘線程能夠再次獲取鎖。這樣就能夠萬無一失了嗎?這裏有一個問題,若是在第一步setnx成功後,忽然網絡閃斷,expire命令執行失敗,一樣也有死鎖的風險。這兩步並不具有原子性,不保證所有成功或所有失敗。

順便給你們推薦一個Java技術交流羣: 710373545裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!

正確的構建方式

public static boolean getDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { 
 String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime); 
 if (LOCK_SUCCESS.equals(result)) { 
return true; 
 } 
return false; 
} 
複製代碼

參數解釋:

key:鍵

value:值

nx:若是當前key存在,則set失敗,不然成功

ex:設置key的過時時間

expireTime:key的過時時間,時間到了,Redis會自動刪除key和value。

這個命令,將上面的錯誤例子2中的兩個操做合爲一個原子操做,保證了同時成功或同時失敗。

解鎖方式:

錯誤例子1:

jedis.del(key); 
複製代碼

執行這個操做的線程,不去判斷鎖的擁有者就刪除鎖。

還記的set命令能夠設置value嗎?在獲取鎖的操做時,主要是判斷key是否存在,那麼value有什麼用呢??若是在刪除鎖的時候,不去判斷當前鎖的擁有者,任何線程均可以釋放鎖。這個時候,value值就起到做用了。

錯誤例子2:

if(value==jedis.get(key)){ 
 jedis.del(key); 
} 
複製代碼

咱們在加鎖的時候,能夠將value設置成惟一標識當前線程的一個值,這個值能夠是一個UUID,當釋放鎖的時間,判斷value是否和set時的值相同,若是相同,則說明加鎖和釋放鎖是同一個線程,容許釋放。不然釋放鎖失敗。

這樣就能夠絕對安全了嗎??答案固然是否認的。這步操做,一樣不具有原子性。若是ThreadA在執行value==jedis.get(key)返回true後的瞬間,del命令還沒來的及執行,key過時了,而此時ThreadB獲取到鎖,以後ThreadA執行del命令,把ThreadB的鎖釋放掉了。

因此要保證兩部操做的原子性,咱們不得不利用簡單的Lua腳本。

正確的解鎖姿式:

public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { 
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; 
 Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); 
 if (RELEASE_SUCCESS.equals(result)) { 
 return true; 
 } 
 return false; 
} 
複製代碼

Redis在2.6後內部內嵌Lua腳本解釋器,因此咱們能夠經過簡單的Lua腳原本保證上述操做的原子性。代碼中的Lua腳本的的意思是:咱們把LockKey賦值給KEYS[1],把RequestId賦值給ARGV[1],若是key中的值等於RequestId,返回true不然返回false。這樣就保證了釋放鎖操做時原子的,而且當前客戶端只會釋放當前客戶端的鎖。

原文連接:http://stor.51cto.com/art/201906/597813.htm?utm_source=tuicool&utm_medium=referral

相關文章
相關標籤/搜索