Chuyển đến nội dung chính

Bài 13: Transaction Management & Caching

@Transactional deep dive — propagation, isolation levels, rollback rules. Spring Cache abstraction với Caffeine và Redis. Cache eviction strategies.

💻 Lập trình — Bài 12 Bài 13: Transaction Management & Caching

Spring Boot 4: Từ Cơ bản đến Nâng cao

Phần 4: Tính năng Nâng cao

xdev.asia

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 @Transactional từ 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)

PropagationMô tả
REQUIRED (default)Dùng TX hiện tại hoặc tạo mới
REQUIRES_NEWLuôn tạo TX mới, suspend TX cũ
NESTEDTX lồng nhau, rollback không ảnh hưởng TX cha
SUPPORTSDùng TX nếu có, không tạo mới
NOT_SUPPORTEDChạy ngoài TX, suspend TX nếu có
MANDATORYBắt buộc phải có TX, throw exception nếu không
NEVERKhô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

  1. Implement money transfer giữa 2 tài khoản với @Transactional, đảm bảo atomicity. Test rollback khi số dư không đủ
  2. 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
  3. Tạo audit log service dùng REQUIRES_NEW propagation để log không bị mất khi transaction chính rollback