重點講redis 分佈式鎖,後兩種持續更新中。。。
鎖:
當在單進程系統中,用到多線程時,多個線程改變一個變量,這時候,需要對變量或者代碼塊進行同步,避免多線程引發的線程不安全問題,即數據不一致。而同步的本質就是加鎖,目的是爲了實現多個線程同一時刻操作同一代碼的時候,只能有一個線程執行任務。
對於單機來講,可以加關鍵字 synchronized 或者 volatile 來實現同步也可以通過互斥鎖。
分佈式:
分佈式,與單機的區別是,單機是一個進程,當然可以是多線程,但分佈式是多進程。主要體現在集羣上。
單機可以共享堆內存,多進程的話,就需要用一個所有機器都能看到的地方。
根據CAP理論,即:一致性(Consistency),可用性(Availability),分區容錯性(Partition tolerance)
這三個基本需求,最多隻能同時滿足其中兩項。
對於分佈式系統而言,分區容錯性是基礎,是必不可少的,所以只能在 C 、A 中取捨了。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證最終一致性。
分佈式鎖:
一般分佈式鎖有三種實現方式:
1. 基於數據庫實現分佈式鎖
2. 基於緩存(redis,memcached,tair)實現分佈式鎖
3. 基於Zookeeper實現分佈式鎖
分佈式鎖一般滿足幾個條件:
- 互斥性。在任意時刻,只有一個客戶端能持有鎖。
- 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
- 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
- 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了
一:基於數據庫表做樂觀鎖
樂觀鎖的特點先進行業務操作,不到萬不得已不去拿鎖。即“樂觀”的認爲拿鎖多半是會成功的,因此在進行完業務操作需要實際更新數據的最後一步再去拿一下鎖就好。
一般是根據數據庫表的版本號(version)控制的, ---- 經典的ABA問題,銀行轉賬問題。
操作:
先執行select操作,查詢當前數據的數據版本號,再根據這個版本號爲條件,進行數據庫操作(update),並跟新版本號。
二:redis 分佈式鎖
注: 該加鎖方法僅針對單實例 Redis 可實現分佈式加鎖,對於 Redis 集羣則無法使用
基本鎖
- 原理:利用Redis的setnx如果不存在某個key則設置值,設置成功則表示取得鎖成功。
- 缺點:如果獲取鎖後的進程,在還沒執行完的時候掛調了,則鎖永遠不會釋放。
改進型
- 改進:在基本型是鎖上的setnx後設置expire,保證即使獲取鎖的進程不主動釋放鎖,過一段時間後也能自動釋放。
- 缺點:
- setnx與expire不是一個原子操作,可能執行完setnx該進程就掛了。
- 當鎖過期後,該進程還沒執行完,可能造成同時多個進程取得鎖。(貌似這個問題目前還沒有很優雅的解決方案)
再改進
- 改進:利用Lua腳本,將setnx與expire變成一個原子操作,可解決一部分問題。
- 缺點:還是鎖過期的問題。
Lua腳本:獲取鎖對應的value值,檢查是否與clientId相等,如果相等則刪除鎖(解鎖)
我們將Lua代碼傳到jedis.eval()方法裏,並使參數KEYS[1]賦值爲lockKey,ARGV[1]賦值爲clientId。eval()方法是將Lua代碼交給Redis服務端執行。
注:執行eval()方法可以確保原子性。就是在eval命令執行Lua代碼的時候,Lua代碼將被當成一個命令去執行,並且直到eval命令執行完成,Redis纔會執行其他命令
加鎖代碼:
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, milliseconds);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.set方法的參數介紹:
- 第一個爲key,我們使用key來當鎖,因爲key是唯一的。
- 第二個爲value,我們傳的是clientId,很多童鞋可能不明白,有key作爲鎖不就夠了嗎,爲什麼還要用到value?原因就是我們在上面講到可靠性時,分佈式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值爲requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。clientId可以使用UUID.randomUUID().toString()方法生成。
- 第三個爲nxxx,這個參數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作;
- 第四個爲expx,這個參數我們傳的是PX,意思是我們要給這個key加一個過期的設置,具體時間由第五個參數決定。
- 第五個爲time,與第四個參數相呼應,代表key的過期時間。
解鎖代碼:
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
這樣redis分佈式鎖就OK了
完整鎖代碼鏈接:redis分佈式鎖工具類
若是多機器部署redis,可以參考 redisson,這是Redis官方提供的Java組件。https://github.com/redisson/redisson
參考:https://www.cnblogs.com/suolu/p/6588902.html
http://www.cnblogs.com/seesun2012/p/9214653.html
https://www.cnblogs.com/szlbm/p/5588543.html
https://www.cnblogs.com/linjiqin/p/8003838.html