Skip to main content

3. Implement in-memory cache

Trong phần này, chúng ta sẽ tìm hiểu cách triển khai bộ nhớ đệm trong ứng dụng Spring Boot của chúng ta. Bộ nhớ đệm sẽ giúp cải thiện hiệu suất của ứng dụng bằng cách giảm thiểu số lượng truy vấn đến cơ sở dữ liệu.

Add caching dependencies (Gradle)

Để sử dụng bộ nhớ đệm trong Spring Boot, trước tiên chúng ta cần thêm các phụ thuộc cần thiết vào tệp build.gradle của dự án:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
}

Enable caching

Tiếp theo, chúng ta cần bật tính năng bộ nhớ đệm trong ứng dụng. Điều này có thể được thực hiện bằng cách thêm chú thích @EnableCaching vào lớp cấu hình chính của ứng dụng:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

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

Configure Cache Manager (Optional)

Mặc định, Spring Boot sử dụng bộ nhớ đệm ConcurrentMapCacheManager.

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
cacheManager.setCacheNames(Arrays.asList("users", "products"));
return cacheManager;
}
}

Apply Cache Annotation

@Cacheable - Cache method results

Dùng để lưu trữ kết quả của phương thức vào bộ nhớ đệm. Khi phương thức được gọi với cùng một tham số, kết quả sẽ được lấy từ bộ nhớ đệm thay vì thực hiện lại phương thức.

@Service
public class UserService {

@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("Fetching user from database: " + id);
// Simulate database call
return userRepository.findById(id);
}

@Cacheable(value = "users", key = "#email")
public User getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
}

@CachePut - Always execute and update cache

Dùng để cập nhật bộ nhớ đệm với kết quả của phương thức, ngay cả khi phương thức được gọi.

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}

@CacheEvict - Remove from cache

Dùng để xóa một mục khỏi bộ nhớ đệm. Điều này có thể được sử dụng khi dữ liệu đã thay đổi và cần cập nhật bộ nhớ đệm.

@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}

// Clear entire cache
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() {
// Implementation
}

@Caching - Multiple cache operations

Dùng để kết hợp nhiều hoạt động bộ nhớ đệm trong một phương thức.

@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "userProfiles", key = "#user.id")
})
public void deleteUserAndProfile(User user) {
// Implementation
}

Configure Cache Properties (Optional)

Dùng để cấu hình các thuộc tính bộ nhớ đệm trong tệp application.properties hoặc application.yml. Ví dụ:

# Cache type (simple is default for in-memory)
spring.cache.type=simple

# Cache names
spring.cache.cache-names=users,products,orders

# For Caffeine cache (alternative)
# spring.cache.type=caffeine
# spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
dependencies {
implementation 'com.github.ben-manes.caffeine:caffeine:2.9.2'
}

Configure Caffeine

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterAccess(Duration.ofMinutes(10))
.recordStats());
return cacheManager;
}
}

Custom Key Generation (Optional)

Create custom key generators:

@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "_" +
method.getName() + "_" +
StringUtils.arrayToDelimitedString(params, "_");
}
}

// Usage
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUser(Long id, String type) {
return userRepository.findByIdAndType(id, type);
}

Conditional Caching

@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User getUserById(Long id) {
return userRepository.findById(id);
}

@Cacheable(value = "users", key = "#id", unless = "#result.status == 'INACTIVE'")
public User getActiveUser(Long id) {
return userRepository.findById(id);
}

Testing Cache

@SpringBootTest
class CacheTest {

@Autowired
private UserService userService;

@Autowired
private CacheManager cacheManager;

@Test
void testUserCaching() {
// First call - should hit database
User user1 = userService.getUserById(1L);

// Second call - should hit cache
User user2 = userService.getUserById(1L);

// Verify cache contains the user
Cache userCache = cacheManager.getCache("users");
assertThat(userCache.get(1L)).isNotNull();
}
}

Monitor and Debug

Add logging to see cache operations:

# Enable cache debug logging
logging.level.org.springframework.cache=DEBUG
logging.level.com.github.benmanes.caffeine=DEBUG

Common Cache Strategies

  1. Read-Through Cache: Automatically loads data into the cache on a cache miss.
  2. Write-Through Cache: Writes data to the cache and the underlying data store in a single operation.
  3. Cache Aside: The application code is responsible for loading data into the cache and managing cache invalidation.
  4. Time-Based Expiration: Automatically evicts cache entries after a specified duration.
  5. Size-Based Eviction: Removes the least recently used entries when the cache reaches its maximum size.