Redis
Di satu server, Daravel tidak membutuhkan Redis: isolate coordinator
menyimpan cache, lock, dan rate limit untuk semua worker isolate di memori,
dan queue database bersifat ACID. daravel_redis ditujukan untuk beberapa
server, saat state itu harus dibagi dan database menjadi bottleneck.
| Setup Anda | Pakai |
|---|---|
| Satu VPS atau satu container | Default-nya (CACHE_STORE=process, queue database). Jangan tambahkan Redis |
| Beberapa server, trafik kecil, sudah ada PostgreSQL atau MySQL | CACHE_STORE=database, queue database, REVERB_SCALING_ENABLED lewat database. Tetap tanpa Redis |
| Beberapa server dengan latensi ketat, rate limit yang ketat, banyak job atau WebSocket lintas server, atau aplikasi Laravel yang sudah memakai Redis | daravel_redis |
Paket ini bekerja dengan Redis 6.2 ke atas dan dengan Valkey, di satu server, di belakang Sentinel, maupun sebagai Redis Cluster, dan berbicara protokolnya sendiri (tanpa library native).
Instalasi#
dartisan redis:install --docker # atau: dart pub add daravel_redis
redis:install menambahkan paketnya, .withRedis() dan command redis:*,
key .env di .env.example, dan dengan --docker sebuah service Valkey di
file compose (--server=redis untuk Redis).
// bootstrap/app.dart
import 'package:daravel_redis/daravel_redis.dart';
Application createApp() => Application.configure(basePath: basePath)
.withRedis()
// ...
.create();
withRedis() mendaftarkan semua yang dijelaskan di bawah; tidak ada yang
terhubung sampai sesuatu memakai Redis, jadi tidak ada biayanya selama
.env tidak memintanya.
Memilihnya di .env#
REDIS_HOST=10.0.0.15
REDIS_PASSWORD=secret
# atau REDIS_URL=rediss://user:secret@redis.example.com:6380/0
CACHE_STORE=redis # cache, lock, denylist JWT
CACHE_LIMITER=redis # rate limit, masing-masing satu round trip
QUEUE_CONNECTION=redis # job diambil begitu di-push
REVERB_SCALING_ENABLED=true
REVERB_SCALING_DRIVER=redis # server broadcasting berbagi client Echo
| Pengaturan | Yang dikerjakan Redis |
|---|---|
CACHE_STORE=redis |
Cache dan Cache.lock di semua server. Logout (Auth.jwtGuard().invalidate()) juga sampai ke semua server, karena denylist JWT disimpan di store default |
CACHE_STORE=tiered |
Sama, dengan memori setiap isolate di depan Redis (di bawah) |
CACHE_LIMITER=redis |
Setiap percobaan adalah satu skrip Lua; percobaan yang diblokir tidak dihitung |
QUEUE_CONNECTION=redis |
Job di Redis; worker yang menganggur menunggu dengan BLPOP dan langsung menjalankan job yang di-push, tidak menunggu --sleep. Lock unique job serta queue:restart / queue:pause juga masuk ke Redis (QUEUE_UNIQUE_LOCK_STORE=redis) |
REVERB_SCALING_DRIVER=redis |
Frame antarserver lewat Pub/Sub; anggota presence, API channels, dan channel cache di Redis (Broadcasting) |
Cache memakai koneksi cache (REDIS_CACHE_DB, default 1), sehingga
dartisan cache:clear hanya menghapus key cache itu sendiri (SCAN dan
UNLINK pada prefix-nya, tidak pernah FLUSHDB) dan tidak pernah
menyentuh job yang sedang antre.
Konfigurasi#
| Variabel | Default | Kegunaan |
|---|---|---|
REDIS_URL |
tidak ada | redis:// atau rediss:// (TLS), user:password@host:port/database; redis+sentinel:// dan redis+cluster:// mendaftar beberapa host (di bawah). Menggantikan variabel di bawah |
REDIS_HOST, REDIS_PORT |
127.0.0.1, 6379 |
Server |
REDIS_USERNAME, REDIS_PASSWORD |
tidak ada | User ACL (Redis 6+) dan password |
REDIS_DB, REDIS_CACHE_DB |
0, 1 |
Database koneksi default dan cache, seperti di Laravel |
REDIS_PREFIX |
<app>-database- |
Ditaruh di depan setiap key, default Laravel 12 |
REDIS_CACHE_LOCK_CONNECTION |
default |
Tempat lock cache disimpan, seperti lock_connection dari Laravel |
REDIS_TLS |
false |
TLS tanpa rediss://; boot memberi peringatan bila host publik dijangkau tanpanya |
REDIS_COMMAND_TIMEOUT, REDIS_CONNECT_TIMEOUT |
1s, 2s |
Perintah yang timeout menutup koneksinya, yang dibuka lagi pada perintah berikutnya |
REDIS_MAX_IN_FLIGHT |
1024 |
Perintah yang menunggu balasan di satu koneksi; selebihnya langsung ditolak |
REDIS_MODE |
standalone |
sentinel atau cluster, bersama REDIS_NODES |
REDIS_NODES |
REDIS_HOST:REDIS_PORT |
host:port,host:port: para Sentinel, atau node cluster tempat memulai |
REDIS_SENTINEL_SERVICE |
mymaster |
Nama master di Sentinel |
REDIS_SENTINEL_USERNAME, REDIS_SENTINEL_PASSWORD |
tidak ada | Kredensial Sentinel itu sendiri |
REDIS_SERIALIZER |
json |
php menulis nilai cache seperti serialize() dari Laravel; keduanya selalu bisa dibaca |
REDIS_TIERED_MAX_KEYS, REDIS_TIERED_TTL |
10000, 60s |
Entri store tiered per isolate dan umur terpanjangnya |
QUEUE_REDIS_ENGINE |
list |
stream menyimpan job di stream dengan consumer group |
QUEUE_REDIS_NAMESPACE |
daravel |
Ruang key untuk queue, terpisah dari queues: dari Laravel |
/up/ready mendapat pengecekan redis (PING). Password tidak pernah
muncul di log maupun error.
Sentinel dan Cluster#
# Master yang disebut Sentinel, ditemukan lagi setelah failover
REDIS_URL=redis+sentinel://:secret@10.0.0.21:26379,10.0.0.22:26379/mymaster/0
# Redis Cluster, dari node mana pun
REDIS_URL=redis+cluster://:secret@10.0.0.31:6379,10.0.0.32:6379
Dengan Sentinel, setiap koneksi menanyakan master ke para Sentinel lalu
mengecek ROLE-nya. Setelah failover, master lama memutus client-nya, yang
kemudian menyambung ke master baru; penulisan yang ditolak master yang
sudah diturunkan (READONLY) tidak sempat berjalan dan dikirim ulang ke
master baru. Penulisan yang diterima master lama sesaat sebelum Sentinel
menurunkannya bisa hilang: jendela itu berasal dari replikasi asinkron
Redis.
Di cluster, setiap perintah dikirim ke master hash slot key-nya, berdasarkan
slot map yang dibaca dengan CLUSTER SLOTS dan dikoreksi oleh MOVED dan
ASK saat slot berpindah. MGET, DEL, UNLINK, EXISTS, dan TOUCH
atas key dari beberapa slot dipecah per slot; SCAN (dan dengan demikian
cache:clear) mendatangi setiap master. Semua yang dijalankan Daravel
sebagai satu skrip berbagi hash tag ({default} untuk sebuah queue),
sehingga tetap di satu slot; skrip Anda sendiri atas beberapa key juga
membutuhkannya ({user:7}:a, {user:7}:b), atau server menjawab
CROSSSLOT. Cluster hanya punya database 0, jadi REDIS_CACHE_DB tidak
berlaku di sana.
Cache di setiap isolate#
CACHE_STORE=tiered menyimpan apa yang dibaca setiap isolate di memorinya
sendiri (L1) di depan Redis (L2), dan Redis memberi tahu setiap server saat
salah satu key itu ditulis (CLIENT TRACKING dalam mode broadcast, RESP3).
Pembacaan dari L1 tidak pernah lebih lama dari penulisan terakhir yang
diberitahukan ke server itu; yang memisahkannya hanyalah perjalanan singkat
pemberitahuan tersebut.
final user = await Cache.store('tiered').remember('user:7', null, loadUser);
Nilainya disimpan di bawah prefix tersendiri (tiered:), sehingga hanya
nilai-nilai itu yang di-broadcast; lock dan rate limit tetap ke store
redis seperti biasa. Selama tracking belum berjalan, dan di server yang
tidak mendukungnya, setiap pembacaan langsung ke Redis. Sebuah entri hidup
paling lama REDIS_TIERED_TTL dan mengikuti time to live key-nya sendiri;
paling banyak REDIS_TIERED_MAX_KEYS yang disimpan, dan yang paling lama
tidak dibaca keluar lebih dulu.
Cache tags#
await Cache.tags(['people', 'artists']).put('John', john, ttl: const Duration(hours: 1));
await Cache.tags(['people']).flush();
Entri yang diberi tag disusun seperti Laravel 12 menyusunnya di Redis:
nilainya di <cache prefix><sha1 dari tag>:<key>, dan key setiap tag di
sorted set <cache prefix>tag:<name>:entries dengan skor waktu
kedaluwarsanya. Flush menghapus key yang didaftar tag-nya, sehingga
masing-masing framework bisa mem-flush apa yang diberi tag oleh yang lain.
Key yang sudah kedaluwarsa tetap ada di daftar sampai
dartisan cache:prune-stale-tags (padanan command Laravel dengan nama yang
sama, layak dijadwalkan) membuangnya. Store tiered juga bisa memberi tag,
dengan id tag seperti array store dari Laravel.
Engine queue#
QUEUE_REDIS_ENGINE=list (default) menyimpan job di list dan sorted set.
stream menyimpannya di Redis stream yang dibaca oleh consumer group:
server sendiri yang mencatat job yang sedang berjalan (XPENDING), dan job
yang worker-nya mati diambil worker lain setelah QUEUE_RETRY_AFTER
(XAUTOCLAIM, menurut jam server). Keduanya membangunkan worker yang
menganggur dengan cara yang sama, dan payload-nya tetap persis seperti yang
di-push. Ganti engine saat queue kosong: masing-masing menyimpan job di
key-nya sendiri.
Algoritma rate limit#
Fixed window dari Laravel bisa meloloskan sampai dua kali batas di sekitar
reset. Dengan CACHE_LIMITER=redis, dua algoritma lain masing-masing cukup
satu skrip, memakai jam server Redis:
limiter.define('login', (request) => Limit.perMinute(5).by(request.ip).gcra());
limiter.define('search', (request) => Limit.perMinute(60).slidingWindow());
| Algoritma | Yang dihitung |
|---|---|
fixedWindow (default) |
Cara Laravel: hit sejak hit pertama di jendela itu |
slidingWindow() |
Jendela saat ini ditambah jendela sebelumnya, ditimbang menurut seberapa banyak bagiannya masih dalam decay |
gcra() |
Satu percobaan setiap decay / batas, seluruh batas sebagai burst setelah periode sepi |
Retry-After menyebutkan kapan percobaan berikutnya akan lolos di
masing-masing algoritma.
Event yang bisa dilanjutkan#
router.get('/orders/events', (request) =>
Response.sse(Redis.events('orders').since(request)));
await Redis.events('orders').publish({'id': 7}, event: 'created');
Setiap event masuk ke sebuah stream (<REDIS_PREFIX>sse:orders, kira-kira
1000 terakhir disimpan) dan id stream-nya menjadi id: event tersebut.
Browser yang menyambung ulang mengirim Last-Event-ID; ia lebih dulu
menerima yang terlewat, lalu event live, tanpa ada yang terkirim dua kali
maupun hilang di antaranya. Semua listener sebuah channel dalam satu isolate
berbagi satu koneksi XREAD BLOCK.
Facade Redis#
await Redis.set('greeting', 'halo', ttl: const Duration(minutes: 5));
final greeting = await Redis.get('greeting');
await Redis.command(['HSET', 'user:7', 'name', 'Ana']);
final [name, ttl] = await Redis.connection().pipeline([
['HGET', 'user:7', 'name'],
['TTL', 'user:7'],
]);
final script = RedisScript("return redis.call('INCRBY', KEYS[1], ARGV[1])");
await Redis.eval(script, keys: ['visits'], args: [1]);
await Redis.publish('orders', '{"id":7}');
Redis.subscribe(['orders']).messages.listen((message) { /* ... */ });
publish(..., sharded: true) dan subscribe([...], sharded: true) memakai
Pub/Sub ber-shard (Redis 7): di cluster sebuah pesan tetap di dalam shard
channel-nya, dan subscription mengikuti shard itu saat slot-nya berpindah.
Channel ber-shard harus berbagi hash slot ({orders}:created,
{orders}:paid).
// Paling banyak 10 per menit, dihitung bersama Redis::throttle('reports') dari Laravel
await Redis.throttle('reports').allow(10).every(const Duration(minutes: 1))
.then(() => sendReport(), (error) => releaseLater());
// Paling banyak 3 sekaligus (Redis::funnel dari Laravel)
await Redis.funnel('imports').limit(3).block(const Duration(seconds: 5))
.then(() => runImport());
throttle dan funnel memakai skrip dan key dari Laravel (dengan
REDIS_PREFIX), sehingga kedua framework menghitung terhadap satu batas
yang sama. Seperti di Laravel, jendela throttle berjalan dari satu detik
bulat sampai detik itu ditambah periodenya. funnel di cluster membutuhkan
hash tag di namanya ({imports}), karena slot-slotnya adalah key
tersendiri.
Setiap isolate memegang satu koneksi per server, dan perintah yang dikirim tanpa saling menunggu berbagi round trip-nya (pipelining). Perintah blocking dan subscription mendapat koneksi sendiri; subscription akan subscribe ulang setelah koneksinya terputus.
Perintah#
// bootstrap/commands.dart
import 'package:daravel_redis/commands.dart';
final List<CommandFactory> commands = [...redisCommands];
Perintah-perintah ini berasal dari library tersendiri, sehingga server tidak ikut mengompilasi CLI.
| Perintah | Kegunaan |
|---|---|
redis:ping [--connection] |
Server, versinya, mode, dan round trip |
redis:stats [--connection] |
INFO setiap master (memori, client, hit) dan apa yang dikerjakan client (koneksi, redirect, failover) |
redis:keys-audit [--connection] [--match] [--limit] |
Key per tipe di bawah REDIS_PREFIX, dan key cache yang tidak pernah kedaluwarsa |
redis:cli [--connection] [--write] [command ...] |
Satu perintah, atau prompt; tanpa --write hanya perintah baca yang dijalankan (KEYS juga tidak: pakai SCAN) |
redis:inspect <key> [--connection] |
Isi sebuah key: tipe, time to live, encoding, ukuran, memori, dan item pertamanya (nilai disembunyikan di production). Juga tersedia sebagai tool MCP redis_inspect (dartisan) |
cache:prune-stale-tags [--store] |
Membuang key yang sudah kedaluwarsa dari daftar tag |
Jaminan#
- Pembacaan cache fail-open: saat Redis mati, pembacaan dianggap miss dan aplikasi menjawab dari sumber aslinya. Penulisan melempar exception.
- Lock fail-closed dan dimiliki pemiliknya, seperti
RedisLockdari Laravel. Ini lock satu instance, bukan Redlock: failover dengan replikasi asinkron bisa menghilangkannya. Untuk pekerjaan yang tidak boleh berjalan dua kali (penagihan), gunakan lock di database. - Job berjalan minimal sekali: job yang di-reserve worker yang mati akan
berjalan lagi setelah
QUEUE_RETRY_AFTER. Handler sebaiknya idempoten, seperti di Laravel. Payload job tetap persis seperti yang di-push. - Pub/Sub paling banyak sekali: event yang terbit saat sebuah server sedang menyambung ulang tidak sampai ke client di server itu.
Bersama Laravel di Redis yang sama#
| Lock | Dibagi bila keduanya memakai REDIS_PREFIX dan CACHE_PREFIX yang sama: key yang sama di koneksi yang sama, SET NX PX, dan skrip pelepasan yang sama. dartisan adopt memberi Daravel prefix cache sendiri, jadi samakan CACHE_PREFIX dengan prefix dari Laravel untuk berbagi lock |
| Angka di cache | Dibagi: disimpan sebagai digit biasa |
| Nilai cache lainnya | Dibagi dengan REDIS_SERIALIZER=php: Daravel menulis serialize() dari PHP untuk string, boolean, list, dan map, serta membaca nilai dari Laravel, dengan object menjadi map berisi property-nya. Dengan json, Laravel tidak bisa membaca nilai dari Daravel |
| Tag | Dibagi: key dan daftar tag yang sama; flush dari salah satunya sampai ke apa yang diberi tag oleh yang lain |
Redis.throttle, Redis.funnel |
Dibagi dengan Redis::throttle dan Redis::funnel dengan nama yang sama: skrip dan key dari Laravel |
| Queue | Dipisah (namespace daravel:): worker Laravel tidak bisa menjalankan job Daravel, begitu pula sebaliknya |
bench/interop/redis.sh mengecek semua ini terhadap Laravel 12 dengan
predis: lock yang diambil salah satunya ditolak bagi yang lain dengan
pemilik yang sama, angka di cache bertambah dari kedua sisi, masing-masing
membaca array dan entri ber-tag dari sisi lainnya serta mem-flush tag milik
sisi lain, dan satu throttle menghitung keduanya.
Belum tersedia#
REDIS_TOPOLOGY=coordinator, yang akan menyalurkan perintah semua isolate
lewat satu set koneksi per server untuk layanan terkelola dengan batas
koneksi rendah, sudah direncanakan. Sampai saat itu setiap isolate membuka
koneksinya sendiri.