# 前言
今年接觸了一個策略類手游相關的項目,后端本身計劃是使用skynet進行開發的,后來結合項目的時間緊急程度和客戶端開發組討論后決定使用PHP進行快速開發,后期再使用其他語言框架進行拆分業務;綜合考慮最后選用了webman作為主要開發框架。
整體項目分為配置服務、http-api服務、websocket服務三大部分,其中配置管理主要是兼容客戶端生成的配置數據進行導入導出轉換加載,底層使用MySQL進行儲存,多服務間使用Redis進行一級緩存,服務進程間使用了基于APCu的共享緩存,后期我將該共享緩存組件化也貢獻給了社區。
[https://www.workerman.net/plugin/133](https://www.workerman.net/plugin/133)
# Redis
在游戲開發界實際上使用Redis的情況還是比較多的,我們使用Redis主要還是為了將一些數據緩存共享給各個服務器實例:
~~~
┌─────┐ ┌─────┐
| 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為區服,每個區服下可能存在abc不同的服務器實例,他們需要共享相同的區服配置;每個區服各自管理自己的數據庫數據區域/數據庫實例;每個區服下的服務器實例對于數據庫數據的要求是強需求,且為變動較為頻繁的數據內容,與web的微服務有區別,所以我們沒有使用類似Nacos或者其他配置中心進行處理,從而用更適配當前場景的Redis作為緩存服務。
同時Redis也可以作為用戶登錄鑒權相關中的一環,也可以為運營相關功能提供一些輔助,比如使用Redis-Stream作為消息隊列,處理一些事件通知等。
# 共享內存
在游戲開發中,許多業務都是在內存中進行的計算處理,而我們上述的模式是多進程模式,進程間通訊是一個比較頻繁出現的點;一開始解決這個問題是粗暴的將一些固定業務固定在對應的進程上執行,盡可能避免進程間的通訊問題,后來隨著業務逐步的擴大,單純限制業務是沒辦法完全實現的,這時候有考慮過使用webman的channel;但實際上channel基于socket涉及系統內核態用戶態的拷貝等問題,同時受網絡影響受限,在一些業務的計算處理上會帶來比較高的延遲,包括Redis也同樣是這樣的問題,我們需要實現數據的零拷貝。
后續我們的目標鎖定在了共享內存上,因為共享內存可以輕易的在進程間進行通訊交換,而且不存在深拷貝和網絡等問題,效率、性能非常的高,整體微秒級別的響應滿足我們的需求;于是我基于PHP的拓展APCu封裝了適合我們業務場景的插件包進行使用。
# webman-shared-cache
我們的基礎應用實現了定時器來從MySQL數據庫讀取配置信息,定時器的處理器也在讀取數據刷入Redis的同時觸發共享內存的更新事件,上層業務通過更新事件的回調出發會將Redis的數據刷入共享內存中,以便當前區服實例的各個進程能夠使用。
我們使用緩存的場景很多都是MAP數據,所以我在實現插件的時候特別實現了類似Redis-Hash相關的功能:
* HSet/HGet/HDel/HKeys/HExists
由于我們需要一些自增自減的運算,所以也實現了以下功能點:
* HIncr/HDecr,支持浮點運算
由于APCu的特性所以儲存的數據也是支持儲存對象數據的;
# webman-shared-cache為何使用鎖?
之前我有和社區的同學們聊過,他們不是很理解為什么我在實現插件的時候自己使用了鎖,這是因為APCu本身的自行實現了對它自身函數的原子性操作,但我們使用它的時候是在多進程的環境下,每一個進程內存在多次APCu的操作,為了業務的原子性,我們希望這多次的操作要在一個原子性內完成,所以需要一個鎖來進行隔離,以免在多進程的環境下被其他進程的操作污染,整體是類似MySQl的事務的:
~~~php
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;
}
~~~
比如上述代碼,就是一個Hash key的自增操作,我們需要在讀取Hash后在寫入,讀取和寫入應為一體的;
原子性執行函數Atomic的實現如下:
~~~php
/**
* 原子操作
* - 無法對鎖本身進行原子性操作
* - 只保證handler是否被原子性觸發,對其邏輯是否拋出異常不負責
* - handler盡可能避免超長阻塞
* - lockKey會被自動設置特殊前綴#lock#,可以通過Cache::LockInfo進行查詢
*
* @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) {
// 阻塞保險
if (time() >= $startTime + self::$fuse) {return false;}
// 創建鎖
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 {
// 創建鎖
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;
}
~~~
當使用阻塞模式的時候,我們會在當前進程內使用一個while循環來進行阻塞搶占,為了不將當前進程阻塞死,我們還加入了一個保險,由`self::$fuse`提供;
## 注意
這里在實踐過程中需要注意的是,Atomic在傳入回調函數時切勿再使用匿名函數作為參數值或者是通過use傳入一個匿名函數,如:
~~~
$fuc = function() {
// do something
}
Cache::Atomic('test', function () use ($fuc) {
// do anything
})
~~~
APCu底層會對函數參數值或引用參數進行序列化儲存,但匿名函數不可以被序列化,所以會拋出一個異常;但你可以通過當前對象的屬性值或者靜態屬性來保存一個匿名函數,然后在Atomic的回調內調用使用。
# 0.4.x版本
由于目前我使用Webman基于SQLite和共享內存在自行實現一個具備RAFT的輕調度服務插件和服務注冊與發現插件,所以特此為其完善增加了Channel特性;
Channel可以輔助實現類似Redis-List、Redis-stream、Redis-Pub/Sub的功能。
## Channel
Channel是個特殊的數據格式,他的格式是固定如下的:
~~~
[
'--default--' => [
'futureId' => null,
'value' => []
],
workerId_1 => [
'futureId' => 1,
'value' => []
],
workerId_2 => [
'futureId' => 1,
'value' => []
],
......
]
~~~
它在共享內存中的鍵默認以**#Channel#**開頭。
* **\--default--**是默認儲存空間,**workerId\_1/workerId\_2**等是子通道儲存空間,命名是由用戶代碼傳入的,這里**建議使用workerman自帶的workerId**即可。
* 默認儲存空間和子通道儲存空間是互斥的,也就是說當存在子通道儲存空間時,是不存在--default--的,反之亦然;子通道儲存空間是當當前通道存在監聽器時生成的,而在監聽器產生前,消息會暫存在--default--空間,當監聽器創建時,--default--的數據value會被同步到子通道儲存空間內,**加入value的隊頭**。
* 每一個子通道儲存空間的value都是拷貝的,存在相同的數據,各自監聽器監聽各自的子通道儲存空間;消息的發布支持向所有子通道發布,也可以指定子通道進行發布。
* 監聽器的底層使用了workerman的定時器,區別與workerman的timer,在event驅動下定時器的間隔是0,也就是一個future,而其他的事件驅動是0.001s為間隔。
## 實現一個List
由于監聽器創建消費是基于workerId的,我們可以通過不同進程創建相同的workerId的監聽器來對同一個子通道進行監聽:
1. A進程使用list作為workerId:
~~~
Cache::ChCreateListener('test', 'list', function(string $channelKey, string|int $workerId, mixed $message) {
// TODO 你的業務邏輯
});
~~~
2. B進程也同樣創建list的workerId監聽器:
~~~
Cache::ChCreateListener('test', 'list', function(string $channelKey, string|int $workerId, mixed $message) {
// TODO 你的業務邏輯
});
~~~
3. 此時Channel test的數據如下:
~~~
[
'list' => [
'futureId' => 1,
'value' => []
],
......
]
~~~
**注意:共享內存中儲存的futureId為最后一個監聽器創建的futureId;當當前進程需要對監聽器進行移除時,請勿使用該數據,對應進程內可以通過`Cache::ChCreateListener()`的返回值獲取到當前進程創建的futureId用于移除監聽器,不使用共享內存中儲存的futureId即可**
4. 這時任意進程通過`Cache::ChPublish('test', '這是一個測試消息', true);`發送消息,或者指定workerId`Cache::ChPublish('test', '這是一個測試消息', true, 'list');`。
## 實現一個Pub/Sub
1. A進程使用workerman的workerId作為workerId:
~~~
Cache::ChCreateListener('test', $worker->id, function(string $channelKey, string|int $workerId, mixed $message) {
// TODO 你的業務邏輯
});
~~~
2. B進程使用workerman的workerId作為workerId:
~~~
Cache::ChCreateListener('test', $worker->id, function(string $channelKey, string|int $workerId, mixed $message) {
// TODO 你的業務邏輯
});
~~~
3. 此時Channel test的數據可能如下:
~~~
[
1 => [
'futureId' => 1,
'value' => []
],
2 => [
'futureId' => 1,
'value' => []
]
]
~~~
4. 這時,任意進程通過`Cache::ChPublish('test', '這是一個測試消息', false);`發送消息即可。
**注:發送消息第三個參數使用false時,如發送時還未創建監聽器,消息則不會儲存至Channel,即監聽后才可存在消息**
## 實現類似Redis-stream
與Pub/Sub相同,只不過發布消息使用`Cache::ChPublish('test', '這是一個測試消息', true);`, 當發布消息指定workerId時,可以實現類似Redis-Stream Group的功能。
**注:這里更復雜的功能可能需要對workerId進行變通,不能簡單使用workerman自帶的workerId,只需要自行規劃好即可**
- 設計模式系列
- 工廠方法模式
- 序言
- Windows程序注冊為服務的工具WinSW
- 基礎
- 安裝
- 開發規范
- 目錄結構
- 配置
- 快速入門
- 架構
- 請求流程
- 架構總覽
- URL訪問
- 容器和依賴注入
- 中間件
- 事件
- 代碼層結構
- 四個層次
- 路由
- 控制器
- 請求
- 響應
- 數據庫
- MySQL實時同步數據到ES解決方案
- 阿里云DTS數據MySQL同步至Elasticsearch實戰
- PHP中的MySQL連接池
- PHP異步非阻塞MySQL客戶端連接池
- 模型
- 視圖
- 注解
- @SpringBootApplication(exclude={DataSourceAutoConfiguration.calss})
- @EnableFeignClients(basePackages = "com.wotu.feign")
- @EnableAspectJAutoProxy
- @EnableDiscoveryClient
- 錯誤和日志
- 異常處理
- 日志處理
- 調試
- 驗證
- 驗證器
- 驗證規則
- 擴展庫
- 附錄
- Spring框架知識體系詳解
- Maven
- Maven和Composer
- 構建Maven項目
- 實操課程
- 01.初識SpringBoot
- 第1章 Java Web發展史與學習Java的方法
- 第2章 環境與常見問題踩坑
- 第3章 springboot的路由與控制器
- 02.Java編程思想深度理論知識
- 第1章 Java編程思想總體
- 第2章 英雄聯盟的小案例理解Java中最為抽象的概念
- 第3章 徹底理解IOC、DI與DIP
- 03.Spring與SpringBoot理論篇
- 第1章 Spring與SpringBoot導學
- 第2章 Spring IOC的核心機制:實例化與注入
- 第3章 SpringBoot基本配置原理
- 04.SprinBoot的條件注解與配置
- 第1章 conditonal 條件注解
- 第2章 SpringBoot自動裝配解析
- 05.Java異常深度剖析
- 第1章 Java異常分類剖析與自定義異常
- 第2章 自動配置Url前綴
- 06.參數校驗機制與LomBok工具集的使用
- 第1章 LomBok工具集的使用
- 第2章 參數校驗機制以及自定義校驗
- 07.項目分層設計與JPA技術
- 第1章 項目分層原則與層與層的松耦合原則
- 第2章 數據庫設計、實體關系與查詢方案探討
- 第3章 JPA的關聯關系與規則查詢
- 08.ORM的概念與思維
- 第1章 ORM的概念與思維
- 第2章 Banner等相關業務
- 第3章 再談數據庫設計技巧與VO層對象的技巧
- 09.JPA的多種查詢規則
- 第1章 DozerBeanMapper的使用
- 第2章 詳解SKU的規格設計
- 第3章 通用泛型Converter
- 10.令牌與權限
- 第1章 通用泛型類與java泛型的思考
- 常見問題
- 微服務
- demo
- PHP中Self、Static和parent的區別
- Swoole-Cli
- 為什么要使用現代化PHP框架?
- 公眾號
- 一鍵部署微信公眾號Markdown編輯器(支持適配和主題設計)
- Autodesigner 2.0發布
- Luya 一個現代化PHP開發框架
- PHPZip - 創建、讀取和管理 ZIP 文件的簡單庫
- 吊打Golang的PHP界天花板webman壓測對比
- 簡潔而強大的 YAML 解析庫
- 推薦一個革命性的PHP測試框架:Kahlan
- ServBay下一代Web開發環境
- 基于Websocket和Canvas實現多人協作實時共享白板
- Apipost預執行腳本如何調用外部PHP語言
- 認證和授權的安全令牌 Bearer Token
- Laradock PHP 的 Docker 完整本地開發環境
- 高效接口防抖策略,確保數據安全,避免重復提交的終極解決方案!
- TIOBE 6月榜單:PHP穩步前行,編程語言生態的微妙變化
- Aho-Corasick字符串匹配算法的實現
- Redis鍵空間通知 Keyspace Notification 事件訂閱
- ServBay如何啟用并運行Webman項目
- 使用mpdf實現導出pdf文件功能
- Medoo 輕量級PHP數據庫框架
- 在PHP中編寫和運行單元測試
- 9 PHP運行時基準性能測試
- QR碼生成器在PHP中的源代碼
- 使用Gogs極易搭建的自助Git服務
- Gitea
- webman如何記錄SQL到日志?
- Sentry PHP: 實時監測并處理PHP應用程序中的錯誤
- Swoole v6 Alpha 版本已發布
- Proxypin
- Rust實現的Redis內存數據庫發布
- PHP 8.4.0 Alpha 1 測試版本發布
- 121
- Golang + Vue 開發的開源輕量 Linux 服務器運維管理面板
- 內網穿透 FRP VS Tailscale
- 新一代開源代碼托管平臺Gitea
- 微服務系列
- Nacos云原生配置中心介紹與使用
- 輕量級的開源高性能事件庫libevent
- 國密算法
- 國密算法(商用密碼)
- GmSSL 支持國密SM2/SM3/SM4/SM9/SSL 密碼工具箱
- GmSSL PHP 使用
- 數據庫
- SQLite數據庫的Web管理工具
- 阿里巴巴MySQL數據庫強制規范
- PHP
- PHP安全測試秘密武器 PHPGGC
- 使用declare(strict_types=1)來獲得更健壯的PHP代碼
- PHP中的魔術常量
- OSS 直傳阿里騰訊示例
- PHP源碼編譯安裝APCu擴展實現數據緩存
- BI性能DuckDB數據管理系統
- 為什么別人可以是架構師!而我卻不是?
- 密碼還在用 MD5 加鹽?不如試試 password_hash
- Elasticsearch 在電商領域的應用與實踐
- Cron 定時任務入門
- 如何動態設置定時任務!而不是寫死在Linux Crontab
- Elasticsearch的四種查詢方式,你知道多少?
- Meilisearch vs Elasticsearch
- OpenSearch vs Elasticsearch
- Emlog 輕量級開源博客及建站系統
- 現代化PHP原生協程引擎 PRipple
- 使用Zephir編寫C擴展將PHP源代碼編譯加密
- 如何將PHP源代碼編譯加密,同時保證代碼能正常的運行
- 為什么選擇Zephir給PHP編寫動態擴展庫?
- 使用 PHP + XlsWriter實現百萬級數據導入導出
- Rust編寫PHP擴展
- 阿里云盤開放平臺對接進行文件同步
- 如何構建自己的PHP靜態可執行文件
- IM后端架構
- RESTful設計方法和規范
- PHP編譯器BPC 7.3 發布,成功編譯ThinkPHP8
- 高性能的配置管理擴展 Yaconf
- PHP實現雪花算法庫 Snowflake
- PHP官方現代化核心加密庫Sodium
- pie
- 現代化、精簡、非阻塞PHP標準庫PSL
- PHP泛型和集合
- 手把手教你正確使用 Composer包管理
- JWT雙令牌認證實現無感Token自動續期
- 最先進PHP大模型深度學習庫TransformersPHP
- PHP如何啟用 FFI 擴展
- PHP超集語言PXP
- 低延遲雙向實時事件通信 Socket.IO
- PHP OOP中的繼承和多態
- 強大的現代PHP高級調試工具Kint
- PHP基金會
- 基于webman+vue3高質量中后臺框架SaiAdmin
- 開源免費的定時任務管理系統:Gocron
- 簡單強大OCR工具EasyOCR在PHP中使用
- PHP代碼抽象語法樹工具PHP AST Viewer
- MySQL數據庫管理工具PHPMyAdmin
- Rust編寫的一款高性能多人代碼編輯器Zed
- 超高性能PHP框架Workerman v5.0.0-beta.8 發布
- 高并發系列
- 入門介紹及安裝
- Lua腳本開發 Hello World
- 執行流程與階段詳解
- Nginx Lua API 接口開發
- Lua模塊開發
- OpenResty 高性能的正式原因
- 記一次查找 lua-resty-mysql 庫 insert_id 的 bug
- 包管理工具OPM和LuaRocks使用
- 異步非阻塞HTTP客戶端庫 lua-resty-http
- Nginx 內置綁定變量
- Redis協程網絡庫 lua-resty-redis
- 動態HTML渲染庫 lua-testy-template
- 單獨的
- StackBlitz在線開發環境
- AI
- 基礎概念
- 12312
- 基礎鏡像的坑
- 利用phpy實現 PHP 編寫 Vision Transformer (ViT) 模型
- 語義化版本 2.0.0