Redis #
Redis adalah in-memory data structure store yang berfungsi sebagai database, cache, message broker, dan queue secara bersamaan. Kecepatannya yang luar biasa — ratusan ribu operasi per detik dengan latensi sub-milidetik — membuatnya menjadi komponen yang hampir selalu ada di aplikasi Ruby on Rails modern: session store, cache halaman, rate limiting, Sidekiq job queue, ActionCable pub/sub, distributed lock, leaderboard, dan banyak lagi. Gem redis adalah client resmi yang paling matang di ekosistem Ruby, sementara connection_pool menangani thread-safety untuk aplikasi multi-threaded. Artikel ini membahas semua struktur data Redis, pola caching yang idiomatik, dan integrasi mendalam dengan Rails.
Instalasi dan Koneksi #
gem install redis
gem install connection_pool # untuk multi-thread
# Gemfile
gem 'redis', '~> 5.0'
gem 'connection_pool', '~> 2.4'
require 'redis'
# Koneksi dasar
redis = Redis.new(
host: "localhost",
port: 6379,
db: 0, # database 0-15
password: ENV["REDIS_PASSWORD"],
timeout: 1, # connection timeout
read_timeout: 1, # read timeout
write_timeout: 1 # write timeout
)
# Via URL (lebih ringkas)
redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
# Dengan TLS (Redis Cloud, production)
redis = Redis.new(
url: ENV["REDIS_TLS_URL"],
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }
)
# Test koneksi
puts redis.ping # => "PONG"
puts redis.info["redis_version"]
# Selalu tutup koneksi
redis.close
Connection Pool — Wajib untuk Multi-Thread #
require 'redis'
require 'connection_pool'
# Buat pool — satu koneksi per thread
REDIS = ConnectionPool.new(size: 10, timeout: 5) do
Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
end
# Gunakan dengan blok
REDIS.with do |redis|
redis.set("kunci", "nilai")
redis.get("kunci")
end
# Atau dengan Redis::Pool (Redis gem 5+)
redis = Redis.new(
url: ENV["REDIS_URL"],
timeout: 1,
pool_size: 10, # pool terintegrasi di Redis gem 5+
pool_timeout: 5
)
String — Tipe Data Dasar #
String adalah tipe paling fundamental di Redis — bisa menyimpan teks, angka, JSON, atau data biner:
redis = Redis.new
# SET dan GET
redis.set("nama", "Rina")
puts redis.get("nama") # => "Rina"
# SET dengan TTL (Time To Live) dalam detik
redis.set("token_sesi", SecureRandom.hex(32), ex: 3600) # expire 1 jam
redis.set("otp", "123456", ex: 300) # expire 5 menit
# SET dengan TTL dalam milidetik
redis.set("lock", "1", px: 5000) # expire 5000ms = 5 detik
# SETNX — set hanya jika key belum ada (return true/false)
berhasil = redis.setnx("kunci_unik", "nilai")
puts berhasil # => true jika berhasil, false jika sudah ada
# SET dengan NX dan EX sekaligus (atomic)
redis.set("distributed_lock", "process_id", nx: true, ex: 10)
# GET dan SET sekaligus (GETSET)
nilai_lama = redis.getset("counter", "0")
# MSET dan MGET — bulk operations
redis.mset("a", 1, "b", 2, "c", 3)
puts redis.mget("a", "b", "c").inspect # => ["1", "2", "3"]
# Increment dan Decrement (atomic!)
redis.set("view_count", 0)
redis.incr("view_count") # => 1
redis.incrby("view_count", 10) # => 11
redis.decr("view_count") # => 10
redis.incrbyfloat("rating", 0.5) # => 0.5
# String operations
redis.append("log", "baris pertama\n")
redis.append("log", "baris kedua\n")
redis.strlen("log") # panjang string
# TTL — sisa waktu hidup key
puts redis.ttl("token_sesi") # => sisa detik, -1 jika tidak punya TTL, -2 jika tidak ada
puts redis.pttl("lock") # dalam milidetik
# Perpanjang atau hapus TTL
redis.expire("token_sesi", 7200) # perpanjang ke 2 jam
redis.persist("token_sesi") # hapus TTL (jadi permanen)
redis.expireat("event", Time.now.to_i + 86400) # expire pada timestamp tertentu
Hash — Objek Terstruktur #
Redis Hash adalah map field-value dalam satu key — ideal untuk menyimpan objek:
# HSET — set satu atau banyak field
redis.hset("pengguna:1", "nama", "Rina", "email", "[email protected]", "umur", 28)
# HGET dan HMGET
puts redis.hget("pengguna:1", "nama") # => "Rina"
puts redis.hmget("pengguna:1", "nama", "email").inspect # => ["Rina", "[email protected]"]
# HGETALL — semua field sebagai Hash Ruby
profil = redis.hgetall("pengguna:1")
puts profil.inspect # => {"nama"=>"Rina", "email"=>"[email protected]", "umur"=>"28"}
# HSETNX — set field hanya jika belum ada
redis.hsetnx("pengguna:1", "created_at", Time.now.iso8601)
# HINCRBY — increment field numerik (atomic)
redis.hincrby("pengguna:1", "login_count", 1)
# HDEL — hapus field
redis.hdel("pengguna:1", "field_tidak_perlu")
# HEXISTS, HKEYS, HVALS, HLEN
puts redis.hexists("pengguna:1", "nama") # => true
puts redis.hkeys("pengguna:1").inspect # => ["nama", "email", "umur", ...]
puts redis.hlen("pengguna:1") # => jumlah field
# Pola umum: simpan model sebagai Redis Hash
class UserCache
KEY_PREFIX = "user:"
def self.simpan(user)
key = "#{KEY_PREFIX}#{user.id}"
Redis.new.hset(key,
"id", user.id,
"nama", user.nama,
"email", user.email,
"role", user.role,
"cached_at", Time.now.iso8601
)
Redis.new.expire(key, 3600) # TTL 1 jam
end
def self.ambil(id)
data = Redis.new.hgetall("#{KEY_PREFIX}#{id}")
return nil if data.empty?
data.transform_keys(&:to_sym)
end
def self.hapus(id)
Redis.new.del("#{KEY_PREFIX}#{id}")
end
end
List — Queue dan Stack #
Redis List adalah linked list yang mendukung push/pop dari kedua ujung — ideal untuk queue, stack, dan riwayat:
# LPUSH / RPUSH — tambah di kiri / kanan
redis.rpush("antrian_email", "[email protected]")
redis.rpush("antrian_email", "[email protected]", "[email protected]")
redis.lpush("log_aktivitas", "login:user1") # tambah di awal (terbaru di kiri)
# LRANGE — ambil range elemen
puts redis.lrange("antrian_email", 0, -1).inspect # semua elemen
puts redis.lrange("log_aktivitas", 0, 9).inspect # 10 terbaru
# LPOP / RPOP — ambil dan hapus dari kiri / kanan
email = redis.lpop("antrian_email") # ambil dari depan (FIFO)
redis.rpop("antrian_email") # ambil dari belakang (LIFO)
# BLPOP — blocking pop (tunggu sampai ada elemen)
# Sangat berguna untuk consumer queue
hasil = redis.blpop("antrian_email", timeout: 30)
# => ["antrian_email", "[email protected]"] atau nil jika timeout
# LLEN — panjang list
puts redis.llen("antrian_email")
# LINSERT — sisipkan sebelum/setelah elemen tertentu
redis.linsert("list", :before, "elemen_target", "elemen_baru")
# Batasi panjang list (hanya simpan N elemen terbaru)
redis.lpush("aktivitas_user:1", "event baru")
redis.ltrim("aktivitas_user:1", 0, 99) # pertahankan hanya 100 terbaru
# LPOS — cari posisi elemen (Redis 6.0.6+)
redis.lpos("list", "nilai_dicari")
Set — Koleksi Unik #
Redis Set adalah koleksi elemen unik yang tidak berurutan — sangat efisien untuk membership check:
# SADD — tambah anggota
redis.sadd("tag:artikel:1", "ruby", "pemrograman", "tips")
redis.sadd("tag:artikel:2", "ruby", "rails", "web")
# SMEMBERS — semua anggota
puts redis.smembers("tag:artikel:1").inspect # => Set {"ruby", "pemrograman", "tips"}
# SISMEMBER — cek keanggotaan (O(1))
puts redis.sismember("tag:artikel:1", "ruby") # => true
puts redis.sismember("tag:artikel:1", "java") # => false
# SCARD — jumlah anggota
puts redis.scard("tag:artikel:1") # => 3
# Operasi himpunan
# SUNION — gabungan
puts redis.sunion("tag:artikel:1", "tag:artikel:2").inspect
# => {"ruby", "pemrograman", "tips", "rails", "web"}
# SINTER — irisan
puts redis.sinter("tag:artikel:1", "tag:artikel:2").inspect
# => {"ruby"}
# SDIFF — selisih
puts redis.sdiff("tag:artikel:1", "tag:artikel:2").inspect
# => {"pemrograman", "tips"}
# SREM — hapus anggota
redis.srem("tag:artikel:1", "tips")
# SPOP — ambil dan hapus anggota acak (berguna untuk random sampling)
redis.spop("hadiah_pool", 3) # ambil 3 pemenang acak
# Pola: tracking online users
redis.sadd("online_users", user_id)
redis.expire("online_users", 300) # reset setiap 5 menit
# Pola: unique visitor tracking per hari
hari_ini = Date.today.strftime("%Y%m%d")
redis.sadd("visitor:#{hari_ini}", ip_address)
redis.expire("visitor:#{hari_ini}", 86400)
puts redis.scard("visitor:#{hari_ini}") # jumlah unique visitor hari ini
Sorted Set — Leaderboard dan Ranking #
Sorted Set adalah Set dengan score numerik — sangat efisien untuk leaderboard, rate limiting, dan delayed queue:
# ZADD — tambah anggota dengan score
redis.zadd("leaderboard", 1500, "rina")
redis.zadd("leaderboard", 2300, "budi")
redis.zadd("leaderboard", 1800, "citra")
redis.zadd("leaderboard", 2300, "deni") # score sama dengan budi
# ZRANGE — ambil berdasarkan rank (ascending)
puts redis.zrange("leaderboard", 0, -1, with_scores: true).inspect
# => [["rina", 1500.0], ["citra", 1800.0], ["budi", 2300.0], ["deni", 2300.0]]
# ZREVRANGE — descending (skor tertinggi dulu)
top3 = redis.zrevrange("leaderboard", 0, 2, with_scores: true)
top3.each_with_index do |(nama, skor), i|
puts "##{i+1}: #{nama} — #{skor.to_i} poin"
end
# ZSCORE — skor anggota tertentu
puts redis.zscore("leaderboard", "rina") # => 1500.0
# ZRANK / ZREVRANK — posisi ranking
puts redis.zrevrank("leaderboard", "budi") # => 0 (rank pertama dari atas)
# ZINCRBY — tambah skor (atomic)
redis.zincrby("leaderboard", 200, "rina") # rina sekarang 1700
# ZRANGEBYSCORE — ambil anggota dalam range skor
redis.zrangebyscore("leaderboard", 1500, 2000)
# Rate Limiting dengan Sorted Set
def rate_limit_ok?(user_id, max_requests: 100, window: 60)
kunci = "rate:#{user_id}"
sekarang = Time.now.to_f
batas = sekarang - window
redis.multi do |r|
r.zadd(kunci, sekarang, sekarang.to_s) # tambah timestamp request ini
r.zremrangebyscore(kunci, "-inf", batas) # hapus yang sudah kadaluarsa
r.zcard(kunci) # hitung request dalam window
r.expire(kunci, window)
end.last <= max_requests
end
# Delayed Queue — eksekusi tugas di waktu tertentu
def jadwalkan(tugas, waktu_eksekusi)
redis.zadd("delayed_queue", waktu_eksekusi.to_f, tugas.to_json)
end
def ambil_tugas_jatuh_tempo
sekarang = Time.now.to_f
redis.zrangebyscore("delayed_queue", "-inf", sekarang, limit: [0, 10]).tap do |tugas|
tugas.each { |t| redis.zrem("delayed_queue", t) }
end
end
Pipelining dan Transaksi #
Pipelining mengirim banyak perintah sekaligus tanpa menunggu respons — sangat meningkatkan throughput:
# Tanpa pipelining: N round-trips ke Redis
100.times { |i| redis.set("key:#{i}", i) } # lambat
# Dengan pipelining: 1 round-trip untuk semua perintah
redis.pipelined do |pipe|
100.times { |i| pipe.set("key:#{i}", i) }
end
# Atau kumpulkan respons
hasil = redis.pipelined do |pipe|
pipe.get("a")
pipe.get("b")
pipe.incr("counter")
end
puts hasil.inspect # => ["nilai_a", "nilai_b", 1]
# MULTI/EXEC — transaksi atomic
# Semua perintah dalam blok dieksekusi atomik
redis.multi do |r|
r.set("saldo:1", 500_000)
r.set("saldo:2", 1_500_000)
r.incr("total_transaksi")
end
# WATCH + MULTI/EXEC — optimistic locking
loop do
redis.watch("saldo:1") do
saldo = redis.get("saldo:1").to_i
break if saldo < 100_000 # tidak cukup saldo
# Jika saldo berubah antara WATCH dan EXEC → multi mengembalikan nil (gagal)
hasil = redis.multi do |r|
r.set("saldo:1", saldo - 100_000)
r.incrby("saldo:2", 100_000)
end
break if hasil # sukses
# Jika nil (konflik) → coba ulang loop
end
end
Distributed Lock #
Lock terdistribusi memastikan hanya satu proses yang menjalankan operasi kritis di sistem terdistribusi:
# Implementasi distributed lock dengan SETNX + EXPIRE (atomic di Redis 2.6.12+)
class DistributedLock
def initialize(redis, nama, ttl: 10)
@redis = redis
@kunci = "lock:#{nama}"
@nilai = "#{Process.pid}-#{Thread.current.object_id}-#{SecureRandom.hex(8)}"
@ttl = ttl
end
def acquire
@redis.set(@kunci, @nilai, nx: true, ex: @ttl)
end
def release
# Hanya hapus jika nilai cocok (kita yang memasang lock)
# Lua script untuk atomic check-and-delete
script = <<~LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA
@redis.eval(script, keys: [@kunci], argv: [@nilai])
end
def with_lock
raise "Gagal acquire lock: #{@kunci}" unless acquire
begin
yield
ensure
release
end
end
end
# Penggunaan
lock = DistributedLock.new(redis, "proses_pembayaran:#{pesanan_id}", ttl: 30)
lock.with_lock do
# Hanya satu proses yang bisa masuk sini per pesanan
proses_pembayaran(pesanan_id)
end
# Dengan gem redlock (implementasi Redlock algorithm yang lebih robust)
# gem install redlock
require 'redlock'
redlock = Redlock::Client.new(["redis://localhost:6379"])
redlock.lock("resource_kritis", 10_000) do |locked|
if locked
# proses eksklusif
else
puts "Tidak bisa acquire lock"
end
end
Pub/Sub #
Redis Pub/Sub memungkinkan messaging real-time antar proses:
# PUBLISHER — kirim pesan ke channel
publisher = Redis.new
publisher.publish("notifikasi:user:99", JSON.generate({
tipe: "pesan_baru",
dari: "Budi",
isi: "Halo!"
}))
# SUBSCRIBER — dengarkan channel di thread terpisah
Thread.new do
subscriber = Redis.new
subscriber.subscribe("notifikasi:user:99") do |on|
on.message do |channel, pesan|
data = JSON.parse(pesan)
puts "#{channel}: #{data['tipe']} dari #{data['dari']}"
# Kirim ke WebSocket, notifikasi push, dll
end
on.subscribe do |channel, jumlah_subscription|
puts "Subscribe ke #{channel} (total: #{jumlah_subscription})"
end
end
end
# PSUBSCRIBE — subscribe dengan pattern wildcard
Thread.new do
subscriber = Redis.new
subscriber.psubscribe("notifikasi:*") do |on|
on.pmessage do |pattern, channel, pesan|
puts "Pattern #{pattern} matched #{channel}: #{pesan}"
end
end
end
sleep 1
publisher.publish("notifikasi:user:99", "pesan baru")
publisher.publish("notifikasi:user:100", "pesan untuk user lain")
Caching Pattern di Rails #
# config/initializers/redis.rb
REDIS_CACHE = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1"))
# Pola fetch — ambil dari cache, jika tidak ada hitung dan simpan
def ambil_dengan_cache(kunci, ttl: 3600)
cached = REDIS_CACHE.get(kunci)
return JSON.parse(cached, symbolize_names: true) if cached
hasil = yield # hitung nilai
REDIS_CACHE.setex(kunci, ttl, JSON.generate(hasil))
hasil
end
# Penggunaan
statistik = ambil_dengan_cache("stats:penjualan:hari_ini", ttl: 300) do
Pesanan.hari_ini.group(:status).count
end
# Cache dengan invalidasi
def simpan_produk(produk)
produk.save!
REDIS_CACHE.del("produk:#{produk.id}") # invalidasi cache produk ini
REDIS_CACHE.del("produk:list:halaman:1") # invalidasi halaman pertama
end
# Rails.cache dengan Redis backend
# config/environments/production.rb
config.cache_store = :redis_cache_store, {
url: ENV["REDIS_URL"],
expires_in: 1.hour,
namespace: "cache:#{Rails.env}",
pool_size: 10,
pool_timeout: 5,
error_handler: ->(method:, returning:, exception:) {
Rails.logger.error "Redis cache error: #{exception.message}"
}
}
# Gunakan Rails.cache
Rails.cache.fetch("produk:#{id}", expires_in: 1.hour) do
Produk.find(id)
end
Rails.cache.write("setting:maintenance", false, expires_in: 5.minutes)
Rails.cache.read("setting:maintenance")
Rails.cache.delete("setting:maintenance")
Rails.cache.delete_matched("produk:*") # hapus semua cache produk
Lua Scripting — Operasi Atomic Kompleks #
Lua script dieksekusi secara atomic di Redis — tidak ada race condition:
# Script: tambah ke sorted set dan batasi jumlah anggota
tambah_dan_trim_script = <<~LUA
local key = KEYS[1]
local score = ARGV[1]
local member = ARGV[2]
local max_size = tonumber(ARGV[3])
redis.call("zadd", key, score, member)
local size = redis.call("zcard", key)
if size > max_size then
redis.call("zremrangebyrank", key, 0, size - max_size - 1)
end
return redis.call("zcard", key)
LUA
# Jalankan script
redis.eval(
tambah_dan_trim_script,
keys: ["top_skor"],
argv: [1500, "rina", 100] # maks 100 anggota
)
# Cache script dengan SHA untuk efisiensi (EVALSHA)
sha = redis.script(:load, tambah_dan_trim_script)
redis.evalsha(sha, keys: ["top_skor"], argv: [1600, "budi", 100])
Sidekiq — Background Job dengan Redis #
Sidekiq menggunakan Redis sebagai backend untuk job queue:
# Gemfile
# gem 'sidekiq', '~> 7.2'
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.redis = {
url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"),
pool_size: ENV.fetch("SIDEKIQ_CONCURRENCY", 10).to_i + 5
}
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
end
# app/workers/kirim_email_worker.rb
class KirimEmailWorker
include Sidekiq::Worker
sidekiq_options(
queue: :email,
retry: 5, # retry maksimal 5x
backtrace: true
)
def perform(user_id, template, data = {})
user = User.find(user_id)
NotifikasiMailer.send(template, user, data).deliver_now
end
end
# Enqueue job
KirimEmailWorker.perform_async(user.id, :selamat_datang)
KirimEmailWorker.perform_in(1.hour, user.id, :pengingat)
KirimEmailWorker.perform_at(Time.now + 2.days, user.id, :followup)
HyperLogLog — Estimasi Unique Count #
HyperLogLog memungkinkan estimasi jumlah unique element dengan penggunaan memori yang sangat kecil (hanya 12KB):
# Tambahkan elemen
redis.pfadd("unique_visitor:20240815", "ip1", "ip2", "ip3", "ip1") # ip1 duplikat diabaikan
# Hitung estimasi (akurasi ~0.81%)
puts redis.pfcount("unique_visitor:20240815") # => ~3
# Merge beberapa HyperLogLog
redis.pfmerge("unique_visitor:minggu_ini",
"unique_visitor:20240812",
"unique_visitor:20240813",
"unique_visitor:20240814",
"unique_visitor:20240815"
)
puts redis.pfcount("unique_visitor:minggu_ini") # estimasi unique visitor minggu ini
Redis Stream — Event Log Permanen #
Redis Stream adalah struktur data log append-only mirip Kafka — untuk event sourcing sederhana:
# Tambahkan event ke stream
id = redis.xadd(
"pesanan_events",
"*", # auto-generate ID berdasarkan timestamp
"event", "pesanan_dibuat",
"pesanan_id", "12345",
"total", "150000",
"user_id", "99"
)
puts "Event ID: #{id}" # => "1723724400000-0"
# Baca event terbaru
events = redis.xrange("pesanan_events", "-", "+", count: 10)
events.each do |id, fields|
puts "#{id}: #{fields}"
end
# Consumer group — distribusi event ke beberapa consumer
redis.xgroup("CREATE", "pesanan_events", "worker_group", "$", mkstream: true)
# Consumer baca dari group
pesan = redis.xreadgroup(
"GROUP", "worker_group", "worker-1",
COUNT: 10, BLOCK: 5000,
"pesanan_events" => ">" # ">" = pesan yang belum diassign
)
# Acknowledge setelah diproses
redis.xack("pesanan_events", "worker_group", id)
Monitoring Redis #
# Info Redis
info = redis.info
puts "Redis versi: #{info['redis_version']}"
puts "Memory used: #{info['used_memory_human']}"
puts "Connected clients: #{info['connected_clients']}"
puts "Commands/sec: #{info['instantaneous_ops_per_sec']}"
puts "Hit rate: #{info['keyspace_hits'].to_f / (info['keyspace_hits'].to_f + info['keyspace_misses'].to_f) * 100}%"
# Monitor key yang paling sering diakses
redis.debug(:sleep, 0) # pastikan debug tersedia
# Slowlog — query yang lambat
slowlog = redis.slowlog(:get, 10)
slowlog.each do |entry|
puts "#{entry[0]}: #{entry[2]}μs — #{entry[3].join(' ')}"
end
# DBSIZE — jumlah key
puts redis.dbsize
# Scan key dengan pattern (tidak blocking seperti KEYS)
cursor = "0"
loop do
cursor, keys = redis.scan(cursor, match: "produk:*", count: 100)
keys.each { |k| puts k }
break if cursor == "0"
end
Ringkasan #
- Connection pool wajib untuk multi-thread — satu koneksi Redis tidak thread-safe; gunakan
ConnectionPoolataupool_sizebawaan Redis gem 5+ agar setiap thread dapat koneksi sendiri.- Pilih tipe data yang tepat — String untuk nilai sederhana, Hash untuk objek, List untuk queue/stack, Set untuk membership, Sorted Set untuk ranking/rate limiting, HyperLogLog untuk unique count perkiraan.
- TTL pada semua cache key — selalu set expiry agar Redis tidak kehabisan memori; pilih TTL berdasarkan seberapa sering data berubah dan seberapa lama stale data bisa diterima.
- Lua script untuk operasi atomic kompleks — operasi yang melibatkan banyak perintah dan harus atomic (bukan
MULTI/EXEC) lebih tepat menggunakan Lua script yang dieksekusi atomik di server.- Pipelining untuk throughput tinggi — kirim banyak perintah dalam satu round-trip dengan
redis.pipelined { }daripada satu per satu; bisa meningkatkan throughput 10-100x.- WATCH + MULTI untuk optimistic locking — untuk kondisi race yang jarang terjadi, lebih efisien dari distributed lock; jika nilai berubah antara WATCH dan EXEC, transaksi gagal dan bisa diulang.
- Distributed lock dengan nilai unik — selalu simpan nilai unik (bukan “1”) sebagai nilai lock, dan gunakan Lua script untuk release agar tidak menghapus lock milik proses lain.
SCANbukanKEYSdi production —KEYS *memblokir Redis sampai selesai; gunakanSCANdengan cursor yang iteratif dan tidak blocking.- Sorted Set untuk rate limiting — simpan timestamp request sebagai score, hapus yang sudah kadaluarsa dengan
ZREMRANGEBYSCORE, hitung sisa denganZCARD; semua operasi O(log N).- Monitor hit rate cache — hit rate di bawah 80% menandakan terlalu banyak cache miss; periksa apakah TTL terlalu pendek atau key naming tidak konsisten.