Spring Boot Caching: API වලට වේගය දෙමු! 🚀 | Cacheable, CacheEvict SC Guide

Spring Boot Caching: API වලට වේගය දෙමු! 🚀 | Cacheable, CacheEvict SC Guide

ආයුබෝවන් කට්ටියටම! කොහොමද ඉතින්?

අද අපි කතා කරන්න යන්නේ ඔයාලගේ Spring Boot applications වලට Super-Saiyan power එකක් වගේ speed එකක් දෙන්න පුළුවන්, හරිම පට්ට topic එකක් ගැන – ඒ තමයි Caching! 🚀

ගොඩක් වෙලාවට අපේ apps වලට data ගේන්න DB එකට හරි වෙන remote service එකකට හරි request යවනවා. හැබැයි මේක හැම පාරම කරද්දි applications slow වෙනවා විතරක් නෙවෙයි, server load එකත් වැඩි වෙනවා, resource consumption එකත් ඉහළ යනවා. එතකොට අපේ user ලා 'අනේ මේක බබා වගේ ස්ලෝ නේ!' කියනවා. 😫 විශේෂයෙන්ම ලංකාවේ වගේ Internet speed එක අඩු වෙලාවට, mobile data පාවිච්චි කරන user කෙනෙක්ට මේ slow response එක දරාගන්න අමාරුයි. අනිත් එක, ඔයාලගේ Cloud bills වැඩි වෙන්නත් පුලුවන් හැම පාරම DB queries දුවද්දි.

මේකට තියෙන නියම solution එක තමයි Caching. Caching කියන්නේ වැඩිපුර පාවිච්චි වෙන data ටිකක් තාවකාලිකව memory එකේ හරි, වෙනත් fast storage එකක හරි තියාගන්න එක. එතකොට ආයෙත් ඒ data ඕන වුණාම DB එකට යන්නේ නැතුව cache එකෙන් කෙලින්ම ගන්න පුලුවන්. හරි, එහෙනම් අපි බලමු Spring Boot එක්ක මේක කොහොමද කරන්නේ කියලා. මේකෙන් ඔයාලගේ application එකට වේගය එන හැටි ඔයාලටම බලාගන්න පුළුවන්.

Caching කියන්නේ මොකක්ද බං? 🤔

හරි, මුලින්ම බලමු Caching කියන්නේ හරියටම මොකක්ද කියලා. පොඩි උදාහරණයකින් කිව්වොත්, ඔයාලා ගෙදර එද්දි හැමදාම කඩෙන් පාන් ගන්නවා කියලා හිතමු. හැමදාම අලුතෙන් පාන් ගන්න යනවා වෙනුවට, සමහර වෙලාවට පාන් ටිකක් අරගෙන ගෙදර තියාගන්නවා නේද? එතකොට බඩගිනි වුණාම කඩේ යන්න ඕනේ නෑ, ගෙදරින්ම ගන්න පුළුවන්, වෙලාවත් ඉතුරුයි, ගමනත් ඉතුරුයි. Caching කියන්නේත් ඔය වගේ වැඩක්. 🍞

Software වලදී, data cache කරනවා කියන්නේ, නිතර නිතර request කරන data ටිකක් වේගවත් තැනක (memory, Redis server, distributed cache) තියාගන්න එක. එතකොට, යම්කිසි request එකක් ආවම, මුලින්ම බලන්නේ cache එකේ ඒ data තියෙනවද කියලා. තියෙනවා නම්, එතනින්ම දීලා වැඩේ ඉවරයි. නැත්නම් විතරයි, DB එකට හරි වෙන remote service එකකට හරි ගිහින් data ගෙනත්, cache එකේත් තියාගෙන, user ට දෙන්නේ. මේකෙන් ලැබෙන වාසි නම් හුඟක් තියෙනවා:

  • Performance වැඩි වෙනවා: Database calls අඩු වෙන නිසා response time එක අඩු වෙනවා.
  • Resource Saving: DB server එකට එන load එක අඩු වෙනවා. Memory, CPU usage එකත් control වෙනවා.
  • Cost Reduction: Cloud environment එකක ඉන්නවා නම්, DB calls අඩු වෙන එකෙන් AWS, Azure, GCP වල bills අඩු කරගන්න පුළුවන්.

දැන් ඔයාට හිතෙයි මේක හැම දේටම දාන්න පුලුවන් නේද කියලා. ඔව්, පුලුවන්. හැබැයි මතක තියාගන්න ඕන දෙයක් තමයි, cache එකේ තියෙන data update වුණොත් ඒක cache එකෙන් අයින් කරන්න ඕන, නැත්නම් පරණ data user ට පෙන්වන්න පුළුවන්. ඒකට තමයි @CacheEvict වගේ methods පාවිච්චි කරන්නේ. ඒ ගැන පස්සේ කතා කරමු.

Spring Boot Caching වලට සෙට් වෙමු 🚀

Spring Boot කියන්නේ caching integrate කරන්න හරිම ලේසි framework එකක්. ඔයාලට කරන්න තියෙන්නේ පොඩි configuration ටිකක් විතරයි. මුලින්ම, ඔයාලගේ pom.xml එකට spring-boot-starter-cache dependency එක add කරගන්න ඕන.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

ඊළඟට, ඔයාලගේ main application class එකේ හරි, වෙනත් configuration class එකක හරි @EnableCaching annotation එක දාන්න ඕන. මේකෙන් තමයි Spring Boot වලට 'අඩෝ, මට Caching ඕන!' කියලා කියන්නේ.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class ProductServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

දැන් ඔයාලගේ application එක Spring Boot Caching පාවිච්චි කරන්න සූදානම්. ඊළඟට අපි බලමු මේක methods වලට implement කරන්නේ කොහොමද කියලා.

@Cacheable - දත්ත ගබඩා කරමු 💾

@Cacheable annotation එක තමයි data ටික cache කරන්න පාවිච්චි කරන්නේ. ඔයාලට ඕනම method එකක් run කරලා එන result එක cache කරන්න මේක පාවිච්චි කරන්න පුළුවන්. හැබැයි මේක දාන්නේ සාමාන්‍යයෙන් data retrieve කරන methods වලට. 🧐

උදාහරණයක් විදියට, product details ගන්න method එකක් හිතමු.

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class ProductService {

    private Map<Long, String> products = new HashMap<>();

    public ProductService() {
        products.put(1L, "Laptop");
        products.put(2L, "Mouse");
        products.put(3L, "Keyboard");
    }

    @Cacheable(value = "products", key = "#id")
    public String getProductById(Long id) {
        System.out.println("Fetching product from database/remote service for ID: " + id);
        // Simulate a delay (e.g., database call)
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return products.get(id);
    }

    @Cacheable(value = "products", key = "#name") // Example with string key
    public String getProductByName(String name) {
        System.out.println("Fetching product from database/remote service for Name: " + name);
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return products.entrySet().stream()
                .filter(entry -> entry.getValue().equalsIgnoreCase(name))
                .map(Map.Entry::getValue)
                .findFirst()
                .orElse(null);
    }
}

මේකේ value කියන්නේ cache එකට දෙන නම. key කියන්නේ cache එකේ data එක අඳුනගන්න පාවිච්චි කරන identifier එක. මෙතනදී අපි method එකට එන id එක key එක විදියට පාවිච්චි කරනවා. @Cacheable දැම්මාම, පළවෙනි පාර getProductById call කරද්දී product එක DB එකෙන් ගෙනත් cache එකේ තියාගන්නවා. දෙවෙනි පාරටත් ඒ id එකම දීලා call කරද්දී, cache එකෙන් කෙලින්ම result එක දෙනවා, System.out.println එක run වෙන්නේ නෑ! (මේ delay එක දාලා තියෙන්නේ DB call එකක simulation එකක් විදියට, එතකොට ඔයාලට වෙනස හොඳටම තේරෙයි).

තව options තියෙනවා: condition, unless වගේ.

  • condition: මේක true වුණොත් විතරයි cache කරන්නේ. උදාහරණයක් විදියට, @Cacheable(value="products", key="#id", condition="#id > 0").
  • unless: මේක false වුණොත් විතරයි cache කරන්නේ. උදාහරණයක් විදියට, @Cacheable(value="products", key="#id", unless="#result == null"). (result එක null නම් cache කරන්න එපා).

@CacheEvict - කැෂේ එක පිරිසිදු කරමු 🧹

දැන් අපි data cache කරා. හැබැයි හිතන්න, product එකක price එක update වුණා කියලා. එතකොට cache එකේ තියෙන්නේ පරණ price එක. ඒක user ට පෙන්වන්න බෑනේ. 😥 ඒකට තමයි @CacheEvict පාවිච්චි කරන්නේ. මේකෙන් පුලුවන් cache එකෙන් data අයින් කරන්න.

සාමාන්‍යයෙන් data update කරන, delete කරන methods වලට මේක දානවා. ඒ වගේම, සමහර වෙලාවට system එකේම cache එක reset කරන්නත් මේක පාවිච්චි කරන්න පුළුවන්.

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class ProductService {
    // ... previous getProductById and getProductByName methods ...

    @CacheEvict(value = "products", key = "#id")
    public String updateProduct(Long id, String newName) {
        System.out.println("Updating product in database for ID: " + id);
        products.put(id, newName);
        return "Product updated: " + newName;
    }

    @CacheEvict(value = "products", key = "#id")
    public String deleteProduct(Long id) {
        System.out.println("Deleting product from database for ID: " + id);
        return products.remove(id);
    }

    // Example to evict all entries in a specific cache
    @CacheEvict(value = "products", allEntries = true)
    public void clearAllProductsCache() {
        System.out.println("Clearing all products cache!");
    }
}

මෙහිදීත් value එකෙන් cache එකේ නම දෙනවා. key එකෙන් මොන entry එකද අයින් කරන්නේ කියලා කියනවා.
allEntries = true කියලා දැම්මොත්, ඒ value එකට අයිති හැම entry එකක්ම cache එකෙන් අයින් වෙනවා. මේක ප්‍රයෝජනවත් වෙන්නේ යම්කිසි event එකක් නිසා whole cache එකම clear කරන්න ඕන වුණාම.
beforeInvocation = true කියන attribute එකත් වැදගත්. මේක true වුණොත්, method එක run වෙන්න කලින්ම cache එක clear කරනවා. false (default) වුණොත්, method එක successfully run වුණාට පස්සේ තමයි clear කරන්නේ. beforeInvocation = true දාන්නේ, method එක fail වුණත් cache එක clear වෙන්න ඕන නම්. (උදා: DB update එක fail වුණත් cache එක clear කරලා, ඊළඟට එන request එකට fresh data ගන්න සැලැස්වීම).

Practical Exercise: API එකකට Caching දාමු 💻

හරි, දැන් අපි මේවා practical විදියට API එකකට දාන්නේ කොහොමද කියලා බලමු. සරල REST API එකක් හදමු products retrieve කරන්න, update කරන්න.

මුලින්ම ProductService එකේ code එක කලින් දැක්කා වගේ හදාගන්න. දැන් අපි ProductController එක හදමු.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public String getProduct(@PathVariable Long id) {
        return productService.getProductById(id);
    }

    @PutMapping("/{id}")
    public String updateProduct(@PathVariable Long id, @RequestParam String name) {
        return productService.updateProduct(id, name);
    }

    @DeleteMapping("/{id}")
    public String deleteProduct(@PathVariable Long id) {
        return "Product deleted: " + productService.deleteProduct(id);
    }

    @PostMapping("/clear-cache")
    public String clearCache() {
        productService.clearAllProductsCache();
        return "Product cache cleared!";
    }
}

දැන් ඔයාලගේ application එක run කරලා බලන්න. ProductServiceApplication එක run කරාම Spring Boot application එක start වෙයි.

ටෙස්ට් කරමු (Let's Test!) 🧪:

ඔයාලට Postman, Insomnia වගේ tool එකක් පාවිච්චි කරන්න පුළුවන්, නැත්නම් browser එකෙන් වුණත් GET requests යවන්න පුළුවන්.

  1. http://localhost:8080/api/products/1 මේ URL එකට request එකක් යවන්න (GET request). Console එකේ Fetching product from database/remote service for ID: 1 කියලා print වෙයි, මොකද මුලින්ම product එක cache එකේ නෑ. Response එක විදියට 'Laptop' කියලා එයි.
  2. ආයෙත් ඒ URL එකටම request එකක් යවන්න. මේ පාර console එකේ අර message එක print වෙන්නේ නෑ, මොකද product එක cache එකෙන් ගත්තා. Response එක තාමත් 'Laptop' වෙයි. මේක තමයි caching! වේගය දැනෙනවා නේද?
  3. http://localhost:8080/api/products/1?name=NewLaptop මේ URL එකට PUT request එකක් යවන්න. (Body එකේ name=NewLaptop වගේ දෙයක් දැම්මත් හරි).
    • Console එකේ Updating product in database for ID: 1 කියලා print වෙයි.
    • මේ update එකත් එක්කම ID 1 ට අයිති cache entry එක @CacheEvict නිසා අයින් වෙනවා.
  4. ආයෙත් http://localhost:8080/api/products/1 මේ URL එකට GET request එකක් යවන්න. දැන් ආයෙත් Fetching product from database/remote service for ID: 1 කියලා print වෙලා, අලුත් NewLaptop එක එන්න ඕන. ඒ කියන්නේ cache එක clear වෙලා, අලුත් data ආයෙත් cache වෙලා. නියමයි නේද! 😎
  5. http://localhost:8080/api/products/clear-cache මේ URL එකට POST request එකක් යවන්න. මේකෙන් products කියන cache එකේ තියෙන හැම entry එකක්ම clear වෙයි.

Conclusion: දුවමු වේගෙන්! 🏁

ඉතින් යාලුවනේ, ඔයාලට දැන් තේරෙනවා ඇති Spring Boot එක්ක Caching කොච්චර ලේසිද කියලා. මේකෙන් ඔයාලගේ applications වල performance එක වැඩි කරන්න, server load එක අඩු කරන්න, user experience එක improve කරන්න පුළුවන්. සල්ලිත් ඉතුරු වෙනවා හොරට! 😉

අපිට මේ වගේ simple in-memory cache එකක් විතරක් නෙවෙයි, Production environment වලට Redis, Ehcache, Caffeine වගේ advanced caching providers පාවිච්චි කරන්න පුළුවන්. ඒ ගැන පස්සේ දවසක කතා කරමු. දැනට, මේ simple caching mechanism එක ඔයාලගේ projects වලට දාලා බලන්න. මේකේ බලය ඔයාලටම අත්දකින්න පුළුවන්.

මොනාහරි ප්‍රශ්න තියෙනවා නම්, මේ article එකේ මොනාහරි පැහැදිලි නැත්නම්, නැත්නම් ඔයාලගේ අත්දැකීම් බෙදාගන්න ඕන නම්, comment section එකේ අහන්න. අපි උදව් කරන්න ලෑස්තියි! එහෙනම් තවත් අලුත් දෙයක් අරගෙන එනකන් හැමෝටම ජය වේවා! Caching වලින් සෙට් වෙලා applications වලට සුපිරි speed එකක් දෙන්න!