亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

1. 前言

對于一個在線運行的系統,如果需要修改數據庫已有數據,需要先讀取舊數據,再寫入新數據。因為讀數據和寫數據不是原子操作,所以在高并發的場景下,關注的數據可能會修改失敗,需要使用鎖控制。

2. 分布式場景

2.1 分布式鎖場景

面試官提問: 為什么要使用分布式鎖?分布式鎖解決了什么問題?

題目解析:

首先分析鎖的應用場景,我們對于已有數據的修改可以歸納為兩個動作:

(1)讀舊數據;
(2)寫新數據。

然后分析并發操作導致臟數據的過程:

圖片描述

(并發場景問題)

對于并發執行的兩次請求,兩個請求同時讀到舊數據值為 10,第一個請求執行操作后新值為 30,第二個請求執行操作后新值為 40,最終只有第二次請求成功寫入數據實體,導致第一次請求失效。

在單機部署的系統中,我們可以直接使用本地的鎖(例如 Java 的 Object 對象鎖)解決上述的并發沖突問題,但是當服務器分布式部署時,單機的鎖并不能跨網絡調用,所以需要使用分布式鎖解決問題。

2.2 Redis 分布式鎖

面試官提問: 既然談到了分布式鎖的應用場景,在實戰環境是如何實現分布式鎖的呢?

題目解析:

目前分布式鎖最主要有三種實現方式:

(1)基于 Redis 集群的模式;
(2)基于 Zookeeper 集群的模式;
(3)基于 DB 數據庫的模式

本章節只關注 Redis 的部分,核心思路是通過 setnx 指令,實例:

    public static void wrongWayLock(Jedis jedis, String prefix_key, String id, int expire_time) {
        // 加鎖
        Long result = jedis.setnx(prefix_key, id);
        if (result==1){
            // 如果加鎖成功,設置過期時間   
            jedis.expire(prefix_key,expire_time);
        }
    }

加鎖步驟主要分為兩步:

(1)通過 setnx 指令加鎖,setnx 的含義是 set if not exist,即如果 redis 不存在已有的 prefix_key ,則寫入 prefix_key ,設置對應 value=id,并且調用返回為 1,如果已有 prefix_key ,則不寫入并且返回非 1.

(2)通過 expire 指令,設置過期時間,如果 prefix_key 代表的鎖一直沒有刪除,則在定時后自動失效,防止產生死鎖的情況。

上述代碼并不完美,其中 setnx()expire() 函數并不是原子操作,如果執行 setnx() 指令之后,redis 集群出現網絡抖動或者在線服務本身異常,導致后續 expire() 指令并沒有執行,prefix_key 代表的鎖并沒有被加上過期時間,還是有產生死鎖的可能性,我們對上述代碼進行改造,實例:

    public static boolean setLock(Jedis jedis, String prefix_key, String id, int expire_time) {
        if(jedis.set(prefix_key, id, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expire_time) == 1) {
            return true; //加鎖成功
        }
        return false; //加鎖失敗
    }

這種方案是將加鎖和設置過期時間合并為一個步驟,一次 set,是原子操作。另外還有諸多開源代碼解決這個問題,例如通過開源 lua 腳本,基于 redis 集群進行改造。

既然有加鎖的過程,就有操作執行結束之后釋放鎖的過程,實例:

    public static void unLock(Jedis jedis, String prefix_key, String id){
      	//如果在集群中存在prefix_key的值,并且和之前配置的id相同
        if(id.equals(jedis.get(prefix_key))){
          	//刪除prefix_key鍵值對
            jedis.del(prefix_key);
        }
    }

使用分布式鎖都是為了應對高并發的場景,高并發場景下,上述代碼存在嚴重的并發執行問題。

例如第一行 if 判斷完成之后,其他線程已經提前進入條件判斷并且執行了 del 操作,當前線程再執行 del 操作就不合理。

還是出現了沒有保證操作原子性的問題,通用的解決方案是通過 lua 腳本的 eval() 函數,首先獲取鎖對應的 value(即我們的 id ),如果相等

才刪除鎖,lua 腳本能保證原子性,實例:

    public  boolean unlock(String prefix_key,String request){
        //lua腳本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = jedis.eval(script, Collections.singletonList(prefix_key), Collections.singletonList(id));
        if (result == 1){
            return true ;
        }
        return false;
    }

3. 小結

本章節介紹了使用 Redis 實現最基礎的分布式鎖問題,給出了滿足原子性的加鎖和解鎖操作,需要候選人能夠給面試官清晰解釋兩步操作的關注點。另外,本章節對于一些可能存在的問題沒有給出具體解決方案,例如 prefix_key 經過超時時間后自動過期,但是業務還沒有執行完成,以及 Redis 集群的主從同步可能發生的宕機問題。