Tùy chỉnh chiến lược khóa và thời gian sống bộ nhớ đệm trong Spring Boot với @Cacheable

Trong bài viết trước, chúng ta đã làm quen với các chú giải bộ nhớ đệm cơ bản của Spring như @Cacheable, @CacheEvict@CachePut. Bài này đi sâu hơn vào hai khía cạnh nâng cao: cách tạo khóa (key) tùy chỉnh và cách thiết lập thời gian sống (TTL) cho dữ liệu được lưu trong bộ nhớ đệm — đặc biệt khi tích hợp với Redis.

I. Môi trường phát triển

Chúng ta sử dụng các thành phần sau:

  • Spring Boot 2.2.1.RELEASE
  • Maven 3.5.3
  • Redis 5.0
  • IDEA làm môi trường phát triển

Phụ thuộc Maven cần khai báo:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
</dependencies>

II. Tùy chỉnh khóa và thời gian sống

1. Cơ chế sinh khóa mặc định

Chú giải @Cacheable hỗ trợ hai thuộc tính liên quan đến khóa:

  • cacheNames hoặc value: định danh nhóm bộ nhớ đệm (tương đương tiền tố)
  • key: biểu thức SpEL để xác định giá trị khóa cụ thể

Khóa Redis được xây dựng theo mẫu: <cacheName>::<key-value>.

Ví dụ:

@Cacheable(value = "user")
public String fetchUserById(Long id) {
  return "user_" + id;
}

Khi gọi fetchUserById(123), khóa sẽ là user::123.

Các trường hợp khác:

  • Không có tham số: cacheName::SimpleKey []
  • Nhiều tham số: cacheName::SimpleKey [arg1,arg2]
  • Đối tượng phức tạp: dùng toString() của đối tượng

2. Tạo khóa tùy chỉnh

Để kiểm soát hoàn toàn cách sinh khóa, bạn triển khai interface KeyGenerator:

@Component("customKeyGen")
public class CustomKeyGenerator implements KeyGenerator {
  @Override
  public Object generate(Object target, Method method, Object... params) {
    StringBuilder sb = new StringBuilder();
    sb.append(target.getClass().getSimpleName())
      .append('.')
      .append(method.getName())
      .append('(');
    for (int i = 0; i < params.length; i++) {
      if (i > 0) sb.append(',');
      sb.append(params[i] == null ? "null" : params[i].toString());
    }
    sb.append(')');
    return sb.toString();
  }
}

Sau đó chỉ định trong chú giải:

@Cacheable(value = "profile", keyGenerator = "customKeyGen")
public UserProfile loadProfile(String userId, boolean includeSettings) {
  return new UserProfile(userId, includeSettings);
}

Khóa sinh ra sẽ có dạng: profile::UserProfileService.loadProfile(userId,true).

3. Thiết lập thời gian sống (TTL) toàn cục

Spring Cache không cung cấp thuộc tính TTL trực tiếp trong chú giải. Thay vào đó, cấu hình qua RedisCacheConfiguration:

@Bean
public RedisCacheConfiguration redisCacheConfig() {
  ObjectMapper mapper = new ObjectMapper();
  mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  RedisSerializationContext.SerializationPair<Object> pair =
      RedisSerializationContext.SerializationPair.fromSerializer(
          new GenericJackson2JsonRedisSerializer(mapper)
      );

  return RedisCacheConfiguration.defaultCacheConfig()
      .serializeValuesWith(pair)
      .entryTtl(Duration.ofMinutes(10)); // TTL mặc định: 10 phút
}

Để áp dụng cấu hình riêng theo từng nhóm bộ nhớ đệm:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
  Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
  configMap.put("shortLived", redisCacheConfig().entryTtl(Duration.ofSeconds(30)));
  configMap.put("longLived", redisCacheConfig().entryTtl(Duration.ofHours(24)));

  return RedisCacheManager.builder(factory)
      .cacheDefaults(redisCacheConfig())
      .withInitialCacheConfigurations(configMap)
      .build();
}

4. Thiết lập TTL tại chỗ (trong chú giải)

Để gắn TTL trực tiếp vào giá trị value, ta mở rộng RedisCacheManager:

public class DynamicTtlCacheManager extends RedisCacheManager {
  public DynamicTtlCacheManager(RedisCacheWriter writer, RedisCacheConfiguration defaultConfig) {
    super(writer, defaultConfig);
  }

  @Override
  protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
    String[] parts = name.split("=", 2);
    String cacheName = parts[0];
    if (parts.length == 2) {
      long seconds = Long.parseLong(parts[1]);
      cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(seconds));
    }
    return super.createRedisCache(cacheName, cacheConfig);
  }
}

Sử dụng như sau:

@Cacheable(value = "session=120") // TTL = 120 giây
public SessionData getSession(String sessionId) {
  return new SessionData(sessionId);
}

Và đăng ký bean:

@Bean
@Primary
public RedisCacheManager dynamicTtlCacheManager(RedisConnectionFactory factory) {
  return new DynamicTtlCacheManager(
      RedisCacheWriter.lockingRedisCacheWriter(factory),
      redisCacheConfig()
  );
}

5. Kiểm tra TTL thực tế

Thêm endpoint kiểm tra thời gian còn lại:

@GetMapping("/cache-info")
public Map<String, Object> getCacheInfo(@RequestParam String keyPrefix) {
  Map<String, Object> result = new HashMap<>();
  Set<String> keys = redisTemplate.keys(keyPrefix + "*");
  
  Map<String, Long> ttlMap = new HashMap<>();
  for (String k : keys) {
    Long ttl = redisTemplate.getExpire(k);
    ttlMap.put(k, ttl);
  }
  
  result.put("keys", keys);
  result.put("ttl", ttlMap);
  return result;
}

III. Tổng kết

Các điểm chính cần ghi nhớ:

  • @Cacheable: đọc từ bộ nhớ đệm nếu tồn tại; nếu không, thực thi phương thức và lưu kết quả.
  • @CacheEvict: xóa mục khỏi bộ nhớ đệm.
  • @CachePut: luôn thực thi phương thức và cập nhật bộ nhớ đệm.
  • @Caching: nhóm nhiều chú giải cùng lúc.
  • Khóa mặc định dựa trên số lượng và kiểu tham số — dễ gây xung đột nếu không kiểm soát.
  • TTL nên được cấu hình linh hoạt: toàn cục, theo nhóm, hoặc gắn trực tiếp vào chú giải.

Việc kết hợp KeyGeneratorRedisCacheManager mở rộng giúp kiểm soát hoàn toàn hành vi bộ nhớ đệm, đảm bảo hiệu năng và tính nhất quán trong ứng dụng thực tế.

Thẻ: spring-boot Redis spring-cache keygenerator TTL

Đăng vào ngày 30 tháng 8 lúc 11:29