Redis: Caching & Token Blacklist

12 min readIntermediate
RedisCacheNoSQLBackend

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.

AspekDatabase Relasional (mis. PostgreSQL)Redis
Lokasi dataDisk (persisten)RAM (in-memory, opsional persist ke disk)
Model dataTabel, baris, kolom, relasi (SQL)Key-value, beberapa tipe data
Kecepatan bacaRelatif lambat (millisecond)Sangat cepat (sub-millisecond)
Konsistensi & query kompleksKuat (JOIN, transaction, constraint)Terbatas, tidak ada JOIN
PersistensiBawaan, dijaminPerlu konfigurasi (RDB/AOF), bisa hilang
Use caseSumber kebenaran utama dataCache, 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.

TipeBentukContoh Kasus
STRINGTeks/angka sederhanaCounter, session token, JSON kecil
LISTKumpulan berurutanAntrian (queue), log terbaru
SETKumpulan unik tanpa urutanDaftar tag, cek keanggotaan
HASHMap field → valueProfil session pengguna
ZSET (Sorted Set)Kumpulan unik + skorLeaderboard, data berdasarkan peringkat
BitmapsManipulasi bitPelacakan 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 LPUSH untuk memasukkan dan BRPOP untuk mengambil dari belakang LIST:
    LPUSH email:queue user@mail.com
    BRPOP email:queue 0
    
  • Set tag artikel — gunakan SADD untuk menambah dan SISMEMBER untuk memeriksa keanggotaan SET:
    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:

  1. Data basi — isi cache tidak akan pernah sinkron dengan database yang sudah berubah.
  2. Memori penuhRAM terbatas; 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.

Redis — cache-aside pattern

Alurnya sederhana:

  1. Cek cache — aplikasi menanyakan key ke Redis.
  2. Hit — data ditemukan, langsung dikembalikan tanpa menyentuh database.
  3. Miss — data tidak ditemukan, aplikasi membaca dari database.
  4. Tulis cache — hasil dari database disimpan ke Redis beserta TTL.
  5. 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:

PolaCara KerjaKapan Cocok
Cache-asideApp baca DB lalu isi cache manualPaling umum, data per-item, baca banyak
Write-throughTulis ke cache dulu, lalu ke DB secara sinkronData harus konsisten seketika
Write-backTulis 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 perhatikan key default (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:

StrategiCara KerjaKapan Dipakai
TTLData hilang otomatis setelah masa hidupPaling umum, dijamin Redis
Invalidation manualApp hapus key saat data berubahData sering di-update
LRU (Least Recently Used)Hapus data paling lama tidak dipakai saat memori penuhmaxmemory-policy di konfigurasi
LFU (Least Frequently Used)Hapus data paling jarang dipakaiPola 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 jti ada 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 rotation yang 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 HASH dengan 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 JSON lalu 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 RAM bisa 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

Materi lanjutan:

← Back to technical articles