Pada aplikasi CodeIgniter 3 dengan trafik tinggi, cache dapat menurunkan beban database dan mempercepat waktu respons secara signifikan. Namun, manfaat tersebut hanya terasa jika strategi cache invalidation dan cache warmup dirancang dengan disiplin. Cache yang cepat tetapi tidak konsisten akan menghasilkan data usang, bug sulit direproduksi, dan perilaku yang membingungkan di produksi.
Artikel ini membahas pola yang praktis untuk CodeIgniter 3: invalidasi setelah operasi CRUD, versioned cache key, pengelompokan ala tag secara sederhana, pre-warm untuk halaman atau data populer, serta mitigasi race condition dan cache stampede. Contoh implementasi difokuskan pada controller, model, dan hook agar invalidasi tetap konsisten lintas alur aplikasi.
Mengapa cache invalidation sulit?
Masalah utama cache bukan hanya menyimpan data, melainkan memastikan data itu masih valid. Pada aplikasi yang sering menerima perubahan data, satu entitas bisa memengaruhi banyak tampilan:
- Halaman detail artikel
- Daftar artikel terbaru
- Widget sidebar artikel populer
- Respons API publik
- Halaman kategori atau pencarian
Jika hanya satu cache key yang dihapus setelah update, cache lain bisa tetap usang. Karena itu, pendekatan terbaik bukan bergantung pada satu key statis, tetapi memakai pola yang lebih terstruktur:
- Versioned cache key untuk menghindari penghapusan massal yang mahal.
- Grouping sederhana agar sekumpulan cache terkait bisa dinyatakan usang bersama.
- Invalidasi terpusat di layer model/service, bukan tersebar acak di banyak controller.
- Warmup selektif untuk data yang paling sering diakses.
Arsitektur cache yang disarankan di CodeIgniter 3
Pilih backend cache yang tepat
CodeIgniter 3 menyediakan driver cache seperti file, APC, Memcached, atau Redis melalui ekstensi/driver tambahan tergantung implementasi proyek. Untuk aplikasi trafik tinggi:
- Redis cocok jika Anda butuh operasi atomik, TTL fleksibel, dan lock sederhana.
- Memcached baik untuk cache volatile berkecepatan tinggi, tetapi fitur lock/tag biasanya perlu pendekatan tambahan.
- File cache sebaiknya hanya untuk trafik rendah atau lingkungan development, karena invalidasi dan konkurensinya lebih lemah.
Jika proyek sudah menggunakan driver cache bawaan CI3, tetap bisa menerapkan pola yang sama selama Anda menjaga penamaan key dan alur invalidasi.
Struktur key yang konsisten
Gunakan format key yang mudah dibaca dan mudah dicari saat debugging. Contoh:
app:{env}:post:{id}:v{version}
app:{env}:post:list:homepage:v{version}
app:{env}:category:{slug}:posts:v{version}
app:{env}:api:posts:popular:v{version}Elemen pentingnya:
- Prefix aplikasi agar tidak bentrok dengan aplikasi lain.
- Environment seperti
prodataustaging. - Tipe resource seperti
post,category,api. - Versi untuk invalidasi tanpa perlu
delete pattern.
Versioned cache key dan grouping sederhana
Prinsip versioned key
Alih-alih menghapus semua key terkait satu entitas, simpan nomor versi terpisah. Saat data berubah, naikkan versinya. Aplikasi akan otomatis membaca key baru, dan key lama akan dibiarkan kedaluwarsa melalui TTL. Ini lebih aman dibanding mencoba menghapus semua key turunan satu per satu.
Misalnya untuk artikel:
- Versi detail artikel:
post_version:{id} - Versi list artikel homepage:
post_list_version:homepage - Versi kategori:
category_version:{slug}
Saat artikel diupdate, Anda tidak harus tahu semua key cache final yang pernah dibuat. Cukup naikkan versi yang relevan.
Library helper cache sederhana
class App_cache {
protected $CI;
protected $ttl = 300;
public function __construct()
{
$this->CI =& get_instance();
$this->CI->load->driver('cache', ['adapter' => 'redis', 'backup' => 'file']);
}
public function get_version($key)
{
$version = $this->CI->cache->get($key);
if ($version === FALSE) {
$version = 1;
$this->CI->cache->save($key, $version, 86400);
}
return (int) $version;
}
public function bump_version($key)
{
$current = $this->get_version($key);
$next = $current + 1;
$this->CI->cache->save($key, $next, 86400);
return $next;
}
public function build_key($base, $version)
{
return 'app:prod:' . $base . ':v' . $version;
}
}Catatan: jika backend Anda mendukung operasi increment atomik, gunakan itu untuk mengurangi risiko bentrok versi pada beban tinggi. Jika tidak, pendekatan
read-modify-writedi atas tetap berguna, tetapi perlu dipahami ada potensi race pada update yang sangat paralel.
Grouping ala tag secara sederhana
CodeIgniter 3 tidak memiliki sistem cache tag seperti beberapa framework modern. Solusi praktisnya adalah memakai versi per grup. Contoh grup:
group_version:post_listsgroup_version:homepage_widgetsgroup_version:category:{slug}
Lalu gabungkan versi grup ke dalam key final:
$postVersion = $this->app_cache->get_version('post_version:' . $id);
$listVersion = $this->app_cache->get_version('group_version:post_lists');
$key = $this->app_cache->build_key('post:' . $id . ':listctx:' . $listVersion, $postVersion);Pola ini membantu ketika satu perubahan data berdampak ke banyak konteks tampilan.
Invalidasi konsisten setelah CRUD
Prinsip utama: invalidasi di model/service, bukan hanya controller
Kesalahan umum adalah menghapus cache hanya di satu controller admin. Padahal data yang sama bisa diubah dari endpoint lain, CLI, import batch, atau integrasi internal. Karena itu, invalidasi harus diletakkan sedekat mungkin dengan operasi tulis data.
Contoh model untuk create/update/delete artikel
class Post_model extends CI_Model {
public function create($data)
{
$this->db->insert('posts', $data);
$id = $this->db->insert_id();
$this->invalidate_post_cache($id, $data['category_slug'] ?? null);
return $id;
}
public function update($id, $data)
{
$old = $this->db->get_where('posts', ['id' => $id])->row_array();
$this->db->where('id', $id);
$this->db->update('posts', $data);
$categorySlug = $data['category_slug'] ?? ($old['category_slug'] ?? null);
$this->invalidate_post_cache($id, $categorySlug, $old);
return true;
}
public function delete($id)
{
$old = $this->db->get_where('posts', ['id' => $id])->row_array();
$this->db->delete('posts', ['id' => $id]);
$this->invalidate_post_cache($id, $old['category_slug'] ?? null, $old);
return true;
}
protected function invalidate_post_cache($id, $categorySlug = null, $old = null)
{
$this->load->library('app_cache');
$this->app_cache->bump_version('post_version:' . $id);
$this->app_cache->bump_version('group_version:post_lists');
$this->app_cache->bump_version('group_version:homepage_widgets');
if ($categorySlug) {
$this->app_cache->bump_version('group_version:category:' . $categorySlug);
}
if (!empty($old['category_slug']) && $old['category_slug'] !== $categorySlug) {
$this->app_cache->bump_version('group_version:category:' . $old['category_slug']);
}
log_message('debug', 'Cache invalidated for post ID: ' . $id);
}
}Perhatikan bahwa invalidasi tidak hanya untuk detail artikel, tetapi juga daftar dan widget yang mungkin ikut berubah. Ini penting agar cache konsisten dari sudut pandang pengguna.
Contoh controller saat membaca cache
class Posts extends CI_Controller {
public function detail($id)
{
$this->load->model('Post_model');
$this->load->library('app_cache');
$this->load->driver('cache', ['adapter' => 'redis', 'backup' => 'file']);
$version = $this->app_cache->get_version('post_version:' . $id);
$key = $this->app_cache->build_key('post:' . $id, $version);
$post = $this->cache->get($key);
if ($post === FALSE) {
$post = $this->Post_model->find_published_by_id($id);
if ($post) {
$this->cache->save($key, $post, 300);
}
}
if (!$post) {
show_404();
}
$this->load->view('posts/detail', ['post' => $post]);
}
}Hook dan post_controller untuk invalidasi dan warmup yang konsisten
Kapan hook berguna?
Hook berguna untuk tugas lintas-cutting seperti logging cache, menandai request yang memicu invalidasi, atau menjalankan warmup ringan setelah respons selesai. Di CI3, Anda bisa memakai post_controller atau post_system, tetapi tetap hati-hati: jangan memindahkan logika bisnis inti invalidasi sepenuhnya ke hook jika data penting berasal dari model.
Contoh menandai invalidasi dari controller lalu diproses hook
// application/controllers/Admin_posts.php
class Admin_posts extends CI_Controller {
public function update($id)
{
$this->load->model('Post_model');
$data = [
'title' => $this->input->post('title', true),
'content' => $this->input->post('content', false),
'category_slug' => $this->input->post('category_slug', true)
];
$this->Post_model->update($id, $data);
$this->load->library('session');
$this->session->set_userdata('warmup_post_id', $id);
redirect('admin/posts');
}
}// application/hooks/Cache_warmup_hook.php
class Cache_warmup_hook {
public function run()
{
$CI =& get_instance();
$CI->load->library('session');
$postId = $CI->session->userdata('warmup_post_id');
if (!$postId) {
return;
}
$CI->session->unset_userdata('warmup_post_id');
// Idealnya panggil service internal/CLI async. Jika synchronous,
// batasi hanya untuk halaman penting agar tidak menambah latency.
@file_get_contents(site_url('internal/warmup/post/' . $postId));
}
}Pendekatan ini sebaiknya dipakai secara terbatas. Untuk beban tinggi, warmup lebih baik dipindahkan ke job queue, cron, atau endpoint internal yang dipanggil asynchronous agar request pengguna tidak ikut melambat.
Strategi cache warmup untuk halaman dan data populer
Apa yang perlu di-warm?
Warmup tidak perlu untuk semua data. Fokus pada data yang:
- sering diakses,
- mahal dihasilkan dari database,
- memengaruhi halaman landing atau API publik,
- baru saja diinvalidasi setelah CRUD penting.
Contohnya:
- Homepage
- Artikel populer 24 jam terakhir
- Daftar kategori utama
- Detail artikel yang baru dipublikasikan atau diedit
- Respons API untuk widget frontend
Pre-warm melalui endpoint internal
class Internal_warmup extends CI_Controller {
public function post($id)
{
if (!$this->input->is_cli_request() && $this->input->ip_address() !== '127.0.0.1') {
show_error('Forbidden', 403);
}
$this->load->model('Post_model');
$this->load->library('app_cache');
$this->load->driver('cache', ['adapter' => 'redis', 'backup' => 'file']);
$version = $this->app_cache->get_version('post_version:' . $id);
$key = $this->app_cache->build_key('post:' . $id, $version);
$post = $this->cache->get($key);
if ($post === FALSE) {
$post = $this->Post_model->find_published_by_id($id);
if ($post) {
$this->cache->save($key, $post, 300);
echo 'warmed';
return;
}
}
echo 'already_cached';
}
}Jika Anda memakai cron, buat job yang secara periodik me-refresh cache halaman populer berdasarkan log akses atau statistik aplikasi.
Race condition, cache stampede, dan stale-while-revalidate
Race condition saat invalidasi
Race dapat terjadi ketika dua request menulis data yang sama hampir bersamaan, atau saat satu request menginvalidasi sementara request lain masih membangun cache lama. Versioned key membantu karena pembaca akan beralih ke versi terbaru. Namun, pada backend tanpa increment atomik, kenaikan versi bisa saling menimpa jika paralel ekstrem. Solusinya:
- Gunakan backend yang mendukung increment atomik.
- Kurangi logika invalidasi yang tersebar.
- Pastikan write database selesai sebelum bump versi cache.
Cache stampede
Stampede terjadi saat cache kedaluwarsa dan banyak request serentak menghitung ulang data yang sama. Dampaknya adalah lonjakan query database.
Mitigasi sederhana:
- Lock per key sebelum regenerasi cache.
- TTL dengan jitter agar key tidak kedaluwarsa bersamaan.
- Stale-while-revalidate untuk tetap menyajikan data lama sebentar sambil satu request merefresh data.
Contoh lock sederhana
$lockKey = 'lock:post:' . $id;
$cacheKey = $key;
$post = $this->cache->get($cacheKey);
if ($post === FALSE) {
$gotLock = $this->cache->save($lockKey, 1, 10);
if ($gotLock) {
$post = $this->Post_model->find_published_by_id($id);
if ($post) {
$ttl = 300 + rand(0, 30);
$this->cache->save($cacheKey, $post, $ttl);
}
} else {
usleep(200000);
$post = $this->cache->get($cacheKey);
}
}Implementasi lock bergantung pada backend cache. Pada sebagian driver, save tidak menjamin perilaku atomik seperti SETNX. Untuk produksi skala besar, gunakan primitive locking yang memang didukung backend Anda.
Stale-while-revalidate sederhana
Daripada langsung menghapus cache lama, simpan metadata waktu soft-expire dan hard-expire. Jika soft-expire terlewati tetapi hard-expire belum, sistem masih boleh melayani data lama sambil memicu refresh di latar belakang.
Keuntungannya:
- latensi pengguna tetap stabil,
- beban database lebih halus,
- risiko stampede berkurang.
Kekurangannya: Anda menerima kemungkinan data sedikit usang untuk jangka waktu singkat. Ini cocok untuk halaman publik, bukan untuk saldo akun atau data finansial real-time.
Pengujian konsistensi cache
Skenario uji yang wajib
- Buat data baru, lalu pastikan detail dan list menampilkan data terbaru.
- Update kategori artikel, lalu pastikan kategori lama dan baru sama-sama konsisten.
- Delete data, lalu pastikan detail tidak lagi muncul dan list terbarui.
- Jalankan beberapa request paralel setelah TTL habis untuk melihat apakah stampede terjadi.
Tips pengujian praktis
Buat endpoint internal atau command CLI yang:
- menghapus versi tertentu,
- membaca key final yang dipakai,
- mencetak status hit/miss,
- membandingkan hasil cache dengan query langsung database.
Jika memungkinkan, tulis integration test yang memverifikasi urutan berikut: write ke database, bump versi, read kembali dari endpoint publik, dan cocokkan hasilnya.
Monitoring, logging, dan debugging cache
Hit/miss ratio
Tanpa monitoring, Anda tidak tahu apakah cache benar-benar membantu. Minimal, catat:
- jumlah hit dan miss per endpoint,
- waktu regenerasi cache,
- jumlah warmup berhasil/gagal,
- frekuensi invalidasi per resource.
Hit ratio yang rendah bisa berarti TTL terlalu pendek, key terlalu granular, atau cache terlalu sering diinvalidasi.
Logging yang berguna
log_message('debug', 'CACHE HIT key=' . $key);
log_message('debug', 'CACHE MISS key=' . $key);
log_message('debug', 'CACHE BUMP version_key=post_version:' . $id);
log_message('error', 'CACHE WARMUP FAILED post_id=' . $id);Pastikan log cukup informatif tetapi tidak membocorkan data sensitif. Jangan menulis payload lengkap user, token, atau konten privat ke log cache.
Hardening: jangan simpan data sensitif sembarangan
Cache sering dianggap hanya lapisan performa, padahal dari sisi keamanan ia juga tempat penyimpanan data. Beberapa aturan penting:
- Jangan cache data sensitif seperti token sesi, OTP, data kartu, atau informasi yang tidak boleh tersimpan di media bersama.
- Pisahkan cache publik dan privat. Respons yang dipersonalisasi per user harus memakai key yang benar-benar terisolasi jika memang perlu di-cache.
- Gunakan TTL pendek untuk data sensitif bila tak terhindarkan, dan pertimbangkan enkripsi di layer lain jika relevan.
- Lindungi endpoint warmup/internal dengan pembatasan IP, CLI-only, secret token internal, atau reverse proxy ACL.
- Sanitasi isi cache. Simpan hanya field yang diperlukan, bukan seluruh row jika banyak kolom sensitif tidak dibutuhkan.
Penutup
Di CodeIgniter 3, strategi cache yang efektif bukan soal menambahkan driver cache lalu selesai. Anda perlu pola invalidasi yang konsisten, penamaan key yang disiplin, versioned key untuk menghindari penghapusan massal, grouping sederhana untuk dependency antartampilan, dan warmup selektif untuk data populer.
Untuk aplikasi trafik tinggi, fokuslah pada tiga hal: konsistensi, ketahanan saat beban paralel, dan observabilitas. Jika Anda sudah menempatkan invalidasi di model/service, menambahkan lock sederhana, menerapkan stale-while-revalidate, serta memonitor hit/miss ratio, maka cache di CodeIgniter 3 akan jauh lebih aman dioperasikan pada skala produksi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!