1. 程式人生 > 資訊 >蘋果 iOS 15 新版 FaceTime:空間音訊 / 語音增強 / 實時共享音視訊

蘋果 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 指令碼,大致上兩種思路:

  1. 提前在 Redis 服務端寫好 Lua 指令碼,然後在 Java 客戶端去呼叫指令碼(推薦)。
  2. 可以直接在 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("沒拿到鎖");
                }
            });
        }
    }

}