Giới thiệu
Transaction management và caching là hai tính năng quan trọng giúp đảm bảo tính toàn vẹn dữ liệu và cải thiện hiệu năng ứng dụng. Bài này đi sâu vào cơ chế hoạt động, cấu hình và các patterns hay dùng trong production.
1. @Transactional — Deep Dive
1.1 Cơ chế hoạt động
Spring sử dụng AOP Proxy để quản lý transactions. Khi method có @Transactional được gọi, proxy sẽ:
Client → Proxy → begin TX → Target Method → commit/rollback TX
Quan trọng: Gọi method
@Transactionaltừ bên trong cùng class (self-invocation) sẽ không đi qua proxy → transaction không hoạt động.
@Service
public class OrderService {
// ✅ Transaction hoạt động khi gọi từ bên ngoài
@Transactional
public OrderResponse createOrder(CreateOrderRequest request) {
Order order = orderRepository.save(mapToEntity(request));
orderItemRepository.saveAll(mapToItems(request, order));
inventoryService.decreaseStock(request.getItems());
return OrderResponse.from(order);
}
// ❌ self-invocation — Transaction KHÔNG hoạt động
public void processOrders(List<CreateOrderRequest> requests) {
for (var request : requests) {
createOrder(request); // Gọi trực tiếp, không qua proxy
}
}
}
1.2 Propagation (Lan truyền Transaction)
| Propagation | Mô tả |
|---|---|
| REQUIRED (default) | Dùng TX hiện tại hoặc tạo mới |
| REQUIRES_NEW | Luôn tạo TX mới, suspend TX cũ |
| NESTED | TX lồng nhau, rollback không ảnh hưởng TX cha |
| SUPPORTS | Dùng TX nếu có, không tạo mới |
| NOT_SUPPORTED | Chạy ngoài TX, suspend TX nếu có |
| MANDATORY | Bắt buộc phải có TX, throw exception nếu không |
| NEVER | Không được có TX, throw exception nếu có |
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
// Luôn tạo TX mới → audit log được lưu
// ngay cả khi TX gốc bị rollback
auditLogRepository.save(log);
}
1.3 Isolation Levels
@Transactional(isolation = Isolation.REPEATABLE_READ)
public AccountResponse transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findByIdForUpdate(fromId);
Account to = accountRepository.findByIdForUpdate(toId);
from.debit(amount);
to.credit(amount);
accountRepository.saveAll(List.of(from, to));
return AccountResponse.from(from);
}
1.4 Rollback Rules
// Rollback cho tất cả checked exceptions
@Transactional(rollbackFor = Exception.class)
public void riskyOperation() throws Exception { }
// Không rollback cho BusinessException
@Transactional(noRollbackFor = BusinessException.class)
public void processPayment() { }
1.5 Read-Only optimization
// Hibernate sẽ skip dirty checking → tăng performance
@Transactional(readOnly = true)
public Page<ProductResponse> searchProducts(Specification<Product> spec,
Pageable pageable) {
return productRepository.findAll(spec, pageable)
.map(ProductResponse::from);
}
2. Spring Cache Abstraction
2.1 Bật Cache
@Configuration
@EnableCaching
public class CacheConfig { }
2.2 Annotations chính
@Service
public class ProductService {
// Lưu kết quả vào cache "products" với key = id
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
log.info("Fetching product from DB: {}", id);
return productRepository.findById(id)
.map(ProductResponse::from)
.orElseThrow(() -> new ResourceNotFoundException("Product", "id", id));
}
// Update cache khi data thay đổi
@CachePut(value = "products", key = "#id")
public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", "id", id));
product.update(request);
return ProductResponse.from(productRepository.save(product));
}
// Xóa entry khỏi cache
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Xóa toàn bộ cache "products"
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() { }
}
2.3 Cấu hình Caffeine Cache (In-Memory)
// build.gradle.kts
implementation("com.github.ben-manes.caffeine:caffeine")
# application.yml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return manager;
}
}
2.4 Redis Cache (Distributed)
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
time-to-live: 600000 # 10 phút (ms)
cache-null-values: false
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(
RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeValuesWith(
SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withCacheConfiguration("products",
config.entryTtl(Duration.ofMinutes(30)))
.build();
}
}
3. Patterns hay dùng
3.1 Cache-Aside Pattern
Read: App → Cache (hit?) → Yes → return
No → DB → Update Cache → return
Write: App → DB → Evict Cache
3.2 Conditional Caching
// Chỉ cache khi result != null
@Cacheable(value = "users", key = "#email",
unless = "#result == null")
public UserResponse findByEmail(String email) { }
// Chỉ cache khi price > 100
@Cacheable(value = "products", key = "#id",
condition = "#id > 0",
unless = "#result.price < 100")
public ProductResponse getProduct(Long id) { }
Tóm tắt
- @Transactional quản lý transactions qua AOP proxy — hiểu propagation, isolation, rollback rules
- Self-invocation không đi qua proxy → transaction không hoạt động, cần tách service hoặc dùng self-injection
- Spring Cache hỗ trợ @Cacheable, @CachePut, @CacheEvict, dễ dàng chuyển từ Caffeine (in-memory) sang Redis (distributed)
Bài tập
- Implement money transfer giữa 2 tài khoản với @Transactional, đảm bảo atomicity. Test rollback khi số dư không đủ
- Cấu hình Caffeine cache cho ProductService với TTL 10 phút, max 500 entries. Đo thời gian response trước và sau cache
- Tạo audit log service dùng REQUIRES_NEW propagation để log không bị mất khi transaction chính rollback