Belajar Swoole - Table, Atomic & Lock (Shared Memory)
Episode 12 of 26

Belajar Swoole - Table, Atomic & Lock (Shared Memory)

Berbagi state antar worker proses dengan aman: Swoole\Table sebagai key-value store in-memory berperforma tinggi, Swoole\Atomic untuk counter atomik, dan Swoole\Lock untuk sinkronisasi, plus praktik membangun rate limiter dan online user counter.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah di episode 11 kita memindahkan pekerjaan berat ke task worker — pada episode ini kita menghadapi salah satu batasan terbesar Swoole yang sudah kita sebut sejak episode 2: memory terisolasi per worker. Variabel PHP di worker A tidak terlihat di worker B. Untuk aplikasi nyata — menghitung user online, rate limiting, caching kecil — kita butuh memory yang dibagi.

Mengapa episode ini penting? Karena inilah perbedaan antara aplikasi Swoole yang "berjalan" dan yang "berjalan dengan benar". Counter yang hidup di variabel biasa akan salah total begitu memakai 4 worker. Swoole menyediakan tiga senjata untuk ini: Swoole\Table, Swoole\Atomic, dan Swoole\Lock.

Masalah: State Per-Worker

Counter yang salah dengan banyak worker
$counter = 0;
$server->on('Request', function ($req, $res) use (&$counter) {
    $counter++; // hanya bertambah di worker ini!
    $res->end("Count: $counter");
});

Dengan 4 worker, angka yang tampil acak bergantung worker mana yang melayani request. Ini bukan bug kode — ini konsekuensi arsitektur multi-process.

Swoole\Table: Key-Value di Shared Memory

Swoole\Table adalah hash table yang tinggal di shared memory — semua worker membaca/menulis data yang sama. Ia didesain untuk performa ekstrem: operasi seperti incr bersifat atomik dan jauh lebih cepat daripada Redis (tanpa jaringan).

Definisi Table
use Swoole\Table;
 
$users = new Table(65536);               // 65536 baris maksimum
$users->column('fd', Table::TYPE_INT);
$users->column('name', Table::TYPE_STRING, 64);
$users->column('login_at', Table::TYPE_INT);
$users->create();                         // wajib: alokasikan memory

Tiga aturan penting Table:

  1. Definisikan column sebelum create() — setelah create(), skema tidak bisa diubah.
  2. create() harus dipanggil sebelum $server->start() — memory dialokasikan saat itu juga.
  3. Jumlah baris tetap — tetapkan di konstruktor; melebihi batas akan error/gagal insert.

Operasi dasar:

Operasi Table
$users->set(42, ['fd' => 5, 'name' => 'Andi', 'login_at' => time()]);
$row = $users->get(42);
$users->incr(42, 'login_at');      // atomik
$users->decr(42, 'login_at');
$users->del(42);
$users->exists(42);

Online User Counter

Pola on('Open')/on('Close') WebSocket yang kita bangun di episode 6 bisa dijadikan global dengan Table:

Online users dengan Table
<?php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\Table;
 
$online = new Table(65536);
$online->column('fd', Table::TYPE_INT);
$online->column('name', Table::TYPE_STRING, 64);
$online->create();
 
$server = new Server('0.0.0.0', 9502);
 
$server->on('Open', function (Server $server, Request $req) use ($online) {
    $online->set($req->fd, ['fd' => $req->fd, 'name' => "user-{$req->fd}"]);
    $server->push($req->fd, "Online: " . count($online));
});
 
$server->on('Message', function (Server $server, Frame $frame) use ($online) {
    foreach ($online as $row) {
        if ($row['fd'] !== $frame->fd) {
            $server->push($row['fd'], "{$frame->fd}: {$frame->data}");
        }
    }
});
 
$server->on('Close', function (Server $server, int $fd) use ($online) {
    $online->del($fd);
});
 
$server->start();

count($online) memberi jumlah user online di seluruh worker secara konsisten — hal mustahil dengan variabel biasa. Iterasi foreach ($online as $row) membaca semua baris dari shared memory.

Swoole\Atomic: Counter Atomik

Untuk kebutuhan sesederhana angka, Swoole\Atomic jauh lebih ringan daripada Table:

Atomic counter
use Swoole\Atomic;
 
$hits = new Atomic(0); // mulai dari 0
 
$server->on('Request', function ($req, $res) use ($hits) {
    $hits->add(1);
    $res->end("Total hits seluruh worker: " . $hits->get());
});

add, sub, get, set, cmpset (compare-and-swap) semuanya atomik — aman dipakai banyak worker bersamaan tanpa race condition. Sempurna untuk metrik sederhana, sequence ID, dan rate limiter.

Swoole\Lock: Sinkronisasi Kritikal

Ada kalanya operasi kompleks (bukan sekadar counter) butuh akses eksklusif. Swoole\Lock menyediakan mutex di shared memory:

Lock untuk operasi eksklusif
use Swoole\Lock;
 
$lock = new Lock(SWOOLE_MUTEX);
 
$server->on('Request', function ($req, $res) use ($lock) {
    $lock->lock();
    try {
        // baca + tulis struktur kompleks secara eksklusif
        $result = $heavy->process($req);
    } finally {
        $lock->unlock();
    }
    $res->end($result);
});

Warning

Jangan pernah menahan lock sambil menunggu I/O (query DB, HTTP call). Worker yang memegang lock akan membuat worker lain antre di situ — dan bila lock dipegang dalam coroutine yang tertunda, bisa terjadi deadlock. Pola aman: lock hanya untuk kritikal section yang cepat dan murni in-memory.

Praktik: Rate Limiter per IP

Kombinasi Table + Atomic memberi kita rate limiter yang berjalan di seluruh worker tanpa Redis:

Rate limiter per IP
<?php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Table;
 
$limit = new Table(65536);
$limit->column('count', Table::TYPE_INT);
$limit->column('reset_at', Table::TYPE_INT);
$limit->create();
 
$window = 60;   // 60 detik
$max = 100;     // maksimal 100 request
 
$server = new Server('0.0.0.0', 9501);
 
$server->on('Request', function (Request $req, Response $res) use ($limit, $window, $max) {
    $ip = $req->server['remote_addr'] ?? 'unknown';
    $now = time();
 
    $row = $limit->get($ip);
    if ($row === false || $row['reset_at'] < $now) {
        $limit->set($ip, ['count' => 1, 'reset_at' => $now + $window]);
        $remaining = $max - 1;
    } else {
        $count = $limit->incr($ip, 'count') ?: 1;
        $remaining = max(0, $max - $count);
    }
 
    if ($remaining === 0) {
        $res->status(429);
        $res->end('Too Many Requests');
        return;
    }
 
    $res->header('X-RateLimit-Remaining', (string) $remaining);
    $res->end('OK');
});
 
$server->start();

Rate limiter ini bekerja di semua worker, konsisten, dan tidak menambah beban jaringan karena murni in-memory.

Common Pitfalls

MasalahPenyebabSolusi
unable to allocate memory for tableUkuran Table terlalu besar vs memoryTurunkan jumlah baris / ukuran string
Data lenyap saat server restartTable hidup di memory, bukan diskBackup/restore, atau pakai Redis untuk persisten
Error saat set setelah createSkema diubah setelah alokasiDefinisikan column sekali sebelum create()
DeadlockLock ditahan sambil I/OPersingkat kritikal section, pindahkan I/O keluar lock

Penutup

Pada episode 12 ini, kalian telah berbagi state antar worker dengan aman.

Inti yang harus dibawa pulang:

  • Swoole\Table: key-value di shared memory, operasi incr atomik, baris tetap.
  • Definisikan column → create() sebelum server->start().
  • Swoole\Atomic: counter atomik ringan untuk metrik/sequence/rate limit.
  • Swoole\Lock: mutex untuk kritikal section — jangan ditahan saat I/O.
  • Rate limiter per IP dan online counter kini global di seluruh worker.

Di episode 13 selanjutnya, kita memanajemen koneksi database: coroutine MySQL & connection pools — PDOPool, Redis pool, dan query di bawah concurrency tinggi. Sampai jumpa di episode 13!

Belajar Swoole - Table, Atomic & Lock (Shared Memory) | Belajar Swoole