වෙලාව බේරගමු! Advanced Caching Strategies: Caffeine & Redis | SC Guide

වෙලාව බේරගමු! Advanced Caching Strategies: Caffeine & Redis | SC Guide

වෙලාව බේරගමු! Advanced Caching Strategies: Caffeine සහ Redis එක්ක Multi-Level Caching SC Guide

අද කාලේ software applications කියන්නේ speed එකට තමයි value කරන්නේ. Facebook එක load වෙන්න තත්පර 3ක් ගියා කියමුකෝ, අපේ අය නිකමට වගේ එතනින් අයින් වෙලා වෙන app එකකට යනවා. Amazon එකෙන් කියනවා web page එකක් load වෙන්න තත්පර 1ක් පරක්කු වුණොත්, අවුරුද්දකට ඩොලර් බිලියන 1.6ක් වගේ අහිමි වෙනවා කියලා. ඉතින්, මේ speed එක increase කරගන්න අපිට තියෙන ප්‍රධානම tools වලින් එකක් තමයි Caching කියන්නේ.

අපි හැමෝම පොඩි කාලේ ඉඳන්ම දන්න දෙයක්නේ caching කියන්නේ. Browser එකේ cache එකේ photos save වෙනවා, phone එකේ apps වල data save වෙනවා. ඒත් අපි software engineering වලදී මේක කොහොමද use කරන්නේ? ඒ වගේම, අපි කොහොමද මේක තවත් advanced level එකකට අරන් යන්නේ? අද අපි කතා කරන්නේ ඒ ගැන. විශේෂයෙන්ම, Caffeine (Local Cache) සහ Redis (Distributed Cache) කියන දෙක එකතු කරලා multi-level caching solution එකක් හදන විදිහ ගැන තමයි අපි බලන්නේ.

මේ article එක ඉවර වෙනකොට ඔයාලට පුළුවන් වෙයි ඔයාලගේ API එකකට two-tier cache එකක් implement කරන්න. එහෙනම්, අපි පටන් ගමු!

1. Caching මොකටද? (Why Caching?)

හිතන්න ඔයාලා ගෙදර ඉඳන් Office එකට එනවා කියලා. හැමදාම ගෙදර ඉඳන් Office එකට එද්දි, කන්න මොනවහරි ගේන්න කියලා අම්මා කිව්වොත්, ඔයාලා හැමදාම කඩේට යනවද? නැහැනේ! Office එක ළඟ Coffee Shop එකක හෝ Restaurant එකක තියෙන දේවල් වල Menu එකක් ඔයාලා ළඟ තියාගන්නවා නේද? එතකොට ඕන වෙලාවක ඒක බලලා order කරන්න පුළුවන්. ඒක හරියට Caching වගේ තමයි.

අපේ applications වලදී අපි නිතරම data fetch කරන්නේ database එකකින්, external API එකකින්, නැත්නම් වෙනම calculation එකක් කරලා. මේ හැමදේම කරන්න වෙලාව යනවා. database එකට ගිහින් data fetch කරන එක හරි, external API call කරන එක හරි, server එකට load එකක්. ඒ වෙලාව අපිට save කරගන්න පුළුවන් නම්? ආන්න ඒක තමයි Caching වලින් කරන්නේ.

Cache එකක් කියන්නේ ඉක්මනින් access කරන්න පුළුවන් තැනක data ටිකක් තියාගන්න එක. Database එකකින් data ගන්න යන වෙලාව 100ms නම්, cache එකකින් data ගන්න යන වෙලාව 1ms වෙන්න පුළුවන්. හිතන්නකෝ කොච්චර speed එකක්ද කියලා!

Caching වල ප්‍රයෝජන:

  • Faster Response Times: Users ලාට ඉක්මනින් results ලැබෙනවා.
  • Reduced Database/Server Load: Database එකට එන queries අඩු වෙනවා, server එකට එන requests අඩු වෙනවා. Resources save වෙනවා.
  • Improved User Experience: User ලා සතුටින් ඉන්නවා. Application එක smooth.
  • Scalability: වැඩි requests ප්‍රමාණයක් handle කරන්න පුළුවන් වෙනවා, අඩු resources වලින්.

2. Caching එක Tier දෙකකට බෙදමු! (Two-Tier Caching Explained)

දැන් අපි බලමු මේ advanced caching strategy එක කොහොමද වැඩ කරන්නේ කියලා. අපි මේකට කියන්නේ Multi-Level Caching, නැත්නම් Two-Tier Caching කියලා. මෙතනදී අපි cache tiers දෙකක් use කරනවා: Local Cache එකක් සහ Distributed Cache එකක්.

2.1. Local Cache (In-Memory Cache)

Local Cache එකක් කියන්නේ server එකේ memory එකේම (RAM එකේ) data save කරගන්න එක. මේක තමයි හැම Cache එකකටම වඩා වේගවත්ම Cache එක. Data access කරන්න network request එකක් යවන්න ඕන නෑ. Memory එකෙන්ම කෙලින්ම ගන්න පුළුවන්.

ප්‍රයෝජන:

  • Extremely Fast: Data access කරන්න microseconds වගේ වෙලාවක් යන්නේ.
  • Low Latency: Network hops නැති නිසා latency එකක් නැති තරම්.

අවාසි:

  • Limited Size: Server එකේ memory එකට සීමා වෙනවා. ලොකු data sets store කරන්න බෑ.
  • Not Shared Across Servers: Server instance එකක save කරන data එක, තව server instance එකකට access කරන්න බෑ. Load Balancer එකක් පස්සේ server instances කිහිපයක් තිබ්බොත්, එක server එකක cache කරන data එක, අනිත් server එකට නැති වෙන්න පුළුවන්.
  • Data Invalidation Issues: Data update වුණාම හැම server එකේම cache එක invalidate කරන්න ක්‍රමයක් ඕන වෙනවා.

Java වලට Caffeine කියන්නේ මේ වගේ Local Cache එකක්. මේක Google Guava Cache එකේ successor එකක්. Performance අතින් ගොඩක් ඉහළයි.

2.2. Distributed Cache

Distributed Cache එකක් කියන්නේ servers කිහිපයක් අතරේ share වෙන Cache එකක්. මේක වෙනම server එකක (හෝ cluster එකක) host කරලා තියෙන්නේ. ඒ නිසා හැම server instance එකකටම මේ cache එක access කරන්න පුළුවන්. Redis කියන්නේ මේ වගේ Distributed Cache එකක්.

ප්‍රයෝජන:

  • Shared Across Servers: Multiple application instances වලට එකම cache එක share කරන්න පුළුවන්. Consistency maintain කරන්න ලේසියි.
  • Larger Capacity: Local Cache වලට වඩා ගොඩක් වැඩි data ප්‍රමාණයක් store කරන්න පුළුවන්.
  • Persistence (Optional): Redis වගේ ඒවා disk එකට data save කරන්නත් පුළුවන්. Server restart වුණත් data නැති වෙන්නේ නෑ.

අවාසි:

  • Higher Latency: Network request එකක් යන නිසා Local Cache එකට වඩා ටිකක් පරක්කුයි.
  • More Complex Setup: වෙනම server එකක් manage කරන්න වෙනවා.

ඉතින්, මේ දෙක එකට use කරනකොට අපිට Local Cache එකේ speed එකත්, Distributed Cache එකේ scalability එකත්, consistency එකත් ලැබෙනවා. මේක හරියට, ඔයාලගේ desk එකේ Coffee එකයි biscuit ටිකයි තියාගන්නවා වගේ. ඊට පස්සේ කන්න බඩගිනි වුණාම Kitchen එකට යනවා. Kitchen එකේ නැත්නම්, කඩේට යනවා. ඒ වගේ තමයි.

3. Caffeine & Redis: Perfect Match

දැන් අපි බලමු මේ දෙක implement කරන්නේ කොහොමද කියලා. අපි Java application එකක් (Spring Boot වගේ) use කරනවා කියලා හිතමු.

3.1. Local Cache (Caffeine) Setup

Caffeine library එක project එකට add කරගන්න, Maven එක නම් pom.xml එකට මේ dependency එක එකතු කරන්න:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version> <!-- Latest version may vary -->
</dependency>

දැන්, Local Cache එකක් හදන විදිහ බලමු:

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class LocalCacheManager {

    private final Cache<String, String> localCache;

    public LocalCacheManager() {
        this.localCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES) // Cache items expire after 10 minutes
            .maximumSize(10_000) // Max 10,000 items
            .build();
    }

    public String get(String key) {
        return localCache.getIfPresent(key);
    }

    public void put(String key, String value) {
        localCache.put(key, value);
    }

    public void invalidate(String key) {
        localCache.invalidate(key);
    }
}

මේ code එකෙන් අපි කියන්නේ:

  • Cache එකේ item එකක් දාලා විනාඩි 10කට පස්සේ expire කරන්න.
  • Cache එකේ උපරිම items 10,000ක් තියාගන්න. (ඊට වඩා වැඩි වුණොත් Least Recently Used - LRU වගේ policy එකකින් remove කරනවා.)

3.2. Distributed Cache (Redis) Setup

Redis install කරලා run කරන්න ඕනේ. Docker වලින් නම්, docker run --name some-redis -p 6379:6379 -d redis වගේ command එකක් use කරන්න පුළුවන්. Java වලට Redis client විදිහට Jedis හෝ Lettuce (Spring Data Redis වල default) use කරන්න පුළුවන්.

Jedis dependency එක:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.4.3</version> <!-- Latest version may vary -->
</dependency>

Redis client එකක් use කරන විදිහ:

import redis.clients.jedis.Jedis;

public class RedisCacheManager {

    private final Jedis jedis;
    private static final int DEFAULT_EXPIRATION_SECONDS = 3600; // 1 hour

    public RedisCacheManager() {
        this.jedis = new Jedis("localhost", 6379);
    }

    public String get(String key) {
        return jedis.get(key);
    }

    public void put(String key, String value) {
        jedis.set(key, value);
        jedis.expire(key, DEFAULT_EXPIRATION_SECONDS); // Set with default expiration
    }

    public void put(String key, String value, int expirationSeconds) {
        jedis.setex(key, expirationSeconds, value);
    }

    public void invalidate(String key) {
        jedis.del(key);
    }
}

3.3. Two-Tier Caching Logic (API එකකට එකතු කරමු!)

දැන් අපි බලමු මේ දෙක එකතු කරලා data fetch කරන logic එක කොහොමද implement කරන්නේ කියලා. අපි හිතමු අපිට user details fetch කරන service එකක් තියෙනවා කියලා.

import org.springframework.stereotype.Service;

@Service
public class DataService {

    private final LocalCacheManager localCacheManager;
    private final RedisCacheManager redisCacheManager;
    // Assume you have a UserRepository or direct DB access here

    public DataService(LocalCacheManager localCacheManager, RedisCacheManager redisCacheManager) {
        this.localCacheManager = localCacheManager;
        this.redisCacheManager = redisCacheManager;
    }

    public String getUserData(String userId) {
        String cacheKey = "user:" + userId;

        // 1. Check Local Cache (Tier 1)
        String data = localCacheManager.get(cacheKey);
        if (data != null) {
            System.out.println("Data from Local Cache for key: " + cacheKey);
            return data;
        }

        // 2. Check Distributed Cache (Tier 2)
        data = redisCacheManager.get(cacheKey);
        if (data != null) {
            System.out.println("Data from Redis Cache for key: " + cacheKey);
            localCacheManager.put(cacheKey, data); // Populate local cache for next time
            return data;
        }

        // 3. Data not found in any cache, fetch from actual source (e.g., Database)
        System.out.println("Data not found in cache, fetching from Database for key: " + cacheKey);
        data = fetchDataFromDatabase(userId); // Replace with your actual DB call

        if (data != null) {
            // Populate both caches for future requests
            localCacheManager.put(cacheKey, data);
            redisCacheManager.put(cacheKey, data); // Redis cache can have a longer expiry
        } else {
            System.out.println("Data for key: " + cacheKey + " not found in database.");
        }
        return data;
    }

    private String fetchDataFromDatabase(String userId) {
        // Simulate a database call
        try {
            Thread.sleep(500); // Simulate network latency and processing time
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        if (userId.equals("123")) {
            return "{\"id\":\"123\", \"name\":\"Nimal Perera\"}";
        } else if (userId.equals("456")) {
            return "{\"id\":\"456\", \"name\":\"Kamala Silva\"}";
        } else {
            return null;
        }
    }

    // Method to invalidate cache when data changes (e.g., after an update operation)
    public void invalidateUserData(String userId) {
        String cacheKey = "user:" + userId;
        localCacheManager.invalidate(cacheKey);
        redisCacheManager.invalidate(cacheKey);
        System.out.println("Cache invalidated for key: " + cacheKey);
    }
}

මේ logic එකෙන් වෙන්නේ, මුලින්ම Local Cache එක බලනවා. එතන නැත්නම් Distributed Cache එක බලනවා. එතනත් නැත්නම් විතරයි Database එකට යන්නේ. Database එකෙන් data ආවොත්, ඒ data එක local cache එකටත්, distributed cache එකටත් දෙකටම දානවා. ඒ වගේම, data update වුණාම invalidateUserData වගේ method එකක් call කරලා cache එක clear කරන්නත් ඕනේ. නැත්නම් stale data return වෙන්න පුළුවන්.

4. දෙන්නවම පාවිච්චි කරද්දී සැලකිලිමත් වෙන්න ඕන දේවල් (Things to Consider When Using Both)

Multi-level caching කියන්නේ බලගතු technique එකක් වුණාට, මේක implement කරද්දී සැලකිලිමත් වෙන්න ඕන දේවල් ටිකක් තියෙනවා. නැත්නම්, ලොකු problems එන්න පුළුවන්.

4.1. Cache Invalidation

මේක තමයි Caching වල තියෙන ලොකුම challenge එක. Database එකේ data එක update වුණාම, cache එකේ තියෙන data එකත් update වෙන්න ඕනේ. නැත්නම්, users ලාට පරණ (stale) data එක පෙන්නන්න පුළුවන්.

  • Write-Through/Write-Back: Data update කරනකොටම cache එකත් update කරනවා.
  • Explicit Invalidation: Data update වුණාම, code එකෙන්ම cache එකෙන් අදාළ entry එක remove කරනවා (අපි උඩ දීපු invalidateUserData වගේ).
  • Message Queues (e.g., Kafka, RabbitMQ): Distributed systems වලදී, data update වුණා කියලා message එකක් publish කරලා, ඒ message එක listen කරන හැම server instance එකක්ම තමන්ගේ local cache එක invalidate කරනවා. Redis Pub/Sub එකත් මේකට use කරන්න පුළුවන්.

4.2. Eviction Policies සහ Expiration

Cache එකක ඉඩ සීමිත නිසා, data remove කරන්න ඕන වෙනවා. Caffeine වලට maximumSize, expireAfterWrite, expireAfterAccess වගේ options තියෙනවා. Redis වලත් expire time set කරන්න පුළුවන් (TTL - Time To Live). මේ policies හරියට තෝරාගැනීම වැදගත්. කොච්චර වෙලාවක් data එක cache එකේ තියෙන්න ඕනද, කොච්චර ප්‍රමාණයක් තියාගන්න ඕනද වගේ දේවල් තීරණය කරන්න ඕනේ.

4.3. Serialization

Redis වලට data දානකොට String, JSON, MessagePack වගේ formats වලට serialize කරන්න ඕනේ. Object එකක් කෙලින්ම දාන්න බෑ. හොඳට plan කරලා, efficient serialization mechanism එකක් use කරන්න.

4.4. Monitoring

Cache hit rate (cache එකෙන් data ගන්න පුළුවන් වුණු වාර ගණන), miss rate, eviction rate, memory usage වගේ metrics නිතරම monitor කරන්න. මේවා බැලුවම තමයි cache එක හරියට වැඩ කරනවද කියලා දැනගන්න පුළුවන්.

4.5. Added Complexity

Multi-level caching implement කරනකොට system එකේ complexity එක වැඩි වෙනවා. Cache invalidation, coherence වගේ issues manage කරන්න වෙනවා. ඒ නිසා, මේක ඇත්තටම අවශ්‍ය වෙලාවට විතරක් implement කරන්න. "අනේ අප්පා speed එක අඩුයි වගේ" කියලා හිතලා cache එකක් දැම්මට වැඩක් නෑ. Bottleneck එක හරියට identify කරලා ඒකට solution එකක් විදිහට caching use කරන්න.

නිගමනය

ඉතින්, අපි අද කතා කරපු Advanced Caching Strategies, විශේෂයෙන්ම Caffeine සහ Redis යොදාගෙන multi-level caching implement කරන විදිහ ගැන ඔයාලට දැන් හොඳ අවබෝධයක් ඇති කියලා හිතනවා. Application එකක performance එක වැඩි කරගන්න, server resources save කරගන්න මේ strategy එක ගොඩක් වැදගත් වෙනවා. පොඩි enterprise application එකක ඉඳන් ලොකු microservices architecture එකක් වෙනකම් මේ concepts use කරන්න පුළුවන්.

මේ article එක කියෙව්වාට පස්සේ, ඔයාලගේ project එකක පොඩි API endpoint එකකට මේ two-tier cache එක implement කරලා බලන්න. Database එකෙන් කෙලින්ම data ගන්නකොටයි, cache එකක් use කරනකොටයි තියෙන වෙනස ඔයාලටම බලාගන්න පුළුවන්. ගෝල්ෆේස් එකට යනවට වඩා වේගයි!

ඔයාලගේ අදහස්, ප්‍රශ්න, හෝ මේ ගැන අත්දැකීම් තියෙනවා නම් පහලින් comment එකක් දාන්න. අපි ඒ ගැන කතා කරමු. තවත් මේ වගේ article එකකින් හම්බෙමු!