Spring Boot Caching với Redis: Hướng Dẫn Tối Ưu

Giới Thiệu
Caching là một trong những cách hiệu quả nhất để cải thiện hiệu suất ứng dụng. Bằng cách lưu trữ dữ liệu được truy cập thường xuyên trong bộ nhớ, bạn có thể giảm tải database, giảm thời gian phản hồi, và mở rộng quy mô ứng dụng hiệu quả hơn.
Trong hướng dẫn này, bạn sẽ học cách triển khai caching trong Spring Boot sử dụng Redis, hệ thống lưu trữ dữ liệu trong bộ nhớ tiêu chuẩn công nghiệp. Chúng ta sẽ xây dựng dựa trên Hệ Thống Quản Lý Nhân Viên từ các hướng dẫn trước và thêm một lớp caching mạnh mẽ.
Những Gì Bạn Sẽ Học:
✅ Nền tảng về Spring Cache abstraction
✅ Cài đặt Redis và tích hợp với Spring Boot
✅ Triển khai các chiến lược cache (Cache-Aside pattern)
✅ Cache invalidation và quản lý TTL
✅ Giám sát hiệu suất cache
✅ Giải quyết vấn đề N+1 query
✅ Best practices cho distributed caching
Yêu Cầu:
- Đã hoàn thành hướng dẫn Database Integration
- Hiểu biết cơ bản về REST APIs
- Đã cài đặt Java 17+
- Docker để chạy Redis locally
Tại Sao Chọn Redis Cho Caching?
Mới với Redis? Xem hướng dẫn Học Redis: Hướng Dẫn Toàn Diện Cho Người Mới để hiểu các kiến thức nền tảng của Redis trước khi đi vào tích hợp với Spring Boot.
Redis là lựa chọn hàng đầu cho caching trong các ứng dụng production:
Hiệu Suất
- Lưu trữ trong bộ nhớ với độ trễ microsecond
- Có thể xử lý hàng triệu operations mỗi giây
- Nhanh hơn đáng kể so với database queries
Tính Năng
- Hỗ trợ TTL (Time-To-Live) tích hợp sẵn
- Nhiều cấu trúc dữ liệu (strings, hashes, lists, sets)
- Pub/Sub cho cache invalidation
- Tùy chọn persistence để đảm bảo độ bền
Khả Năng Mở Rộng
- Distributed caching trên nhiều nodes
- Mở rộng theo chiều ngang với Redis Cluster
- Master-replica replication
Hệ Sinh Thái
- Tích hợp xuất sắc với Spring Boot
- Hỗ trợ cộng đồng rộng rãi
- Được thử nghiệm kỹ lưỡng trong production
Thiết Lập Dự Án
Bước 1: Thêm Dependencies
Thêm các dependencies cho Redis và caching vào pom.xml:
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Starter Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>Bước 2: Khởi Động Redis với Docker
Mới với Docker? Xem hướng dẫn Learning Docker: Why It Matters để hiểu cơ bản về containerization.
Tạo file docker-compose.yml:
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: employee-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
redis-data:Khởi động Redis:
docker-compose up -dKiểm tra Redis đang chạy:
docker-compose ps
redis-cli ping # Nên trả về PONGBước 3: Cấu Hình Kết Nối Redis
Cập nhật application.yml:
spring:
application:
name: employee-management
datasource:
url: jdbc:postgresql://localhost:5432/employeedb
username: postgres
password: postgres
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true
# Redis Configuration
data:
redis:
host: localhost
port: 6379
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
# Cache Configuration
cache:
type: redis
redis:
time-to-live: 600000 # 10 phút tính bằng milliseconds
cache-null-values: false
key-prefix: "employee::"
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.springframework.cache: DEBUGHiểu Về Spring Cache Abstraction
Spring Cache cung cấp một API thống nhất cho caching, không phụ thuộc vào cache provider cơ bản. Các annotation chính là:
Core Annotations
@Cacheable - Lưu trữ kết quả của method
@Cacheable(value = "employees", key = "#id")
public Employee findById(Long id) {
// Chỉ thực thi nếu cache miss
return repository.findById(id).orElseThrow();
}@CachePut - Cập nhật cache
@CachePut(value = "employees", key = "#employee.id")
public Employee update(Employee employee) {
return repository.save(employee);
}@CacheEvict - Xóa khỏi cache
@CacheEvict(value = "employees", key = "#id")
public void delete(Long id) {
repository.deleteById(id);
}@Caching - Kết hợp nhiều cache operations
@Caching(
evict = {
@CacheEvict(value = "employees", key = "#id"),
@CacheEvict(value = "employeeList", allEntries = true)
}
)
public void delete(Long id) {
repository.deleteById(id);
}Triển Khai Lớp Caching
Bước 1: Kích Hoạt Caching
Tạo class cấu hình cache:
package com.example.employee.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Cấu hình ObjectMapper cho Redis serialization
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
// Tạo JSON serializer
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
// Cấu hình cache mặc định
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer())
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(serializer)
)
.disableCachingNullValues();
// Cấu hình cache tùy chỉnh
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
// Employees cache - TTL 5 phút
cacheConfigurations.put("employees",
defaultConfig.entryTtl(Duration.ofMinutes(5)));
// Employee list cache - TTL 2 phút
cacheConfigurations.put("employeeList",
defaultConfig.entryTtl(Duration.ofMinutes(2)));
// Department cache - TTL 15 phút
cacheConfigurations.put("departments",
defaultConfig.entryTtl(Duration.ofMinutes(15)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.transactionAware()
.build();
}
}Bước 2: Cập Nhật Domain Models
Đảm bảo các entities của bạn có thể serializable:
package com.example.employee.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDate;
@Entity
@Table(name = "employees")
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String firstName;
@Column(nullable = false)
private String lastName;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String position;
@Column(nullable = false)
private Double salary;
@Column(nullable = false)
private LocalDate hireDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
@JsonIgnoreProperties({"employees", "hibernateLazyInitializer", "handler"})
private Department department;
}package com.example.employee.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "departments")
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@Column(length = 500)
private String description;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
@JsonIgnoreProperties("department")
private List<Employee> employees = new ArrayList<>();
}Bước 3: Triển Khai Service Layer Với Cache
package com.example.employee.service;
import com.example.employee.model.Employee;
import com.example.employee.repository.EmployeeRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class EmployeeService {
private final EmployeeRepository employeeRepository;
/**
* Lấy nhân viên theo ID - Cached
* Cache key: employee::{id}
*/
@Cacheable(value = "employees", key = "#id")
@Transactional(readOnly = true)
public Employee getEmployeeById(Long id) {
log.info("Fetching employee from database: {}", id);
return employeeRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Employee not found: " + id));
}
/**
* Lấy tất cả nhân viên - Cached
* Cache key: employee::all
*/
@Cacheable(value = "employeeList", key = "'all'")
@Transactional(readOnly = true)
public List<Employee> getAllEmployees() {
log.info("Fetching all employees from database");
return employeeRepository.findAll();
}
/**
* Lấy nhân viên theo phòng ban - Cached
* Cache key: employee::department::{departmentId}
*/
@Cacheable(value = "employeeList", key = "'department::' + #departmentId")
@Transactional(readOnly = true)
public List<Employee> getEmployeesByDepartment(Long departmentId) {
log.info("Fetching employees by department from database: {}", departmentId);
return employeeRepository.findByDepartmentId(departmentId);
}
/**
* Tạo nhân viên - Invalidates employee list cache
*/
@CacheEvict(value = "employeeList", allEntries = true)
@Transactional
public Employee createEmployee(Employee employee) {
log.info("Creating new employee: {}", employee.getEmail());
return employeeRepository.save(employee);
}
/**
* Cập nhật nhân viên - Updates cache
*/
@Caching(
put = @CachePut(value = "employees", key = "#employee.id"),
evict = @CacheEvict(value = "employeeList", allEntries = true)
)
@Transactional
public Employee updateEmployee(Employee employee) {
log.info("Updating employee: {}", employee.getId());
if (!employeeRepository.existsById(employee.getId())) {
throw new RuntimeException("Employee not found: " + employee.getId());
}
return employeeRepository.save(employee);
}
/**
* Xóa nhân viên - Evicts from cache
*/
@Caching(evict = {
@CacheEvict(value = "employees", key = "#id"),
@CacheEvict(value = "employeeList", allEntries = true)
})
@Transactional
public void deleteEmployee(Long id) {
log.info("Deleting employee: {}", id);
if (!employeeRepository.existsById(id)) {
throw new RuntimeException("Employee not found: " + id);
}
employeeRepository.deleteById(id);
}
/**
* Xóa tất cả employee caches
*/
@Caching(evict = {
@CacheEvict(value = "employees", allEntries = true),
@CacheEvict(value = "employeeList", allEntries = true)
})
public void clearAllCaches() {
log.info("Clearing all employee caches");
}
}Bước 4: Tạo REST Controller
package com.example.employee.controller;
import com.example.employee.model.Employee;
import com.example.employee.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/employees")
@RequiredArgsConstructor
public class EmployeeController {
private final EmployeeService employeeService;
@GetMapping("/{id}")
public ResponseEntity<Employee> getEmployee(@PathVariable Long id) {
Employee employee = employeeService.getEmployeeById(id);
return ResponseEntity.ok(employee);
}
@GetMapping
public ResponseEntity<List<Employee>> getAllEmployees() {
List<Employee> employees = employeeService.getAllEmployees();
return ResponseEntity.ok(employees);
}
@GetMapping("/department/{departmentId}")
public ResponseEntity<List<Employee>> getEmployeesByDepartment(
@PathVariable Long departmentId) {
List<Employee> employees = employeeService.getEmployeesByDepartment(departmentId);
return ResponseEntity.ok(employees);
}
@PostMapping
public ResponseEntity<Employee> createEmployee(@RequestBody Employee employee) {
Employee created = employeeService.createEmployee(employee);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Employee> updateEmployee(
@PathVariable Long id,
@RequestBody Employee employee) {
employee.setId(id);
Employee updated = employeeService.updateEmployee(employee);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
employeeService.deleteEmployee(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/cache/clear")
public ResponseEntity<String> clearCache() {
employeeService.clearAllCaches();
return ResponseEntity.ok("All caches cleared successfully");
}
}Kiểm Tra Cache Behavior
Test 1: Cache Hit vs Cache Miss
Khởi động ứng dụng và kiểm tra caching:
# Request đầu tiên - Cache MISS (truy vấn database)
curl http://localhost:8080/api/employees/1
# Request thứ hai - Cache HIT (từ Redis)
curl http://localhost:8080/api/employees/1
# Kiểm tra logs - bạn sẽ chỉ thấy MỘT database queryExpected Log Output:
# Request đầu tiên
Fetching employee from database: 1
Hibernate: select ... from employees where id=?
# Request thứ hai
(Không có database query - served from cache)Test 2: Cache Invalidation
# Lấy nhân viên (cache miss)
curl http://localhost:8080/api/employees/1
# Lấy lại (cache hit)
curl http://localhost:8080/api/employees/1
# Cập nhật nhân viên (cache updated)
curl -X PUT http://localhost:8080/api/employees/1 \
-H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Updated","email":"[email protected]",...}'
# Lấy lại (cache hit với dữ liệu đã cập nhật)
curl http://localhost:8080/api/employees/1Test 3: Xác Minh Redis Keys
# Kết nối đến Redis
redis-cli
# Liệt kê tất cả keys
KEYS *
# Expected output:
# 1) "employee::employees::1"
# 2) "employee::employeeList::all"
# Lấy cached value
GET "employee::employees::1"
# Kiểm tra TTL
TTL "employee::employees::1"
# Xóa cache cụ thể
DEL "employee::employees::1"Các Chiến Lược Cache
1. Cache-Aside (Lazy Loading)
Pattern: Ứng dụng kiểm tra cache trước, load từ DB nếu miss
@Cacheable(value = "employees", key = "#id")
public Employee getEmployeeById(Long id) {
// Spring xử lý:
// 1. Kiểm tra cache
// 2. Nếu miss, thực thi method
// 3. Lưu kết quả vào cache
return repository.findById(id).orElseThrow();
}Ưu Điểm:
- Chỉ cache dữ liệu được yêu cầu
- Cache failures không ảnh hưởng application
- Đơn giản để triển khai
Nhược Điểm:
- Request đầu tiên chậm (cache miss)
- Rủi ro cache stampede
2. Write-Through
Pattern: Cập nhật cache mỗi khi database được cập nhật
@CachePut(value = "employees", key = "#employee.id")
public Employee updateEmployee(Employee employee) {
Employee updated = repository.save(employee);
// Spring tự động cập nhật cache
return updated;
}Ưu Điểm:
- Cache luôn consistent
- Không có cache misses trên reads
Nhược Điểm:
- Write latency tăng
- Có thể cache dữ liệu không sử dụng
3. Write-Behind (Write-Back)
Pattern: Cập nhật cache ngay lập tức, persist vào DB bất đồng bộ
@Async
@CachePut(value = "employees", key = "#employee.id")
public CompletableFuture<Employee> updateEmployee(Employee employee) {
// Cập nhật cache ngay lập tức
// Persist vào DB bất đồng bộ
return CompletableFuture.completedFuture(repository.save(employee));
}Ưu Điểm:
- Writes rất nhanh
- Giảm tải database
Nhược Điểm:
- Rủi ro mất dữ liệu
- Triển khai phức tạp
- Yêu cầu xử lý lỗi cẩn thận
Giải Quyết Vấn Đề N+1 Query
Vấn đề N+1 xảy ra khi bạn fetch một collection và sau đó truy cập các entities liên quan:
Ví Dụ Về Vấn Đề
// Không có tối ưu hóa
@GetMapping("/employees-with-departments")
public List<Employee> getEmployeesWithDepartments() {
List<Employee> employees = employeeRepository.findAll(); // 1 query
// Truy cập department triggers N queries bổ sung
employees.forEach(e -> e.getDepartment().getName()); // N queries
return employees;
}
// Kết quả: 1 + N queries (hiệu suất tệ!)Giải Pháp 1: Entity Graph với Caching
@EntityGraph(attributePaths = {"department"})
@Query("SELECT e FROM Employee e")
List<Employee> findAllWithDepartment();
@Cacheable(value = "employeesWithDepartment", key = "'all'")
@Transactional(readOnly = true)
public List<Employee> getAllEmployeesWithDepartment() {
return employeeRepository.findAllWithDepartment(); // Single JOIN query
}Giải Pháp 2: DTO Projection với Caching
public record EmployeeDTO(
Long id,
String firstName,
String lastName,
String email,
String departmentName
) {}
@Query("""
SELECT new com.example.employee.dto.EmployeeDTO(
e.id, e.firstName, e.lastName, e.email, d.name
)
FROM Employee e
LEFT JOIN e.department d
""")
List<EmployeeDTO> findAllEmployeeDTOs();
@Cacheable(value = "employeeDTOs", key = "'all'")
public List<EmployeeDTO> getAllEmployeeDTOs() {
return employeeRepository.findAllEmployeeDTOs();
}Giám Sát Cache và Metrics
Thêm Actuator Cho Cache Metrics
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Cập nhật application.yml:
management:
endpoints:
web:
exposure:
include: health,metrics,caches
endpoint:
health:
show-details: always
metrics:
enable:
cache: trueTạo Cache Statistics Endpoint
package com.example.employee.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@RestController
@RequestMapping("/api/cache")
@RequiredArgsConstructor
public class CacheController {
private final CacheManager cacheManager;
private final RedisTemplate<String, Object> redisTemplate;
@GetMapping("/stats")
public Map<String, Object> getCacheStats() {
Map<String, Object> stats = new HashMap<>();
// Lấy cache names
stats.put("cacheNames", cacheManager.getCacheNames());
// Lấy cache sizes
Map<String, Long> cacheSizes = new HashMap<>();
cacheManager.getCacheNames().forEach(cacheName -> {
var cache = cacheManager.getCache(cacheName);
if (cache instanceof RedisCache redisCache) {
String pattern = cacheName + "::*";
Long size = redisTemplate.keys(pattern).size();
cacheSizes.put(cacheName, size);
}
});
stats.put("cacheSizes", cacheSizes);
// Redis info
Map<String, Object> redisInfo = new HashMap<>();
redisInfo.put("connected", Objects.requireNonNull(
redisTemplate.getConnectionFactory()).getConnection().ping());
stats.put("redis", redisInfo);
return stats;
}
@GetMapping("/keys")
public Map<String, Object> getCacheKeys() {
Map<String, Object> result = new HashMap<>();
cacheManager.getCacheNames().forEach(cacheName -> {
String pattern = cacheName + "::*";
var keys = redisTemplate.keys(pattern);
result.put(cacheName, keys);
});
return result;
}
}Xem Cache Metrics
# Cache statistics
curl http://localhost:8080/api/cache/stats
# Cache keys
curl http://localhost:8080/api/cache/keys
# Actuator metrics
curl http://localhost:8080/actuator/metrics/cache.gets
curl http://localhost:8080/actuator/metrics/cache.putsCác Pattern Caching Nâng Cao
1. Conditional Caching
Cache chỉ với các điều kiện cụ thể:
@Cacheable(
value = "employees",
key = "#id",
condition = "#id > 0", // Chỉ cache IDs dương
unless = "#result == null" // Không cache kết quả null
)
public Employee getEmployeeById(Long id) {
return repository.findById(id).orElse(null);
}2. Custom Cache Key Generator
@Configuration
public class CacheConfig {
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName());
key.append("_").append(method.getName());
for (Object param : params) {
key.append("_").append(param.toString());
}
return key.toString();
};
}
}
// Sử dụng
@Cacheable(value = "employees", keyGenerator = "customKeyGenerator")
public Employee getEmployeeById(Long id) {
return repository.findById(id).orElseThrow();
}3. Cache Warming
Pre-load cache khi khởi động:
package com.example.employee.config;
import com.example.employee.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheWarmer {
private final EmployeeService employeeService;
@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
log.info("Warming up cache...");
try {
// Pre-load dữ liệu được truy cập thường xuyên
employeeService.getAllEmployees();
log.info("Cache warming completed successfully");
} catch (Exception e) {
log.error("Cache warming failed", e);
}
}
}4. Time-Based Cache Refresh
Tự động refresh cache trước khi TTL hết hạn:
@Scheduled(fixedRate = 480000) // 8 phút (trước TTL 10 phút)
@CacheEvict(value = "employeeList", key = "'all'")
public void refreshEmployeeCache() {
log.info("Refreshing employee cache");
getAllEmployees(); // Sẽ reload và cache
}Best Practices Cho Production
1. Cấu Hình Connection Pool
spring:
data:
redis:
lettuce:
pool:
max-active: 20 # Số connections tối đa
max-idle: 10 # Số idle connections tối đa
min-idle: 5 # Số idle connections tối thiểu
max-wait: 2000ms # Thời gian chờ tối đa cho connection
shutdown-timeout: 200ms2. Xử Lý Lỗi
@Slf4j
@Component
public class CacheErrorHandler implements org.springframework.cache.interceptor.CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.error("Cache GET error - cache: {}, key: {}", cache.getName(), key, exception);
// Không throw - cho phép fallback vào database
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.error("Cache PUT error - cache: {}, key: {}", cache.getName(), key, exception);
// Không throw - write vào database đã thành công
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
log.error("Cache EVICT error - cache: {}, key: {}", cache.getName(), key, exception);
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.error("Cache CLEAR error - cache: {}", cache.getName(), exception);
}
}Đăng ký error handler:
@Configuration
@EnableCaching
public class CacheConfig implements CachingConfigurer {
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler();
}
}3. Ngăn Chặn Cache Stampede
@Cacheable(value = "employees", key = "#id", sync = true)
public Employee getEmployeeById(Long id) {
// sync=true đảm bảo chỉ một thread load từ DB
// Các threads khác chờ kết quả
return repository.findById(id).orElseThrow();
}4. Multi-Level Caching
Kết hợp local và distributed caching:
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Caffeine (local) + Redis (distributed)
return new CompositeCacheManager(
caffeineCacheManager(), // L1: Local cache
redisCacheManager(connectionFactory) // L2: Distributed cache
);
}Những Lỗi Thường Gặp và Giải Pháp
❌ Vấn Đề 1: Caching Mutable Objects
// TỆ: Trả về cached mutable object
@Cacheable("employees")
public Employee getEmployee(Long id) {
return repository.findById(id).orElseThrow();
}
// Caller sửa đổi cached object
Employee emp = service.getEmployee(1L);
emp.setSalary(100000.0); // Cache bây giờ bị corrupted!✅ Giải Pháp: Trả về defensive copies
@Cacheable("employees")
public Employee getEmployee(Long id) {
Employee emp = repository.findById(id).orElseThrow();
return Employee.builder()
.id(emp.getId())
.firstName(emp.getFirstName())
// ... copy tất cả fields
.build();
}❌ Vấn Đề 2: Không Serializing Lazy-Loaded Entities
// TỆ: Department là lazy-loaded
@Entity
public class Employee {
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}
// Cache lưu proxy, fails khi deserialization✅ Giải Pháp: Sử dụng DTOs hoặc eager fetch cho cached entities
// Option 1: DTO
public record EmployeeDTO(Long id, String name, String departmentName) {}
// Option 2: Eager fetch cho cached queries
@EntityGraph(attributePaths = {"department"})
Employee findByIdForCache(Long id);❌ Vấn Đề 3: Circular References
// TỆ: Gây infinite recursion khi serialization
@Entity
public class Employee {
@ManyToOne
private Department department;
}
@Entity
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees; // Circular reference!
}✅ Giải Pháp: Sử dụng @JsonIgnoreProperties
@Entity
public class Employee {
@ManyToOne
@JsonIgnoreProperties("employees")
private Department department;
}Kiểm Tra Hiệu Suất
Tạo Load Test Script
#!/bin/bash
# load-test.sh
echo "Starting load test..."
# Test cache miss (request đầu tiên)
echo "Test 1: Cache Miss"
time curl -s http://localhost:8080/api/employees/1 > /dev/null
# Test cache hit (các requests tiếp theo)
echo "Test 2: Cache Hit"
for i in {1..100}; do
curl -s http://localhost:8080/api/employees/1 > /dev/null
done
# Đo thời gian phản hồi
echo "Test 3: Response Time Comparison"
echo "Without cache (cleared):"
curl -X DELETE http://localhost:8080/api/cache/clear
time curl -s http://localhost:8080/api/employees/1 > /dev/null
echo "With cache:"
time curl -s http://localhost:8080/api/employees/1 > /dev/nullKết Quả Mong Đợi:
- Không có cache: ~50-100ms (database query)
- Có cache: ~5-10ms (Redis lookup)
- Cải thiện hiệu suất 10-20 lần!
Tổng Kết và Những Điểm Chính
Chúc mừng! Bạn đã triển khai thành công một lớp caching production-ready trong Spring Boot với Redis.
Những Gì Chúng Ta Đã Học:
✅ Spring Cache abstraction (@Cacheable, @CachePut, @CacheEvict)
✅ Cài đặt Redis và tích hợp Spring Boot
✅ Cấu hình cache với custom TTL
✅ Triển khai Cache-Aside pattern
✅ Giải pháp cho vấn đề N+1 query
✅ Giám sát cache và metrics
✅ Các patterns nâng cao (cache warming, conditional caching)
✅ Best practices cho production
✅ Những lỗi thường gặp và giải pháp
Lợi Ích Về Hiệu Suất:
- Thời gian phản hồi nhanh hơn 10-20 lần
- Giảm tải database
- Khả năng mở rộng tốt hơn
- Trải nghiệm người dùng được cải thiện
Các Bước Tiếp Theo:
- Thực Hành: Thêm caching vào các projects hiện tại của bạn
- Thử Nghiệm: Thử các giá trị TTL khác nhau và chiến lược cache
- Giám Sát: Thiết lập Grafana dashboards cho cache metrics
- Tối Ưu: Profile ứng dụng của bạn và xác định các cơ hội cache
- Học Thêm: Khám phá các tính năng nâng cao của Redis (Pub/Sub, Streams, Modules)
Câu Hỏi Thường Gặp
Q: Khi nào tôi nên sử dụng caching?
A: Cache dữ liệu:
- Được đọc thường xuyên, hiếm khi được ghi
- Tốn kém để tính toán hoặc query
- Giống nhau trên nhiều users (hoặc user-specific nhưng được truy cập thường xuyên)
Q: Làm thế nào để chọn giá trị TTL?
A: Cân nhắc:
- Dữ liệu thay đổi thường xuyên như thế nào (TTL thấp hơn cho dữ liệu dynamic)
- Khả năng chấp nhận staleness (users có thể thấy dữ liệu hơi cũ không?)
- Ràng buộc về bộ nhớ (TTL dài hơn = nhiều bộ nhớ hơn)
- Bắt đầu với 5-10 phút và điều chỉnh dựa trên monitoring
Q: Tôi có nên cache mọi thứ không?
A: Không! Không cache:
- Dữ liệu thay đổi nhanh chóng
- Dữ liệu nhạy cảm theo user (trừ khi cần thiết)
- Objects lớn (> 1MB)
- Dữ liệu với logic invalidation phức tạp
Q: Điều gì xảy ra nếu Redis down?
A: Với xử lý lỗi đúng (CacheErrorHandler), ứng dụng của bạn sẽ:
- Log cache errors
- Fall back vào database
- Tiếp tục hoạt động (chậm hơn, nhưng vẫn available)
Q: Làm thế nào để xử lý cache consistency trong distributed systems?
A: Sử dụng:
- Redis Pub/Sub cho cache invalidation messages
- Giá trị TTL ngắn
- Event-driven invalidation
- Cân nhắc eventual consistency
Q: Tôi có thể sử dụng nhiều Redis instances không?
A: Có! Sử dụng Redis Cluster hoặc Sentinel:
spring:
data:
redis:
cluster:
nodes:
- redis-1:6379
- redis-2:6379
- redis-3:6379Tài Nguyên Bổ Sung
Tài Liệu:
Các Hướng Dẫn Liên Quan:
- Học Redis: Hướng Dẫn Toàn Diện Cho Người Mới - Làm chủ kiến thức nền tảng Redis
- Database Integration với JPA
- Performance Optimization Guide (Sắp Ra Mắt)
- Monitoring với Actuator (Sắp Ra Mắt)
Công Cụ:
- RedisInsight - GUI cho Redis
- Apache JMeter - Load testing
- Spring Boot Actuator - Metrics
Chúc bạn caching vui vẻ! 🚀 Nếu bạn thấy hướng dẫn này hữu ích, hãy xem Spring Boot Learning Roadmap đầy đủ.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.