天天看點

雲客Drupal源碼分析之鎖lock服務

我們知道web伺服器是并發通路的,php腳本被不同的線程或程序執行,他們可能真正的同時執行,現在假設有個操作(比如計劃任務)在系統中隻能被執行一次,由于這種并發性就可能出現問題,兩個同時執行的請求可能帶來兩次執行,為了解決這個問題我們就需要“鎖LOCK”了。

鎖LOCK服務定義:

鎖是系統定義的一個服務,定義如下:

lock:
    class: Drupal\Core\Lock\DatabaseLockBackend
    arguments: ['@database']
    tags:
      - { name: backend_overridable }
    lazy: true
           

運作時該服務是通過代理類執行的,導出容器定義資料可以看到服務id:“lock”對應的是代理類:

Drupal\Core\ProxyClass\Lock\DatabaseLockBackend

在容器中,她真正的服務名是:

drupal.proxy_original_service.lock

代理類可以讓服務隻有在真正使用時才被執行個體化,而不是擷取時立即執行個體化。

擷取鎖服務的方法:$lock=\Drupal::lock()

此處$lock稱為鎖對象,由她來建立和管理鎖,從容器中隻能擷取一個鎖對象,當某些進階操作需要多個鎖對象時可以直接執行個體化:

$lock_1=new  \Drupal\Core\Lock\DatabaseLockBackend($database);

但需要多個鎖對象的情況非常罕見,系統中也沒有用到的案例,一般不需要。

鎖原理:

鎖的作用是進行排他操作,保證在同一時間段裡隻有得到鎖的請求才能進行被鎖定的操作,可保證在同一時間段裡的唯一性,避免并發問題,不被其他請求打擾。

drupal鎖系統在資料庫中設定一個資料表來存放鎖信号,表名為:“semaphore”,有三個字段:

name:

鎖名稱,代表着某一個操作,由開發者決定,往往使用函數名,資料表主鍵,不可重複

value:

鎖id,一個有唯一性的随機字元串,代表着一個鎖對象,相同鎖對象設定管理的所有鎖具備相同鎖id,往往一個請求隻有一個鎖對象,是以有時可以近似的講一個請求中設定的所有鎖使用相同id,通過她可以用來釋放一個請求中的所有鎖,但當一個請求有多個鎖對象時應該知道她代表鎖對象而非請求

expire:

unix時間戳,浮點值,代表鎖的到期時間,超期後鎖将失效(又稱為鎖被釋放)

當兩個或多個同時執行的并發請求欲執行一個隻能執行一次的操作時,她們應該擷取一個鎖,隻有得到鎖後才能開始操作,競争鎖的過程就是看誰先在鎖信号表中寫入資料的過程,一旦其中一個請求在該表中寫入了資訊,就意味着她得到了鎖,由于鎖名name是資料庫表的主鍵,主鍵不可以重複,一旦有寫入,那麼其他請求将不能再進行寫入,也就不能得到鎖,進而保證了在并發中隻有一個請求能得到鎖,進而被鎖定的操作隻能執行一次,本質上鎖是由資料庫來進行保障的,當鎖定的操作執行完成應該明确釋放鎖,也就是從資料庫中删除鎖資訊。

用法示例:

$operationID = "yunke"; 
        //代表着将要進行的操作的辨別符,往往可以設定為函數名,就是資料庫的name值
        $lock = \Drupal::lock(); //得到鎖對象
        if ($lock->acquire($operationID)) {
            // 隻有得到了鎖才能執行操作,否則應該繼續等待或放棄執行
            sleep(30); //在本代碼運作期間暫停30秒,30内你可以看到資料庫被寫入了鎖資訊
            $lock->release($operationID); 
            // 當操作結束後應當明确釋放鎖,依賴鎖自動過期有性能損耗
        }
           

鎖對象方法詳解:

在接口:

\Drupal\Core\Lock\LockBackendInterface

中有說明,這裡做進一步的解釋:

public function acquire($name, $timeout = 30.0)

申請一個鎖,如果申請到了将傳回true,否則false,參數$name為要使用的鎖名,應該是代表某種操作的辨別符,又開發者決定,不強制,往往是函數名,鎖名為ASCII編碼的字元串,小于等于255個字元,否則内部将使用Base64進行編碼,$timeout指定鎖的有效時間,一旦超過将失效,預設為30秒,開發者應該估算合适的時間值,在申請成功後還可以再次調用該方法,表示将鎖延期,新的鎖有效期是從目前開始加$timeout指定的時間,而不是在之前的到期時間基礎上加$timeout,由于這點當之前指定的有效期太長時可以再次調用指定一個短的有效期以盡快釋放鎖,如果再次調用時傳回false,那麼表示鎖已經被其他請求占用,應該放棄目前工作或繼續等待。

public function lockMayBeAvailable($name)

判斷鎖是否可以進行申請,比如雖然鎖被另外的請求申請到了,期效已過但卻沒有被釋放,那麼該方法将強制釋放,并傳回true表示鎖可申請,傳回true時表示此時可能成功申請到鎖,傳回false時表示鎖還在占用中

public function release($name)

根據鎖名釋放鎖,當鎖用完時,應該釋放她以便其他請求使用,如果鎖超期,即便沒有明确釋放,在其他請求申請時系統也會收回她

public function releaseAll($lock_id = NULL)

通過鎖id釋放鎖,這将釋放一個鎖對象管理的所有鎖,該方法被構造函數注冊為關機函數,在腳本結束時自動執行,參數$lock_id預設為null,此時将釋放本鎖對象管理的所有鎖,雲客認為該方法需要改進,開發者可能本意想通過鎖id能夠釋放其他鎖對象管理的鎖,但在該方法内卻又僅在本對象有配置設定鎖時才進行釋放操作,好在通常一個請求隻有一個鎖對象,無需這樣的操作;腳本結束時預設會釋放本鎖對象管理的所有鎖

public function getLockId()

得到鎖id,在鎖服務(不是鎖,而是鎖對象, $lock = \Drupal::lock();)生存周期内id不變,運用于所有的鎖,是一個随機字元串,每個鎖對象都不一樣,在同一個請求中如果初始化多個鎖對象,那麼該請求得到的鎖将有多個id,删除時需要用releaseAll方法,或者等待超期自動删除。

public function wait($name, $delay = 30)

當申請鎖失敗時,可以調用該方法以在$delay指定的時間内等待,最多等待$delay指定的時間,機關秒,預設30秒,等待期間一旦發現鎖有可能申請成功,那麼她立即傳回false,這表示不用再等,可馬上申請,但不保證一定能申請成功,如果傳回true則表示依然不能申請到鎖,需要繼續等待,在内部使用php函數usleep間隙性暫停代碼執行以等待申請時機,示例如下:

$lock = \Drupal::lock();
        $operationID = "yunke";
        $is_get_lock = false;
        $is_get_lock = $lock->acquire($operationID);
        if (!$is_get_lock) {
            if (!$lock->wait($operationID, 5)) {
                $is_get_lock = $lock->acquire($operationID);
            }
        }
        if ($is_get_lock) {
            //do something
            $lock->release($operationID);
        }
           

持久鎖:

系統還定義了一個持久鎖,用于系統安裝過程中,她的鎖id是固定的且沒有注冊關機函數,其他和鎖服務一樣,不做詳解,定義如下:

lock.persistent:
    class: Drupal\Core\Lock\PersistentDatabaseLockBackend
    arguments: ['@database']
    tags:
      - { name: backend_overridable }
    lazy: true
           

補充注意:

1、    鎖系統采用的是腳本運作所在主機上的時鐘,在管理者改變伺服器時間或者通過網絡時間協定NTP同步時間時将對鎖系統産生影響

2、    如果網站有多台主機,每個主機時間可能有差異,這将可能導緻問題。

3、    在進行被鎖定資源的共享時,也必須共享存放鎖的資訊表“semaphore”,對于系統之外的操作,鎖系統并不能阻止

我是雲客,【雲遊天下,做客四方】,聯系方式見首頁,歡迎轉載,但須注明出處

繼續閱讀