Tìm hiểu về Cache trong spring qua các ví dụ demo.

Bài này mình sẽ tạo một project spring+groovy+gradle đơn giản để demo một vài thao tác với cache.

Getting Started

gradle.properties

buildscript {
   repositories {
      mavenCentral()
   }
   dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.6.RELEASE")
   }
}
plugins {
   id 'groovy'
   id 'org.springframework.boot' version '2.0.5.RELEASE'
   id 'io.spring.dependency-management' version '1.0.7.RELEASE'
}
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group 'app.tuanluc'
version '1.0-SNAPSHOT'

repositories {
   mavenCentral()
}

repositories {
   mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
   compile("org.springframework.boot:spring-boot-starter-web")
   compile 'org.codehaus.groovy:groovy-all:2.3.11'
   testCompile("junit:junit")
}

Tạo đối tượng Student

class Student {

    String id
    String name
    String address

    Student(String id, String name, String address) {
        super()
        this.id = id
        this.name = name
        this.address = address
    }
}

Enable Caching

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

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

Để sử dụng cache, spring cung cấp annotation @EnableCaching. Bạn có thể đặt annotation này ở hàm main hoặc bất cứ class configuration nào.

Sử dụng Caching với Annotations.

Trong những bước tiếp theo chúng ta sẽ khai báo annotations để caching behavior gắn với method.

@Cacheable

Cách đơn giản nhất để sử dụng cache đó là dùng @Cacheable và khai báo parameter là tên của cache nơi sẽ dùng để lưu trữ kết quả.

@Cacheable(value = "student")
Student getStudentByIDAndName(String id, String Name) {
   try {
      log.info("Going to sleep for 5 Secs.. to simulate backend call.")
      Thread.sleep(1000 * 5)
   }
   catch (InterruptedException e) {
      e.printStackTrace()
   }
   return new Student(id, Name, "HN")
}

Mình sẽ sleep 5s để mô phỏng việc backend phải xử lý dữ liệu.

Tiếp theo tạo controller để test cache:

@GetMapping("/search/{id}/{name}")
Student findStudentByIdAndName(@PathVariable String id, @PathVariable String name) {
    log.info("Searching by ID and Name : $id - $name")
    return studentService.getStudentByIDAndName(id, name)
}

Lần đầu bạn hãy thử seach theo tên và địa chỉ bằng cách vào địa chỉ:

http://localhost:8080/students/search/id01/luc

Chúng ta sẽ thấy ở lần load trình duyệt đầu tiên rất lâu nhưng ở các lần sau sẽ rất nhanh nghĩa là đã cache thành công.

Function getStudentByIDAndName() sẽ kiểm tra trong cache student trước khi thực sự gọi đến method và trả về kết quả.

Chúng ta thử giữ nguyên id và thay đổi name để search:

http://localhost:8080/students/search/id01/huan

Lúc này trình duyệt lại load rất lâu. Nghĩa là server đã chạy lại vào function getStudentByIDAndName() để lấy lại giá trị.

Result console:

Searching by ID and Name : id01 - luc
Going to sleep for 5 Secs.. to simulate backend call.
Searching by ID and Name : id01 - luc
Searching by ID and Name : id01 - luc
Searching by ID and Name : id01 - huan
Going to sleep for 5 Secs.. to simulate backend call.
Searching by ID and Name : id01 - huan
Searching by ID and Name : id01 - huan

– Dữ liệu trong Spring cache lưu trữ dưới dạng key-value.

– Nếu không tự định nghĩa key. Thì key trong spring cache được hiểu là parameter của function đặt dưới annotation.

Trong phần lớn trường hợp thì một cache là đủ nhưng spring vẫn hỗ trợ multiple cache:

@Cacheable({"student", "people"})
public String getPeople(Student student) {...}

Trong trường hợp này, chỉ cần 1 cache chứa kết quả cần thiết thì nó sẽ được trả về và method cũng không cần phải chạy.

@CacheEvict

Bây giờ có 1 vấn đề khi tất cả các method đều sử dụng @Cacheable đó là cache có thể ngày càng lớn, và rất nhiều data đã cũ và không cần thiết sử dụng.
@CacheEvict annotation được sử dụng để remove một, nhiều hoặc tất cả value trong cache.

Để xóa tất cả chúng ta thêm thuộc tính allEntries=true.

@CacheEvict(value = "student", allEntries = true)
Student clearCache() {
   return null
}

Custom key generator

Tạo một method giống phần @Cacheable nhưng chúng ta thêm vào key=”#id”

@Cacheable(value = "student", key = "#id")
Student getStudentByIDAndNameAddress(String id, String name, String address) {
   try {
      log.info("Going to sleep for 5 Secs.. to simulate backend call.")
      Thread.sleep(1000 * 5)
   }
   catch (InterruptedException e) {
      e.printStackTrace()
   }
   return new Student(id, name, address)
}

Đầu tiên vào địa chỉ:

http://localhost:8082/students/search/id03/luc/hcm

Ở lần chạy đầu tiên sẽ thấy trình duyệt load mất 5s. Còn từ những lần sau đã có cache nên rất nhanh.
Tiếp theo bạn hay thay đổi lại giá trị address hoặc name:

http://localhost:8082/students/search/id03/dung/hn

nhưng kết quả trả về ở các lần đều giống nhau cho dù đã thay đổi:

Khác với kết quả khi không tự tạo key ở phần đầu, lần này trình duyệt vẫn trả về kết quả rất nhanh.

{"id":"id03","name":"luc","address":"hcm"}

Trong lần đầu truy cập method được gọi sẽ tìm trong cache ‘student’ chưa có key đó tồn tại nên nó sẽ chạy metod vào lưu trữ lại kết quả trả về là 1 object Student.
Trong các lần tiếp theo dù đã đổi name và address nhưng key chỉ là id nên lúc này method ko cần chạy và kết quả trả về từ cache vẫn như lúc đầu.

Vậy làm sao để giải quyết vấn đề này trong trường hợp ta cần dùng cache mà vẫn muốn update giá trị.

@CachePut

Điểm khác nhau giữa @CachePut và @Cacheable đó là @CachePut sẽ luôn chạy method cho dù đã tồn tại giá trị đó trong cache hay chưa và sẽ update lại giá trị đó.

@CachePut(value = "student")
Student getStudentByIDCachePut(String id) { ... }

Khi cần update giá trị trong cache ta sẽ dùng @CachePut

@Caching

Vì spring không cho phép có 2 annotation giống nhau trên cùng 1 method nên trong trường hợp bạn muốn customize nhiều cache trong 1 method cần sử dụng @Caching.

@Caching(evict = { 
   @Cacheable("addresses")
    @Cacheable(value="directory", key=customer.name) })
public String getAddress(Customer customer) {...}

Conditional Caching(Cache có điều kiện)

Trong nhiều tình huống bạn không muốn lúc nào @CachePut cũng update hay @CacheEvict chỉ remove giá trị trong 1 số trường hợp.

Để có thể thực hiện những việc trên ta sử dụng condition.

condition

@Cacheable(value = "student", key = "#student.id" , condition = "#student.name != 'luc'")
Student findByStudent(Student student) {
   try {
      log.info("Going to sleep for 5 Secs.. to simulate backend call.")
      Thread.sleep(1000 * 5)
   }
   catch (InterruptedException e) {
      e.printStackTrace()
   }
   return student
}

ở trên là 1 ví dụ sử dụng cache condition, method này sẽ lưu vào cache các Student không có name = ‘luc’.

unless

dưới đây là đoạn code sử dụng unless

@Cacheable(value = "student", key = "#id", unless = "#result.name == 'luc'")
    Student getStudentByIDAndNameAddress(String id, String name, String address) {
       try {
          log.info("Going to sleep for 5 Secs.. to simulate backend call.")
          Thread.sleep(1000 * 5)
       }
       catch (InterruptedException e) {
          e.printStackTrace()
       }
       return new Student(id, name, address)
    }

điểm khác với condition, đó là unless mang nghĩa phủ định và ngoài ra unless sẽ thực hiện sau khi method trả về kết quả, còn với condition là trước khi thực hiện method. Để tao tác với kết quả của method chúng ta dùng biến #result (biến này có kiểu giống với kiểu trả về của method trong vd này là kiểu Student).

@Cacheable: chỉ lưu trữ giá trị vào khi thỏa mãn condition.

@CachePut: Method luôn được thực hiện nhưng chỉ update hoặc insert giá trị vào cache nếu thỏa mãn condition.

@CacheEvict: Chỉ thực hiện xóa giá trị tương ứng cache khi thỏa mãn condition.

Cấu hình Cache trong configuration

Ngoài cách sử dụng annotation phía trên method để thao tác với cache ta cũng có thể cấu hình java config cho Cache trong spring.

@Bean
     CacheManager cacheManager() {
         SimpleCacheManager cacheManager = new SimpleCacheManager()
         cacheManager.setCaches(Arrays.asList(
                 new ConcurrentMapCache("student"),
                 new ConcurrentMapCache("addresses")))
         return cacheManager
     }

Và sau đó sử dụng với các chức năng giống như các phần trước:

@Autowired CacheManager cacheManager

Student getStudent(String id, String name) {
     String address = cacheManager.getCache("addresses").get(id)
     Student student = new Student(id, name, address)

     cacheManager.getCache("student").evict(id)     
     cacheManager.getCache("student").put(id, student)     
     return student 
}

Tài liệu tham khảo

 https://www.baeldung.com/spring-cache-tutorial 

Leave a comment