锁和分布式锁
2019獨角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
鎖的由來?:
多線程環(huán)境中,經(jīng)常遇到多個線程訪問同一個 共享資源 ,這時候作為開發(fā)者必須考慮如何維護數(shù)據(jù)一致性,這就需要某種機制來保證只有滿足某個條件(獲取鎖成功)的線程才能訪問資源,而不滿足條件(獲取鎖失敗)的線程只能等待,在下一輪競爭中來獲取鎖才能訪問資源。
兩個知識點:
1.高級緩存Cache
?
?
?
?
?
CPU為了提高處理速度,不和內(nèi)存直接進行交互,而是使用Cache。
可能引發(fā)的問題:
?
?
?
?
?
如果多個處理器同時對共享變量進行讀改寫操作 (i++就是經(jīng)典的讀改寫操作),那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的了,操作完之后共享變量的值會和期望的不一致。
造成此結(jié)果的原因:
多個處理器同時從各自的緩存中讀取變量i,分別進行加1操作,然后分別寫入 系統(tǒng)內(nèi)存中。
處理器層面的解決方案:
處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個 LOCK#信號,當(dāng)一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內(nèi)存。
2.CAS(Compare And Swap)+volatile
CAS 操作包含三個操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。執(zhí)行CAS操作的時候,將內(nèi)存位置的值與預(yù)期原值比較,如果相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。
java的Atomic以及一些它自帶的類中的cas操作都是通過借助cmpxchg指令完成的。他保證同一時刻只能有一個線程cas成功。
舉個例子
以AtomicIneger的源碼為例來看看CAS操作:
?
?
?
?
?
for(;;)表示循環(huán),只有當(dāng)if判斷為true才退出。而if判斷的內(nèi)容就是是否CAS成功。
?
?
?
?
?
?
?
?
?
?
volatile的作用:
1)將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
2)這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
循環(huán)CAS+volatile是實現(xiàn)鎖的關(guān)鍵。
Lock鎖的部分細節(jié)
?
?
?
?
?
?
?
?
?
?
不同場景鎖的表現(xiàn)不同:獨占?共享?讀寫?
?
?
?
?
?
分布式鎖(redis的簡單實現(xiàn))
分布式鎖實現(xiàn)的三個核心要素:
- 1.加鎖
最簡單的方法是使用setnx命令。key是鎖的唯一標(biāo)識,按業(yè)務(wù)來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名為 “l(fā)ock_sale_商品ID” 。而value設(shè)置成什么呢?我們可以姑且設(shè)置成1。加鎖的偽代碼如下:
setnx(key,1)?
SETNX key value
將 key 的值設(shè)為 value ,當(dāng)且僅當(dāng) key 不存在。
若給定的 key 已經(jīng)存在,則 SETNX 不做任何動作。
SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。 時間復(fù)雜度: O(1) 返回值: 設(shè)置成功,返回 1 。 設(shè)置失敗,返回 0 。當(dāng)一個線程執(zhí)行setnx返回1,說明key原本不存在,該線程成功得到了鎖;當(dāng)一個線程執(zhí)行setnx返回0,說明key已經(jīng)存在,該線程搶鎖失敗。
- 2.解鎖
有加鎖就得有解鎖。當(dāng)?shù)玫芥i的線程執(zhí)行完任務(wù),需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執(zhí)行del指令,偽代碼如下:
del(key)釋放鎖之后,其他線程就可以繼續(xù)執(zhí)行setnx命令來獲得鎖。
- 3.設(shè)置超時時間
如果一個得到鎖的線程在執(zhí)行任務(wù)的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,別的線程再也別想進來。
所以,setnx的key必須設(shè)置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間后自動釋放。setnx不支持超時參數(shù),所以需要額外的指令,偽代碼如下:
expire(key, 30)綜合起來,我們分布式鎖實現(xiàn)的第一版?zhèn)未a如下:
if(setnx(key,1) == 1){expire(key,30)do something ......del(key)}上述代碼的問題:
- 1 setnx和expire的非原子性
?
?
?
?
?
setnx剛執(zhí)行成功,還未來得及執(zhí)行expire指令,節(jié)點1 Duang的一聲掛掉了。
?
?
?
?
?
這樣一來,這個鎖就長生不死了。
解決方案:
Redis 2.6.12以上版本為set指令增加了可選參數(shù),偽代碼如下:
set(key,1,30,NX)- 2 del 導(dǎo)致誤刪
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
可以在del釋放鎖之前做一個判斷,驗證當(dāng)前的鎖是不是自己加的鎖
至于具體的實現(xiàn),可以在加鎖的時候把當(dāng)前的線程ID當(dāng)做value,并在刪除之前驗證key對應(yīng)的value是不是自己線程的ID。
加鎖:
String threadId = Thread.currentThread().getId() set(key,threadId ,30,NX)解鎖:
if(threadId .equals(redisClient.get(key))){del(key) }這樣做又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操作,不是原子性。
這一塊要用Lua腳本來實現(xiàn):
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));redis官方說:eval命令在執(zhí)行l(wèi)ua腳本時會當(dāng)作一個命令去執(zhí)行,并且直到命令執(zhí)行完成redis才會去執(zhí)行其他命令,所以就變成了一個原子操作。
- 3出現(xiàn)并發(fā)的可能性
進程1在超時時間內(nèi)未執(zhí)行完代碼,此時進程2是可以獲取鎖的,會出現(xiàn)兩個進程同時訪問一個資源的情況。
解決方案:可以在進程1所在的jvm環(huán)境中開一個線程專門用來“續(xù)命”,當(dāng)需要解鎖的時候,通知這個續(xù)命線程結(jié)束執(zhí)行。
private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";/*** 嘗試獲取分布式鎖* @param jedis Redis客戶端* @param lockKey 鎖* @param requestId 線程Id* @param expireTime 超期時間* @return 是否獲取成功*/public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_SUCCESS.equals(result)) {return true;}return false;} private static final Long RELEASE_SUCCESS = 1L;/*** 釋放分布式鎖* @param jedis Redis客戶端* @param lockKey 鎖* @param requestId 請求標(biāo)識* @return 是否釋放成功*/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;}歡迎工作一到五年的Java工程師朋友們加入Java架構(gòu)開發(fā): 855835163
群內(nèi)提供免費的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構(gòu)資料)合理利用自己每一分每一秒的時間來學(xué)習(xí)提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
轉(zhuǎn)載于:https://my.oschina.net/u/3959468/blog/2878407
總結(jié)
- 上一篇: elasticsearch批量修改,批量
- 下一篇: Cesium入门11 - Interac