前言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命令執行失敗,一樣也有死鎖的風險。這兩步並不具有原子性,不保證所有成功或所有失敗。
正確的構建方式
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