從併發處理談PHP進程間通訊(一)外部介質

 

進程間通訊

進程間通訊(IPC,Inter-Process Communication),多進程開發中,進程間通訊是一個永遠也繞不開的問題。在 web開發中,咱們常常遇到的併發請求問題,本質上也能夠做爲進程間通訊來處理。javascript

進程間通訊,指至少兩個進程或線程間傳送數據或信號的一些技術或方法。進程是計算機系統分配資源的最小單位(嚴格說來是線程)。每一個進程都有本身的一部分獨立的系統資源,彼此是隔離的。爲了能使不一樣的進程互相訪問資源並進行協調工做,纔有了進程間通訊。php

根據定義可知,要進行進程間通訊,咱們須要解決兩個問題:css

  • 互相訪問:消息傳輸和暫時存儲介質選擇問題;
  • 協調工做:消息的存取衝突問題;

文章介紹的中心就是圍繞着這麼兩點來講的, 爲了更使文章更簡明,這邊以以前在公司作的一個需求爲例:html

須要一個循環ID生成器,循環生成從 Min 到 Max 的數字ID,在ID遞增到 Max 後,返回到 Min 從新開始遞增;必須能保證多個進程併發請求時生成的ID不一樣。java

此需求要解決的問題剛好爲咱們要解決的進程間通訊須要解決的兩個問題:python

  • 須要一個消息傳輸通道來傳輸和存儲當前的遞增值。這個比較容易解決,咱們經常使用的文件、數據庫、session、緩存等都能作到。
  • 須要解決多進程同時訪問生成器生成相同ID的問題。要知足這個須要就必需要用到鎖了,並且爲了保證多個進程讀取的數據是不一樣的,須要互斥鎖,另外爲了能保證調用成功率,鎖的獲取最好能實現自旋。

本文經過此需求的不一樣實現,來介紹經過外部介質進行的進程間通訊的方式。另外,不僅PHP語言,其餘語言也能使用這些方法。mysql

文章若有錯漏之處,煩請指出,若是您有更優的辦法,歡迎在下面留言討論。nginx


文件

flock

文件是最基本的存儲介質,它固然能夠做爲消息的傳輸通道來使用。文件的存取各類語言都有各自的多種方案,問題點是多進程併發時的衝突問題。git

解決存取衝突問題咱們使用PHP的 flock() 函數:github

bool flock ( resource $handle , int $operation [, int &$wouldblock ] )

  • $handler 是 使用fopen($path_to_file)獲取到的文件句柄;
  • $operation 是 對文件加鎖的方式,有如下值可選:

    LOCK_SH (獲取共享鎖) / LOCK_EX (獲取互斥鎖) / LOCK_UN (解鎖)

    這裏咱們選用互斥鎖,一個進程獲取到互斥鎖後,其餘進程在嘗試獲取鎖會被阻塞,直到鎖被釋放,即實現了自旋;

    此外,還有一個參數 LOCK_NB,flock 在獲取不到鎖時,默認會阻塞住直到鎖被其餘進程釋放,傳入 LOCK_NB 與 LOCK_SH 或 LOCK_EX 進行或運算結果(LOCK_EX | LOCK_NB),flock 在鎖被其餘進程佔有時,不會阻塞,而是直接返回 false,這裏僅做介紹,咱們並不使用它。

  • $wouldblock 參數是一個引用值,在獲取不到鎖,且不阻塞模式時,$wouldblock 會被設置爲 true;(手冊中說阻塞時纔會被設置爲 true。其實我也奇怪這個變量名的。不知道是否是 bug,個人PHP版本是 5.4.5,有知道的煩請解惑)

代碼實現

下面是循環ID生成器代碼,說明在註釋中:

function getCycleIdFromFile($max, $min = 0) {
    $handler = fopen('/tmp/cycle_id_generator.txt', 'c+');
    if (!flock($handler, LOCK_EX)) {
        throw new Exception('error_get_file_lock!');
    }
    
    $cycle_id = trim(fread($handler, 9));
    $cycle_id++;

    if ($cycle_id > $max) {
        $cycle_id = $min;
    }

    // 文件指針返回到文件頭,並向文件內寫入新的cycle_id
    rewind($handler);
    fwrite($handler, $cycle_id);

    // 多寫入一些空格爲了防止數值升到多位後,忽然置爲少位後面的數字仍保留
    fwrite($handler, str_repeat(' ', 9));

    flock($handler, LOCK_UN);

    return $cycle_id;
}

mysql

select for update

咱們經常使用的 mysql 也能夠被看成中間介質來實現進程間的通訊,咱們規定好某一個數據表內的某一行數據做爲消息交換的中轉站,使用 mysql 自帶的鎖來協調多個進程的存取衝突。

事務的設計目的就是爲了解決多進程併發查詢時數據衝突的問題,但是咱們經常使用的事務只能保證數據衝突時會被回滾,數據不會出現錯誤,並不能實現請求的並行化。對一些數據衝突回滾的請求,須要咱們在外層添加邏輯重試。

這裏介紹 mysql 的一種語法: select for update,會給固定數據加上互斥鎖,且另外一個請求在獲取鎖失敗時,會阻塞至獲取鎖成功,mysql 幫咱們實現了自旋;

用法以下:

  1. 關閉 mysql 的自動提交,自動提交默認打開,除非使用 transition 語句顯示開啓事務,默認會將每一條 sql 做爲一個事務直接提交執行,這裏關閉。 set autocommit=0;
  2. 使用select for update 語句給數據添加互斥鎖。注意:需求 mysql 的 innodb 引擎支持;
  3. 進行數據更新和處理操做;
  4. 主動提交事務,並將 自動提交恢復;commit; set autocommit=1;

代碼實現

而後是代碼實現:

// 數據庫鏈接實現各有不一樣,demo 能夠本身修改一下。
   function getCycleIdFromMysql($max, $min = 0){
        Db::db()->execute('set autocommit = 0');
        $res = Db::db()->qsqlone('SELECT cycle_id FROM cycle_id_generator WHERE id = 1 FOR UPDATE');

        $cycle_id = $res['cycle_id'] + 1;
        if($cycle_id > $max){
            $cycle_id = $min;
        }

        Db::db()->execute("UPDATE cycle_id_generator SET cycle_id = {$cycle_id} WHERE id = 1");

        Db::db()->execute('commit');
        Db::db()->execute('set autocommit = 1');

        return $cycle_id;
    }

redis

incr

redis 是咱們經常使用的緩存服務器,因爲其使用內存存儲數據,性能很高。咱們使用一個固定的普通鍵來做爲消息中轉站,而後利用其 incr 命令的原子性和其執行結果(遞增後的值),實現 cycle_id 的遞增。

incr(key) 若 key 不存在,redis 會先將值設置爲0,而後執行遞增操做;

遞增沒有問題,但是咱們還有個需求是在要其值達到 max 時,再將其置爲 min,這時就可能會出現進程A在更新值爲 min 時,另外一個進程B也檢測到值大於了 max,而後將值置爲 min,但是這時的值已經不是 max,即發生了值重複更新,那麼返回的值必然會有重複;

這時,咱們就須要本身來實現鎖了。

SETNX

redis 的 SETNX 命令檢測某一個 key 是否存在,若不存在,則將 key 的值設置爲 value,並返回結果1; 若 key 已存在,則設置失敗,返回值0。

SETNX key value

它能實現鎖是由於它是一個原子命令,即 檢測 key 是否存在和設置 key 值在一個事務內,不會出現同時兩個進程都檢測到 key 不存在,而後同時去設置 key 的狀況。

咱們以另外一個值的存在與否,來表示 cycle_id 是否正在被另外一個進程修改。

代碼實現

function getCycleIdFromRedis($max, $min = 0) {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
        $key_id = 'cycle_id_generator';

        $cycle_id = $redis->incr($key_id);
        
        if ($cycle_id > $max) {
            // 設置"鎖鍵"的結果 = 獲取互斥結果
            $key_lock = 'cycle_id_lock';
            if (!$redis->setnx($key_lock, 1)) {
                return null;
            }

            $cycle_id = $min;
            $redis->set($key_id, $cycle_id);

            // 最後別忘記釋放互斥鎖
            $redis->delete($key_lock);
        }

        $redis->close();

        return $cycle_id;
    }

注意:因爲 redis 裏沒有能實現自旋鎖的命令,若是需求最高的獲取成功率,咱們在檢測到 cycle_id 已是最大值,且試圖修改獲取鎖失敗時,退出重試,在外層進行重試。

function getCycleId($max, $min = 0) {
        $cycle_id = getCycleIdFromRedis($max, $min);
        if (!is_null($cycle_id)) {
            return $cycle_id;
        }
        // 稍微等待下正在更改的進程
        usleep(500);
        // 這裏使用遞歸,直至獲取成功  併發很高,cycle_id重置很頻繁時慎用.
        return getCycleId($max, $min);
    }

優化

審查代碼咱們會發現,若是 max-min 的值很小的話,redis 會須要常常重置 key 的值,也就常常須要加鎖,重試也就不少。這裏,我提供一個優化方法:

咱們將其 max 設置爲一個很大的值(要能被 max-min 整除),返回值時稍作處理,返回 $current % ($max - $min) + $min;。這樣,key 須要遞增到一個很大的值纔會被重置,加鎖邏輯和外層邏輯會不多執行到,達到提高效率的目的。

總結:

這裏簡單的評價一下上面所說的三種方法:

  • 性能上沒有測試,並且 redis 的性能跟 ID 的大小差值相關,不過猜想在ID大小差值大的狀況下 redis 應該更好一點。

  • 代碼上很是直觀,使用 mysql 很是簡潔,並且 redis 要本身實現自旋,比較噁心。

  • 實現上,固然是文件最爲方便,無任何添加。

本文介紹的都是經過外部介質來進行的通訊,下篇介紹下經過 PHP內置函數庫來進行進程間通訊,歡迎關注;

若是您以爲本文對您有幫助,您能夠點一下推薦。博客持續更新,歡迎關注。

相關文章
相關標籤/搜索