Building a High‑Performance Shared Cache for PHP Game Servers with Webman, Redis, and APCu

This article explains how to design and implement a fast, shared‑memory cache for a PHP‑based strategy game backend using Webman, Redis, MySQL, and APCu, covering service architecture, Redis usage, atomic operations with APCu locks, channel and list mechanisms, and practical code examples for multi‑process communication.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Building a High‑Performance Shared Cache for PHP Game Servers with Webman, Redis, and APCu

In a recent strategy‑game project the backend was originally planned with the Skynet framework, but due to tight deadlines the team switched to rapid PHP development using the Webman framework. The system is divided into three main services: a configuration service, an HTTP‑API service, and a WebSocket service.

Redis

Redis is heavily used in game development for sharing cached data among server instances. Each region (A/B) may contain several server instances (a, b, c) that need to share the same configuration. Because the data is highly dynamic and required with strong consistency, a traditional configuration center like Nacos was not used; instead Redis serves as the cache layer.

┌─────┐                                       ┌─────┐
     |  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

The diagram shows how requests from regions A and B are routed to service instances, which then access Redis and finally the processing layer. Redis also supports authentication, Redis‑Stream as a message queue, and other auxiliary functions.

Shared Memory

Game logic often runs in memory, and the multi‑process architecture makes inter‑process communication (IPC) a frequent bottleneck. Initially the team fixed certain tasks to specific processes to avoid IPC, but as the business grew this approach became unsustainable.

Webman’s channel plugin was considered, but it relies on sockets and incurs kernel‑user copying and network latency. To achieve zero‑copy data exchange, the team turned to shared memory, which eliminates deep copies and network overhead, delivering microsecond‑level response times.

webman‑shared‑cache

A timer periodically reads configuration data from MySQL, writes it to Redis, and then triggers an event that updates the shared memory. Upper‑level business code listens for this event and pulls the latest Redis data into APCu‑based shared memory, making it instantly available to all processes within the same region.

The plugin implements Redis‑like hash commands for map‑type data: HSet / HGet / HDel / HKeys / HExists It also provides atomic increment/decrement operations that support floating‑point numbers: HIncr / HDecr Because APCu stores objects, complex data structures can be cached directly.

Why a Lock?

APCu (Alternative PHP Cache User Cache) is an open‑source PHP extension that provides fast user‑data caching. Although APCu’s own functions are atomic, when used in a multi‑process environment multiple APCu calls must be grouped into a single atomic unit, which requires an explicit lock—similar to a MySQL transaction.

The following method implements a hash‑key increment with a lock to guarantee atomicity:

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;
}

The core of the lock mechanism is the Atomic helper:

protected static function _Atomic(string $lockKey, Closure $handler, bool $blocking = false): bool {
    $func = __FUNCTION__;
    $result = false;
    if ($blocking) {
        $startTime = time();
        while ($blocking) {
            // block‑insurance
            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;
}

When blocking mode is used, a while‑loop with a fuse timeout ( self::$fuse) prevents the process from being stuck indefinitely.

Important Note

Do not pass anonymous functions as arguments to the callback of Atomic, because APCu serialises parameters and anonymous functions cannot be serialised. Instead, store the closure in a class property or a static variable and invoke it inside the callback.

Version 0.4.x Enhancements

In addition to the shared‑memory cache, the author built a lightweight Raft‑based scheduler and a service‑registry plugin. The new Channel feature mimics Redis‑List, Redis‑Stream, and Redis‑Pub/Sub semantics.

Channel Data Structure

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

All keys in shared memory are prefixed with #Channel# . The default space ( --default--) and sub‑channels (e.g., workerId_1) are mutually exclusive; when a sub‑channel exists the default space disappears.

Implementing a List Listener

Listeners are created per worker ID. Example for process A:

Cache::ChCreateListener('test', 'list', function(string $channelKey, string|int $workerId, mixed $message) {
    // your business logic
});

Process B creates the same listener with its own worker ID. The channel stores a futureId that represents the last listener created; to remove a listener you should use the return value of Cache::ChCreateListener() rather than the shared futureId.

Publishing a message:

Cache::ChPublish('test', 'this is a test message', true); // broadcast
Cache::ChPublish('test', 'this is a test message', true, 'list'); // to specific list

Pub/Sub Example

Using the worker ID as the channel key enables a Pub/Sub pattern:

Cache::ChCreateListener('test', $worker->id, function(string $channelKey, string|int $workerId, mixed $message) {
    // handle message
});

Cache::ChPublish('test', 'broadcast message', false);

When the third argument is false, the message is not stored if no listener exists yet.

Redis‑Stream‑like Usage

Setting the third argument to true makes the publish behave like a Redis‑Stream group, allowing messages to be consumed by specific workers.

Note: Complex stream features may require custom worker‑ID handling beyond the default Workerman IDs.

Overall, the article provides a complete guide to building a high‑performance, shared‑memory cache for PHP game backends, leveraging Redis for distributed caching, APCu for intra‑process sharing, and Webman’s channel plugin for flexible message passing.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

redisPHPshared memoryWebman
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.