国产+高潮+在线,国产 av 仑乱内谢,www国产亚洲精品久久,51国产偷自视频区视频,成人午夜精品网站在线观看

高速共享緩存插件分享

chaz6chez

前言

今年接觸了一個(gè)策略類(lèi)手游相關(guān)的項(xiàng)目,后端本身計(jì)劃是使用skynet進(jìn)行開(kāi)發(fā)的,后來(lái)結(jié)合項(xiàng)目的時(shí)間緊急程度和客戶端開(kāi)發(fā)組討論后決定使用PHP進(jìn)行快速開(kāi)發(fā),后期再使用其他語(yǔ)言框架進(jìn)行拆分業(yè)務(wù);綜合考慮最后選用了webman作為主要開(kāi)發(fā)框架。

整體項(xiàng)目分為配置服務(wù)、http-api服務(wù)、websocket服務(wù)三大部分,其中配置管理主要是兼容客戶端生成的配置數(shù)據(jù)進(jìn)行導(dǎo)入導(dǎo)出轉(zhuǎn)換加載,底層使用MySQL進(jìn)行儲(chǔ)存,多服務(wù)間使用Redis進(jìn)行一級(jí)緩存,服務(wù)進(jìn)程間使用了基于APCu的共享緩存,后期我將該共享緩存組件化也貢獻(xiàn)給了社區(qū)。
http://wtbis.cn/plugin/133

Redis

在游戲開(kāi)發(fā)界實(shí)際上使用Redis的情況還是比較多的,我們使用Redis主要還是為了將一些數(shù)據(jù)緩存共享給各個(gè)服務(wù)器實(shí)例:


     ┌─────┐                                       ┌─────┐
     |  A  | ────────────>  service  <──────────── |  B  |
     └─────┘                                       └─────┘
    /   |   \                                     /   |   \
┌───┐ ┌───┐ ┌───┐                             ┌───┐ ┌───┐ ┌───┐
| a | | b | | c | ───────>  instance <─────── | a | | b | | c |
└───┘ └───┘ └───┘                             └───┘ └───┘ └───┘
  |     |     |                                 |     |     |
 1|2   1|2   1|2 ────────>  process  <──────── 1|2   1|2   1|2
 3|4   3|4   3|4                               3|4   3|4   3|4

如圖所示,我們A/B為區(qū)服,每個(gè)區(qū)服下可能存在abc不同的服務(wù)器實(shí)例,他們需要共享相同的區(qū)服配置;每個(gè)區(qū)服各自管理自己的數(shù)據(jù)庫(kù)數(shù)據(jù)區(qū)域/數(shù)據(jù)庫(kù)實(shí)例;每個(gè)區(qū)服下的服務(wù)器實(shí)例對(duì)于數(shù)據(jù)庫(kù)數(shù)據(jù)的要求是強(qiáng)需求,且為變動(dòng)較為頻繁的數(shù)據(jù)內(nèi)容,與web的微服務(wù)有區(qū)別,所以我們沒(méi)有使用類(lèi)似Nacos或者其他配置中心進(jìn)行處理,從而用更適配當(dāng)前場(chǎng)景的Redis作為緩存服務(wù)。

同時(shí)Redis也可以作為用戶登錄鑒權(quán)相關(guān)中的一環(huán),也可以為運(yùn)營(yíng)相關(guān)功能提供一些輔助,比如使用Redis-Stream作為消息隊(duì)列,處理一些事件通知等。

共享內(nèi)存

在游戲開(kāi)發(fā)中,許多業(yè)務(wù)都是在內(nèi)存中進(jìn)行的計(jì)算處理,而我們上述的模式是多進(jìn)程模式,進(jìn)程間通訊是一個(gè)比較頻繁出現(xiàn)的點(diǎn);一開(kāi)始解決這個(gè)問(wèn)題是粗暴的將一些固定業(yè)務(wù)固定在對(duì)應(yīng)的進(jìn)程上執(zhí)行,盡可能避免進(jìn)程間的通訊問(wèn)題,后來(lái)隨著業(yè)務(wù)逐步的擴(kuò)大,單純限制業(yè)務(wù)是沒(méi)辦法完全實(shí)現(xiàn)的,這時(shí)候有考慮過(guò)使用webman的channel;但實(shí)際上channel基于socket涉及系統(tǒng)內(nèi)核態(tài)用戶態(tài)的拷貝等問(wèn)題,同時(shí)受網(wǎng)絡(luò)影響受限,在一些業(yè)務(wù)的計(jì)算處理上會(huì)帶來(lái)比較高的延遲,包括Redis也同樣是這樣的問(wèn)題,我們需要實(shí)現(xiàn)數(shù)據(jù)的零拷貝。

后續(xù)我們的目標(biāo)鎖定在了共享內(nèi)存上,因?yàn)楣蚕韮?nèi)存可以輕易的在進(jìn)程間進(jìn)行通訊交換,而且不存在深拷貝和網(wǎng)絡(luò)等問(wèn)題,效率、性能非常的高,整體微秒級(jí)別的響應(yīng)滿足我們的需求;于是我基于PHP的拓展APCu封裝了適合我們業(yè)務(wù)場(chǎng)景的插件包進(jìn)行使用。

webman-shared-cache

我們的基礎(chǔ)應(yīng)用實(shí)現(xiàn)了定時(shí)器來(lái)從MySQL數(shù)據(jù)庫(kù)讀取配置信息,定時(shí)器的處理器也在讀取數(shù)據(jù)刷入Redis的同時(shí)觸發(fā)共享內(nèi)存的更新事件,上層業(yè)務(wù)通過(guò)更新事件的回調(diào)出發(fā)會(huì)將Redis的數(shù)據(jù)刷入共享內(nèi)存中,以便當(dāng)前區(qū)服實(shí)例的各個(gè)進(jìn)程能夠使用。

我們使用緩存的場(chǎng)景很多都是MAP數(shù)據(jù),所以我在實(shí)現(xiàn)插件的時(shí)候特別實(shí)現(xiàn)了類(lèi)似Redis-Hash相關(guān)的功能:

  • HSet/HGet/HDel/HKeys/HExists

由于我們需要一些自增自減的運(yùn)算,所以也實(shí)現(xiàn)了以下功能點(diǎn):

  • HIncr/HDecr,支持浮點(diǎn)運(yùn)算

由于APCu的特性所以?xún)?chǔ)存的數(shù)據(jù)也是支持儲(chǔ)存對(duì)象數(shù)據(jù)的;

webman-shared-cache為何使用鎖?

之前我有和社區(qū)的同學(xué)們聊過(guò),他們不是很理解為什么我在實(shí)現(xiàn)插件的時(shí)候自己使用了鎖,這是因?yàn)锳PCu本身的自行實(shí)現(xiàn)了對(duì)它自身函數(shù)的原子性操作,但我們使用它的時(shí)候是在多進(jìn)程的環(huán)境下,每一個(gè)進(jìn)程內(nèi)存在多次APCu的操作,為了業(yè)務(wù)的原子性,我們希望這多次的操作要在一個(gè)原子性?xún)?nèi)完成,所以需要一個(gè)鎖來(lái)進(jìn)行隔離,以免在多進(jìn)程的環(huán)境下被其他進(jìn)程的操作污染,整體是類(lèi)似MySQl的事務(wù)的:

protected static function _HIncr(string $key, string|int $hashKey, int|float $hashValue = 1): bool|int|float
{
    $func = __FUNCTION__;
    $result = false;
    $params = func_get_args();
    self::_Atomic($key, function () use (
        $key, $hashKey, $hashValue, $func, $params, &$result
    ) {
        $hash = self::_Get($key, []);
        if (is_numeric($v = ($hash[$hashKey] ?? 0))) {
            $hash[$hashKey] = $result = $v + $hashValue;
            self::_Set($key, $hash);
        }
        return [
            'timestamp' => microtime(true),
            'method'    => $func,
            'params'    => $params,
            'result'    => null
        ];
    }, true);
    return $result;
}

比如上述代碼,就是一個(gè)Hash key的自增操作,我們需要在讀取Hash后在寫(xiě)入,讀取和寫(xiě)入應(yīng)為一體的;

原子性執(zhí)行函數(shù)Atomic的實(shí)現(xiàn)如下:

    /**
     * 原子操作
     *  - 無(wú)法對(duì)鎖本身進(jìn)行原子性操作
     *  - 只保證handler是否被原子性觸發(fā),對(duì)其邏輯是否拋出異常不負(fù)責(zé)
     *  - handler盡可能避免超長(zhǎng)阻塞
     *  - lockKey會(huì)被自動(dòng)設(shè)置特殊前綴#lock#,可以通過(guò)Cache::LockInfo進(jìn)行查詢(xún)
     *
     * @param string $lockKey
     * @param Closure $handler
     * @param bool $blocking
     * @return bool
     */
    protected static function _Atomic(string $lockKey, Closure $handler, bool $blocking = false): bool
    {
        $func = __FUNCTION__;
        $result = false;
        if ($blocking) {
            $startTime = time();
            while ($blocking) {
                // 阻塞保險(xiǎn)
                if (time() >= $startTime + self::$fuse) {return false;}
                // 創(chuàng)建鎖
                apcu_entry($lock = self::GetLockKey($lockKey), function () use (
                    $lockKey, $handler, $func, &$result, &$blocking
                ) {
                    $res = call_user_func($handler);
                    $result = true;
                    $blocking = false;
                    return [
                        'timestamp' => microtime(true),
                        'method'    => $func,
                        'params'    => [$lockKey, '\Closure'],
                        'result'    => $res
                    ];
                });
            }
        } else {
            // 創(chuàng)建鎖
            apcu_entry($lock = self::GetLockKey($lockKey), function () use (
                $lockKey, $handler, $func, &$result
            ) {
                $res = call_user_func($handler);
                $result = true;
                return [
                    'timestamp' => microtime(true),
                    'method'    => $func,
                    'params'    => [$lockKey, '\Closure'],
                    'result'    => $res
                ];
            });
        }
        if ($result) {
            apcu_delete($lock);
        }
        return $result;
    }

當(dāng)使用阻塞模式的時(shí)候,我們會(huì)在當(dāng)前進(jìn)程內(nèi)使用一個(gè)while循環(huán)來(lái)進(jìn)行阻塞搶占,為了不將當(dāng)前進(jìn)程阻塞死,我們還加入了一個(gè)保險(xiǎn),由self::$fuse提供;

注意

這里在實(shí)踐過(guò)程中需要注意的是,Atomic在傳入回調(diào)函數(shù)時(shí)切勿再使用匿名函數(shù)作為參數(shù)值或者是通過(guò)use傳入一個(gè)匿名函數(shù),如:

$fuc = function() {
    // do something
}
Cache::Atomic('test', function () use ($fuc) {
    // do anything
})

APCu底層會(huì)對(duì)函數(shù)參數(shù)值或引用參數(shù)進(jìn)行序列化儲(chǔ)存,但匿名函數(shù)不可以被序列化,所以會(huì)拋出一個(gè)異常;但你可以通過(guò)當(dāng)前對(duì)象的屬性值或者靜態(tài)屬性來(lái)保存一個(gè)匿名函數(shù),然后在Atomic的回調(diào)內(nèi)調(diào)用使用。

0.4.x版本

由于目前我使用Webman基于SQLite和共享內(nèi)存在自行實(shí)現(xiàn)一個(gè)具備RAFT的輕調(diào)度服務(wù)插件和服務(wù)注冊(cè)與發(fā)現(xiàn)插件,所以特此為其完善增加了Channel特性;

Channel可以輔助實(shí)現(xiàn)類(lèi)似Redis-List、Redis-stream、Redis-Pub/Sub的功能。

Channel

Channel是個(gè)特殊的數(shù)據(jù)格式,他的格式是固定如下的:

[
    '--default--' => [
        'futureId' => null,
        'value'    => []
    ],
    workerId_1 => [
        'futureId' => 1,
        'value'    => []
    ],
    workerId_2 => [
        'futureId' => 1,
        'value'    => []
    ],
    ......
]

它在共享內(nèi)存中的鍵默認(rèn)以 #Channel# 開(kāi)頭。

  • --default--是默認(rèn)儲(chǔ)存空間,workerId_1/workerId_2等是子通道儲(chǔ)存空間,命名是由用戶代碼傳入的,這里建議使用workerman自帶的workerId即可。

  • 默認(rèn)儲(chǔ)存空間和子通道儲(chǔ)存空間是互斥的,也就是說(shuō)當(dāng)存在子通道儲(chǔ)存空間時(shí),是不存在--default--的,反之亦然;子通道儲(chǔ)存空間是當(dāng)當(dāng)前通道存在監(jiān)聽(tīng)器時(shí)生成的,而在監(jiān)聽(tīng)器產(chǎn)生前,消息會(huì)暫存在--default--空間,當(dāng)監(jiān)聽(tīng)器創(chuàng)建時(shí),--default--的數(shù)據(jù)value會(huì)被同步到子通道儲(chǔ)存空間內(nèi),加入value的隊(duì)頭

  • 每一個(gè)子通道儲(chǔ)存空間的value都是拷貝的,存在相同的數(shù)據(jù),各自監(jiān)聽(tīng)器監(jiān)聽(tīng)各自的子通道儲(chǔ)存空間;消息的發(fā)布支持向所有子通道發(fā)布,也可以指定子通道進(jìn)行發(fā)布。

  • 監(jiān)聽(tīng)器的底層使用了workerman的定時(shí)器,區(qū)別與workerman的timer,在event驅(qū)動(dòng)下定時(shí)器的間隔是0,也就是一個(gè)future,而其他的事件驅(qū)動(dòng)是0.001s為間隔。

實(shí)現(xiàn)一個(gè)List

由于監(jiān)聽(tīng)器創(chuàng)建消費(fèi)是基于workerId的,我們可以通過(guò)不同進(jìn)程創(chuàng)建相同的workerId的監(jiān)聽(tīng)器來(lái)對(duì)同一個(gè)子通道進(jìn)行監(jiān)聽(tīng):

  1. A進(jìn)程使用list作為workerId:

    Cache::ChCreateListener('test', 'list', function(string $channelKey, string|int $workerId, mixed $message) {
    // TODO 你的業(yè)務(wù)邏輯
    });
  2. B進(jìn)程也同樣創(chuàng)建list的workerId監(jiān)聽(tīng)器:

    Cache::ChCreateListener('test', 'list', function(string $channelKey, string|int $workerId, mixed $message) {
    // TODO 你的業(yè)務(wù)邏輯
    });
  3. 此時(shí)Channel test的數(shù)據(jù)如下:

    [
    'list' => [
        'futureId' => 1,
        'value'    => []
    ],
    ......
    ]

    注意:共享內(nèi)存中儲(chǔ)存的futureId為最后一個(gè)監(jiān)聽(tīng)器創(chuàng)建的futureId;當(dāng)當(dāng)前進(jìn)程需要對(duì)監(jiān)聽(tīng)器進(jìn)行移除時(shí),請(qǐng)勿使用該數(shù)據(jù),對(duì)應(yīng)進(jìn)程內(nèi)可以通過(guò)Cache::ChCreateListener()的返回值獲取到當(dāng)前進(jìn)程創(chuàng)建的futureId用于移除監(jiān)聽(tīng)器,不使用共享內(nèi)存中儲(chǔ)存的futureId即可

  4. 這時(shí)任意進(jìn)程通過(guò)Cache::ChPublish('test', '這是一個(gè)測(cè)試消息', true);發(fā)送消息,或者指定workerIdCache::ChPublish('test', '這是一個(gè)測(cè)試消息', true, 'list');。

實(shí)現(xiàn)一個(gè)Pub/Sub

  1. A進(jìn)程使用workerman的workerId作為workerId:

    Cache::ChCreateListener('test', $worker->id, function(string $channelKey, string|int $workerId, mixed $message) {
    // TODO 你的業(yè)務(wù)邏輯
    });
  2. B進(jìn)程使用workerman的workerId作為workerId:

    Cache::ChCreateListener('test', $worker->id, function(string $channelKey, string|int $workerId, mixed $message) {
    // TODO 你的業(yè)務(wù)邏輯
    });
  3. 此時(shí)Channel test的數(shù)據(jù)可能如下:

    [
    1 => [
        'futureId' => 1,
        'value'    => []
    ],
    2 => [
        'futureId' => 1,
        'value'    => []
    ]
    ]
  4. 這時(shí),任意進(jìn)程通過(guò) Cache::ChPublish('test', '這是一個(gè)測(cè)試消息', false); 發(fā)送消息即可。

    注:發(fā)送消息第三個(gè)參數(shù)使用false時(shí),如發(fā)送時(shí)還未創(chuàng)建監(jiān)聽(tīng)器,消息則不會(huì)儲(chǔ)存至Channel,即監(jiān)聽(tīng)后才可存在消息

實(shí)現(xiàn)類(lèi)似Redis-stream

與Pub/Sub相同,只不過(guò)發(fā)布消息使用 Cache::ChPublish('test', '這是一個(gè)測(cè)試消息', true);, 當(dāng)發(fā)布消息指定workerId時(shí),可以實(shí)現(xiàn)類(lèi)似Redis-Stream Group的功能。

注:這里更復(fù)雜的功能可能需要對(duì)workerId進(jìn)行變通,不能簡(jiǎn)單使用workerman自帶的workerId,只需要自行規(guī)劃好即可

更多內(nèi)容可以查看源碼和文檔

如有需要新特性的支持,歡迎留言和提交issue

3361 11 12
11個(gè)評(píng)論

walkor

全是干貨的絕世好文

Tinywan

哇塞!全是干貨。趕緊用起來(lái)????????????。

Forsend

謝謝大佬,你的分享和文章都很好

Mr_Deng

怎么才能成長(zhǎng)為大神

  • Tinywan 2023-11-21

    向作者和群主看齊

  • chaz6chez 2023-11-21

    其實(shí)就是把所學(xué)知識(shí)盡可能運(yùn)用在工作生活中,并且在工作中盡可能發(fā)現(xiàn)一些可能性,就可以慢慢成長(zhǎng)為大神,溫故而知新即可

  • Tinywan 2023-11-21

    干自己感想干的,真的很好...

  • chaz6chez 2023-11-21

    總有機(jī)會(huì)嘛,萬(wàn)總,有機(jī)會(huì)就去嘗試,沒(méi)機(jī)會(huì)就鞏固唄,哈哈

shanjian

又可以學(xué)新知識(shí)了

army

之前和樓主聊過(guò)鎖的問(wèn)題,原因是樓主對(duì)單key存map,需要對(duì)該map讀改寫(xiě),這樣就導(dǎo)致了并發(fā)安全問(wèn)題,其實(shí)可以把map的key給個(gè)前綴當(dāng)做apcu的單key來(lái)寫(xiě)入就解決并發(fā)安全問(wèn)題了,要獲取這個(gè)“map”就遍歷這個(gè)前綴的key,這樣可以做到并發(fā)安全且性能翻倍。
如果實(shí)在要對(duì)單key存map,可以考慮yac,yac底層不帶鎖性能比apcu還快,配合你的業(yè)務(wù)鎖比apcu更優(yōu)。

  • army 2023-11-25

    apcu底層原理是根據(jù)key分割多個(gè)切片,每個(gè)切片里放置了一個(gè)map,每個(gè)map一個(gè)寫(xiě)鎖來(lái)達(dá)到并發(fā)安全和超高性能讀寫(xiě), 如果在map里再存入map我認(rèn)為這種用法是不合理的,失去了高性能讀寫(xiě)的含義。

  • chaz6chez 2023-11-25

    多key的map存在hit miss,帶來(lái)不完整性,所以要處理的工作也會(huì)很多

  • chaz6chez 2023-11-25

    map本來(lái)就可以嵌套,形成陣列,這個(gè)在計(jì)算的時(shí)候也會(huì)遇到,類(lèi)似python的numpy,我們?cè)趐hp中使用了實(shí)驗(yàn)性質(zhì)的numphp組件處理陣列數(shù)據(jù)計(jì)算

JackDx

關(guān)注

  • 暫無(wú)評(píng)論
windss

使用通道時(shí)一直報(bào)錯(cuò)

監(jiān)聽(tīng)
Cache::ChCreateListener('channel', $worker->id, function (string $key, int|string $workerId, mixed $message) {
var_dump($key);
var_dump($workerId);
var_dump($message);
});

發(fā)布
Cache::ChPublish('channel',$msg,false,0);

錯(cuò)誤
TypeError: call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given in /Users/winds/Project/php/new/api/vendor/workbunny/webman-shared-cache/src/Traits/ChannelMethods.php:163

  • chaz6chez 2024-05-17

    更新到最新版0.4.5試試

  • windss 2024-05-17

    已經(jīng)更新了, Cache::ChPublish($channel,$msg,false,2);指定workerId時(shí),相同$channel 都能收到消息,是不是我寫(xiě)代碼有問(wèn)題

  • chaz6chez 2024-05-17

    你是需要指定通道進(jìn)行publish對(duì)吧,可以提一個(gè)issue,附上測(cè)試樣例,我會(huì)在今天內(nèi)修復(fù)這個(gè)問(wèn)題

  • chaz6chez 2024-05-17

    可以嘗試使用0.4.6,我已修復(fù)指定workerId進(jìn)行publish沒(méi)有按預(yù)期執(zhí)行的bug

  • windss 2024-05-17

    ok,試一下 我在反饋

ab0029

干貨好文

  • 暫無(wú)評(píng)論
muvtou

學(xué)習(xí)了

  • 暫無(wú)評(píng)論
pengzhen

沒(méi)明白這個(gè)插件的使用場(chǎng)景,是指單機(jī)情況下跨進(jìn)程緩存嗎?

  • chaz6chez 2024-10-16

    這個(gè)插件的使用場(chǎng)景在單機(jī)環(huán)境下進(jìn)程通訊多進(jìn)程協(xié)同處理且延遲敏感的服務(wù),因?yàn)楦咚倬彺孀叩氖枪蚕韮?nèi)存,所以沒(méi)有像socket一樣的內(nèi)核用戶態(tài)的拷貝,會(huì)比redis或者自行通過(guò)socket實(shí)現(xiàn)的進(jìn)程通訊快一個(gè)數(shù)量級(jí),在一些游戲場(chǎng)景時(shí)需要進(jìn)行計(jì)算和配置共享,這時(shí)候就可以用這個(gè)插件來(lái)進(jìn)行處理;
    在http服務(wù)下,我們也可以適當(dāng)用這個(gè)插件做一些防抖和緩存處理,因?yàn)槭峭耆咴趦?nèi)存的,所以穩(wěn)定性會(huì)更強(qiáng)一些,不擔(dān)心和懼怕內(nèi)部因?yàn)榫W(wǎng)絡(luò)波動(dòng)的雪崩反應(yīng)

chaz6chez

5174
積分
0
獲贊數(shù)
0
粉絲數(shù)
2018-11-16 加入
??