蘋果 iOS 15 新版 FaceTime:空間音訊 / 語音增強 / 實時共享音視訊
Redis 做分散式鎖
分散式鎖也算是 Redis 比較常見的使用場景。
問題場景:
例如一個簡單的使用者操作,一個線城去修改使用者的狀態,首先從資料庫中讀出使用者的狀態,然後在記憶體中進行修改,修改完成後,再存回去。在單執行緒中,這個操作沒有問題,但是在多執行緒中,由於讀取、修改、存 這是三個操作,不是原子操作,所以在多執行緒中,這樣會出問題。
對於這種問題,我們可以使用分散式鎖來限制程式的併發執行。
基本用法
分散式鎖實現的思路很簡單,就是進來一個線城先佔位,當別的線城進來操作時,發現已經有人佔了,就會放棄或者稍後再試。
在 Redis 中,佔位一般使用 setnx
指令,先進來的線城先佔位,線城的操作執行完成後,再呼叫 del
根據上面的思路,我們寫出的程式碼如下
package com.sdz.distributed_lock; public class distributed_lock { public static void main(String[] args) { Redis redis = new Redis(); redis.execute(jedis->{ Long setnx = jedis.setnx("k1", "v1"); if (setnx == 1) { //沒人佔位 jedis.set("name", "sdz"); String name = jedis.get("name"); System.out.println(name); jedis.del("k1");//釋放資源 }else{ //有人佔位,停止/暫緩 操作 } }); } }
上面的程式碼存在一個小小問題:如果程式碼業務執行的過程中拋異常或者掛了,這樣會導致 del
指令沒有被呼叫,這樣,k1 無法釋放,後面來的請求全部堵塞在這裡,鎖也永遠得不到釋放。
要解決這個問題,我們可以給鎖新增一個過期時間,確保鎖在一定的時間之後,能夠得到釋放。改進後的程式碼如下:
package com.sdz.distributed_lock; public class distributed_lock { public static void main(String[] args) { Redis redis = new Redis(); redis.execute(jedis->{ Long setnx = jedis.setnx("k1", "v1"); if (setnx == 1) { //給鎖新增一個過期時間,防止應用在執行過程中丟擲異常導致鎖無法及時得到釋放 jedis.expire("k1", 5); //沒人佔位 jedis.set("name", "sdz"); String name = jedis.get("name"); System.out.println(name); jedis.del("k1");//釋放資源 }else{ //有人佔位,停止/暫緩 操作 } }); } }
這樣改造之後,還有一個問題,就是在獲取鎖和設定過期時間之間如果如果伺服器突然掛掉了,這個時候鎖被佔用,無法及時得到釋放,也會造成死鎖,因為獲取鎖和設定過期時間是兩個操作,不具備原子性。
為了解決這個問題,從 Redis2.8 開始,setnx 和 expire 可以通過一個命令一起來執行了,我們對上述程式碼再做改進:
package com.sdz.distributed_lock;
import redis.clients.jedis.params.SetParams;
public class distributed_lock {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
if (set !=null && "OK".equals(set)) {
//給鎖新增一個過期時間,防止應用在執行過程中丟擲異常導致鎖無法及時得到釋放
jedis.expire("k1", 5);
//沒人佔位
jedis.set("name", "sdz");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");//釋放資源
}else {
//有人佔位,停止/暫緩 操作
}
});
}
}
解決超時問題
為了防止業務程式碼在執行的時候丟擲異常,我們給每一個鎖添加了一個超時時間,超時之後,鎖會被自動釋放,但是這也帶來了一個新的問題:如果要執行的業務非常耗時,可能會出現紊亂。舉個例子:第一個執行緒首先獲取到鎖,然後開始執行業務程式碼,但是業務程式碼比較耗時,執行了 8 秒,這樣,會在第一個執行緒的任務還未執行成功鎖就會被釋放了,此時第二個執行緒會獲取到鎖開始執行,在第二個執行緒剛執行了 3 秒,第一個執行緒也執行完了,此時第一個執行緒會釋放鎖,但是注意,它釋放的第二個執行緒的鎖,釋放之後,第三個執行緒進來。
對於這個問題,我們可以從兩個角度入手:
- 儘量避免在獲取鎖之後,執行耗時操作。
- 可以在鎖上面做文章,將鎖的 value 設定為一個隨機字串,每次釋放鎖的時候,都去比較隨機字串是否一致,如果一致,再去釋放,否則,不釋放。
對於第二種方案,由於釋放鎖的時候,要去檢視鎖的 value,第二個比較 value 的值是否正確,第三步釋放鎖,有三個步驟,很明顯三個步驟不具備原子性,為了解決這個問題,我們得引入 Lua 指令碼。
Lua 指令碼的優勢:
- 使用方便,Redis 中內建了對 Lua 指令碼的支援。
- Lua 指令碼可以在 Redis 服務端原子的執行多個 Redis 命令。
- 由於網路在很大程度上會影響到 Redis 效能,而使用 Lua 指令碼可以讓多個命令一次執行,可以有效解決網路給 Redis 帶來的效能問題。
在 Redis 中,使用 Lua 指令碼,大致上兩種思路:
- 提前在 Redis 服務端寫好 Lua 指令碼,然後在 Java 客戶端去呼叫指令碼(推薦)。
- 可以直接在 Java 端去寫 Lua 指令碼,寫好之後,需要執行時,每次將指令碼傳送到 Redis 上去執行。
首先在 Redis 服務端建立 Lua 指令碼,內容如下:
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
接下來,可以給 Lua 指令碼求一個 SHA1 和,命令如下:
cat lua/releasewherevalueequal.lua | redis-cli -a javaboy script load --pipe
script load 這個命令會在 Redis 伺服器中快取 Lua 指令碼,並返回指令碼內容的 SHA1 校驗和,然後在Java 端呼叫時,傳入 SHA1 校驗和作為引數,這樣 Redis 服務端就知道執行哪個指令碼了。
接下來,在 Java 端呼叫這個指令碼。
package com.sdz.distributed_lock;
import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
import java.util.UUID;
public class distributed_lock {
public static void main(String[] args) {
Redis redis = new Redis();
for (int i = 0; i < 2; i++) {
redis.execute(jedis -> {
//1.先獲取一個隨機字串
String value = UUID.randomUUID().toString();
//2.獲取鎖
String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
//3.判斷是否成功拿到鎖
if (k1 != null && "OK".equals(k1)) {
//4. 具體的業務操作
jedis.set("name", "sdz");
String site = jedis.get("name");
System.out.println(site);
//5.釋放鎖
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8",
Arrays.asList("k1"), Arrays.asList(value));
} else {
System.out.println("沒拿到鎖");
}
});
}
}
}