Redis: Caching & Token Blacklist
Pernahkah Anda memperhatikan bahwa halaman beranda toko online bisa dimuat jauh lebih cepat dibanding query yang sama ke database? Rahasianya biasanya bukan database yang lebih hebat, melainkan cache — penyimpanan data sementara yang memotong kerja server. Redis adalah salah satu alat paling populer untuk pekerjaan ini, dan di artikel ini kita akan belajar cara menggunakannya di backend, termasuk pola yang paling sering dipakai: cache-aside dan token blacklist.
Apa Itu Redis?
Redis (singkatan dari Remote Dictionary Server) adalah key-value store in-memory — database yang menyimpan data di RAM, bukan di disk seperti kebanyakan database relasional. Karena akses ke RAM jauh lebih cepat daripada disk, Redis bisa melayani jutaan request per detik dengan latensi dalam orde sub-millisecond.
Dari sisi database, Redis termasuk golongan NoSQL: data disimpan sebagai pasangan key dan value, bukan dalam tabel ber-relasi. Ini membuatnya sangat fleksibel, tetapi juga bukan pengganti database utama.
| Aspek | Database Relasional (mis. PostgreSQL) | Redis |
|---|---|---|
| Lokasi data | Disk (persisten) | RAM (in-memory, opsional persist ke disk) |
| Model data | Tabel, baris, kolom, relasi (SQL) | Key-value, beberapa tipe data |
| Kecepatan baca | Relatif lambat (millisecond) | Sangat cepat (sub-millisecond) |
| Konsistensi & query kompleks | Kuat (JOIN, transaction, constraint) | Terbatas, tidak ada JOIN |
| Persistensi | Bawaan, dijamin | Perlu konfigurasi (RDB/AOF), bisa hilang |
| Use case | Sumber kebenaran utama data | Cache, session, antrian, counter |
Kata kuncinya: Redis adalah "database sementara" untuk data akses panas (hot data) — data yang sering dibaca dan tidak perlu disimpan permanen. Data yang harus benar-benar aman tetaplah berada di database utama; Redis hanya membantu agar aksesnya cepat.
Tipe Data Redis
Berbeda dari database key-value sederhana yang hanya menyimpan string, Redis punya beberapa tipe data built-in. Memilih tipe yang tepat membuat kode lebih ringkas dan efisien.
| Tipe | Bentuk | Contoh Kasus |
|---|---|---|
STRING | Teks/angka sederhana | Counter, session token, JSON kecil |
LIST | Kumpulan berurutan | Antrian (queue), log terbaru |
SET | Kumpulan unik tanpa urutan | Daftar tag, cek keanggotaan |
HASH | Map field → value | Profil session pengguna |
ZSET (Sorted Set) | Kumpulan unik + skor | Leaderboard, data berdasarkan peringkat |
Bitmaps | Manipulasi bit | Pelacakan kehadiran harian |
Contoh pemakaiannya:
- Hitung kunjungan — gunakan
INCR, perintah atomik yang menambah nilai secara aman meski banyak request datang bersamaan:INCR page:home:views:2026-08-05 - Antrian sederhana — gunakan
LPUSHuntuk memasukkan danBRPOPuntuk mengambil dari belakangLIST:LPUSH email:queue user@mail.com BRPOP email:queue 0 - Set tag artikel — gunakan
SADDuntuk menambah danSISMEMBERuntuk memeriksa keanggotaanSET:SADD article:42:tags redis cache SISMEMBER article:42:tags cache - Session pengguna — simpan beberapa field sekaligus dalam satu
HASH:HSET session:abc123 user_id 7 role ADMIN HGET session:abc123 role
TTL: Data yang Punya Masa Kadaluarsa
Salah satu fitur yang membuat Redis cocok sebagai cache adalah TTL (Time-To-Live): data diberi masa hidup, dan Redis akan otomatis menghapusnya saat masa itu habis.
SET product:42 '{"name":"Sepatu"} ' EX 300
Perintah di atas menyimpan key product:42 selama 300 detik (5 menit). Tanpa TTL, setiap data yang dimasukkan akan menetap di RAM selamanya sampai dihapus manual. Dua masalah muncul kalau begitu:
- Data basi — isi cache tidak akan pernah sinkron dengan database yang sudah berubah.
- Memori penuh —
RAMterbatas; cache tanpa batas akan memakan semua memori server.
Dengan TTL, kedua masalah itu tertangani otomatis. Pilih nilai TTL yang seimbang: terlalu pendek berarti cache jarang berguna, terlalu panjang berarti risiko data basi makin besar.
Pola Cache-aside (Lazy Loading)
Pola paling umum dalam caching adalah cache-aside (sering disebut lazy loading). Di pola ini, aplikasi — bukan database — yang mengendalikan cache: cache hanya diisi saat ada permintaan yang tidak menemukan datanya.
Alurnya sederhana:
- Cek cache — aplikasi menanyakan key ke Redis.
- Hit — data ditemukan, langsung dikembalikan tanpa menyentuh database.
- Miss — data tidak ditemukan, aplikasi membaca dari database.
- Tulis cache — hasil dari database disimpan ke Redis beserta
TTL. - Balas — data dikembalikan ke pemanggil.
+--------+ 1. baca cache +--------+
| |------------------------->| |
| | 2a. HIT <--------------| Redis |
| | | |
| App | +--------+
| | 2b. MISS (tidak ada) |
| |------------------------------\
| | v
| | +----------------+
| | 3. baca DB | Database |
| |<--------------------| (PostgreSQL) |
| | 4. tulis cache | |
| |-------------------->| |
+--------+ +----------------+
Kelebihan pola ini adalah data yang jarang dibaca tidak pernah masuk cache (hemat memori), dan implementasinya lurus — aplikasi yang memutuskan kapan mengisi dan mengosongkan cache. Kekurangannya, sebuah miss tetap mengharuskan baca ke database, dan cache bisa "basi" jika tidak di-invalidate dengan benar.
Sebagai pembanding, ada beberapa pola lain:
| Pola | Cara Kerja | Kapan Cocok |
|---|---|---|
Cache-aside | App baca DB lalu isi cache manual | Paling umum, data per-item, baca banyak |
Write-through | Tulis ke cache dulu, lalu ke DB secara sinkron | Data harus konsisten seketika |
Write-back | Tulis ke cache dulu, ditulis ke DB belakangan (async) | Beban tulis sangat tinggi |
Cache bukan "biar cepat", melainkan strategi kompromi: konsistensi dikorbankan sedikit demi kecepatan. Karena itu setiap cache harus punya cara untuk dihapus atau kedaluwarsa — jangan pernah biarkan cache hidup tanpa TTL.
Implementasi Spring Boot
Di ekosistem Java, integrasi Redis paling sering lewat Spring Data Redis. Tambahkan dependency ini ke pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Lalu konfigurasi koneksi lewat application.yml:
spring:
data:
redis:
host: localhost
port: 6379
timeout: 2s
RedisTemplate dan StringRedisTemplate
Spring menyediakan RedisTemplate sebagai komponen utama untuk berinteraksi dengan Redis. Untuk nilai berupa JSON, kita perlu memberi tahu Spring bagaimana serialize objek — perhatikan konfigurasi berikut dari referensi Spring Data Redis:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key & hash key sebagai String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// value sebagai JSON
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
return template;
}
}
Penyimpanan dan pembacaan dengan TTL:
@Service
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
public CacheService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void put(String key, Object value, Duration ttl) {
redisTemplate.opsForValue().set(key, value, ttl);
}
public Optional<Object> get(String key) {
Object value = redisTemplate.opsForValue().get(key);
return Optional.ofNullable(value);
}
}
Service Cache-aside untuk Data Produk
Sekarang mari kita rangkai pola cache-aside pada service produk:
@Service
public class ProductService {
private static final String CACHE_KEY = "product:";
private final ProductRepository repository;
private final RedisTemplate<String, Object> redisTemplate;
public ProductService(ProductRepository repository, RedisTemplate<String, Object> redisTemplate) {
this.repository = repository;
this.redisTemplate = redisTemplate;
}
public Product getProduct(Long id) {
String key = CACHE_KEY + id;
// 1. cek cache dulu
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return (Product) cached; // 2. HIT
}
// 3. MISS -> baca database
Product product = repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
// 4. tulis cache dengan TTL 10 menit
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(10));
return product; // 5. balas
}
}
Anotasi @Cacheable dan @CacheEvict
Spring Cache abstraction membungkus pola di atas agar tidak menulis kode cache secara manual. Aktifkan dulu dengan @EnableCaching di salah satu kelas konfigurasi:
@Configuration
@EnableCaching
public class CacheConfig {
}
Kemudian anotasi langsung di service:
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@CacheEvict(value = "products", key = "#id")
public void updateProduct(Long id, ProductUpdateDto dto) {
// logika update; cache untuk id ini otomatis dihapus
}
}
Perilakunya: @Cacheable menyimpan hasil method ke cache bernama products dengan key id; panggilan kedua dengan id sama tidak mengeksekusi method. @CacheEvict menghapus entri saat data diubah.
Peringatan: Spring Cache abstraction menggunakan serializer dan key generator bawaan. Pastikan objek yang dikembalikan serializable (atau terdaftar di
GenericJackson2JsonRedisSerializer), dan perhatikankeydefault (SimpleKey). Bila tidak konsisten, error "Could not read JSON" atau cache yang selalu miss adalah hal yang umum.
Invalidasi Cache
@Cacheable saja tidak cukup — cache harus dinonaktifkan (dibersihkan) saat data diubah, kalau tidak pengguna melihat data lama. Dua waktu terpenting:
- Setelah update — data berubah di database, hapus key dari cache.
- Setelah delete — data hilang, key tidak boleh tersisa.
Contoh pembersihan manual setelah update:
@Service
public class ProductService {
public Product updateProduct(Long id, ProductUpdateDto dto) {
Product product = repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
product.setName(dto.name());
product.setPrice(dto.price());
Product saved = repository.save(product);
String key = "product:" + id;
redisTemplate.delete(key); // invalidasi cache
return saved;
}
}
Strategi eviction yang umum:
| Strategi | Cara Kerja | Kapan Dipakai |
|---|---|---|
TTL | Data hilang otomatis setelah masa hidup | Paling umum, dijamin Redis |
Invalidation manual | App hapus key saat data berubah | Data sering di-update |
LRU (Least Recently Used) | Hapus data paling lama tidak dipakai saat memori penuh | maxmemory-policy di konfigurasi |
LFU (Least Frequently Used) | Hapus data paling jarang dipakai | Pola akses tidak merata |
Token Blacklist dengan Redis
Ini aplikasi Redis yang paling berguna untuk proyek Anda sendiri. Masalahnya dimulai dari sifat JWT (JSON Web Token): token itu stateless, artinya begitu dikirim ke klien, server tidak menyimpan catatan apa pun tentang token tersebut.
+--------+ +--------+
| Klien |---------->| Server |
| | JWT | | -> validasi signature + exp
+--------+ +--------+
(server TIDAK tahu apakah token sudah di-logout)
Konsekuensinya: saat pengguna logout, server tidak bisa "mencabut" token itu — token tetap valid sampai masa exp-nya habis, dan penyerang yang punya salinan token bisa terus memakainya. Solusi yang populer adalah token blacklist di Redis:
- Saat logout / revoke, simpan
jti(identitas unik token) ke Redis dengan TTL sebesar sisa masa berlaku token. - Setiap request masuk, periksa apakah
jtiada di blacklist. Jika ya, tolak request tersebut.
Login: issue JWT (jti=abc, exp dalam 2 jam)
Logout: SET blacklist:abc 1 EX 7200
Request berikutnya:
1. Ambil jti dari token
2. GET blacklist:<jti> -> ada? -> 401 Unauthorized
-> tidak ada? -> lanjut proses
Kode di Spring Boot:
@Service
public class TokenBlacklistService {
private final StringRedisTemplate stringRedisTemplate;
public TokenBlacklistService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void blacklist(String jti, long ttlSeconds) {
stringRedisTemplate.opsForValue().set(
"blacklist:" + jti, "1", Duration.ofSeconds(ttlSeconds));
}
public boolean isBlacklisted(String jti) {
return Boolean.TRUE.equals(
stringRedisTemplate.hasKey("blacklist:" + jti));
}
}
Dan filter yang memeriksa sebelum memproses request:
@Component
public class JwtBlacklistFilter extends OncePerRequestFilter {
private final TokenBlacklistService blacklistService;
private final JwtTokenParser jwtParser;
public JwtBlacklistFilter(TokenBlacklistService blacklistService, JwtTokenParser jwtParser) {
this.blacklistService = blacklistService;
this.jwtParser = jwtParser;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String jti = jwtParser.parseJti(header.substring(7));
if (blacklistService.isBlacklisted(jti)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
filterChain.doFilter(request, response);
}
}
Keunggulan pendekatan ini:
- Revoke sederhana — cukup satu perintah
SET, tidak perlu memodifikasi database. - TTL otomatis — entri blacklist menghilang sendiri setelah token asli kedaluwarsa, tidak menumpuk selamanya.
Catatan: blacklist adalah data "ekstra" yang bisa sangat banyak bila pengguna rajin logout. TTL membuatnya tidak permanen, tapi tetap perhatikan ukuran memori bila aplikasi melayani jutaan pengguna. Untuk skala besar, pertimbangkan pola
refresh token rotationyang digabung dengan blacklist.
Penggunaan Lain yang Berguna
Redis tidak hanya untuk cache dan blacklist. Beberapa pemakaian populer lainnya:
- Session store — simpan sesi login memakai tipe
HASHdengan TTL. - Rate limiting counter — gunakan
INCR+ TTL untuk membatasi jumlah request per detik. - Pub/Sub — mengirim pesan dari satu service ke service lain secara fire-and-forget.
- Distributed lock — koordinasi antar instance aplikasi memakai
SETNX+ TTL.
Contoh rate limiter sederhana:
public boolean allowRequest(String clientIp) {
String key = "ratelimit:" + clientIp;
Long count = stringRedisTemplate.opsForValue().increment(key);
if (count != null && count == 1L) {
stringRedisTemplate.expire(key, Duration.ofSeconds(60));
}
return count != null && count <= 100; // maksimal 100 request per menit
}
Jebakan yang Sering Ditemui
Beberapa hal yang perlu diwaspadai saat memakai Redis di production:
- Cache stampede — saat sebuah key expire, puluhan request sekaligus menembus cache dan menghantam database. Solusinya lock atau single-flight: hanya satu request yang boleh mengisi cache, sisanya menunggu.
- Serialization overhead — menyimpan objek besar sebagai
JSONlalu deserialize setiap request punya biaya. Cache yang terlalu besar justru membuat request lambat. - Data rahasia di Redis — Redis tidak terenkripsi secara default. Jangan simpan password,
access token, atau data pribadi di sana tanpa lapisan keamanan tambahan. - TTL terlalu lama — makin lama
TTL, makin besar kemungkinan cache basi. Padukan dengan invalidasi manual. - Redis bukan pengganti database utama — data di
RAMbisa hilang saat server restart bila persistensi tidak dikonfigurasi. Simpan data penting tetap di database relasional.
Kapan Tidak Perlu Redis
Redis sangat berguna, tapi bukan solusi universal. Anda mungkin belum membutuhkannya jika:
- Beban kecil — trafik ratusan request per detik masih sangat nyaman ditangani database biasa.
- Data selalu berubah — kalau setiap baca hampir selalu menghasilkan data baru, cache malah menambah kerja tanpa manfaat.
- Aplikasi single-instance — keuntungan utama Redis (memori bersama antar instance) tidak terasa jika aplikasi hanya berjalan di satu server.
Ringkasan
Redis adalah key-value store in-memory yang membuat data akses panas menjadi sangat cepat. Pola cache-aside — cek cache, lalu isi saat miss — adalah cara paling umum memakainya, dan TTL adalah pengaman wajib agar cache tidak basi atau memenuhi memori. Di Spring Boot, RedisTemplate dan anotasi @Cacheable/@CacheEvict menyederhanakan integrasi. Untuk masalah JWT yang stateless, Redis menjadi solusi elegan sebagai token blacklist: saat logout, simpan jti dengan TTL, lalu tolak request yang jti-nya ada di blacklist. Tetaplah ingat bahwa Redis berfungsi sebagai pelengkap database utama — bukan penggantinya.
Lanjut membaca
- Redis Documentation
- Redis Data Types
- Spring Data Redis Reference
- Redis: Cache-aside Pattern
- AWS Caching Overview
Materi lanjutan: