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

Lesson 13: Transaction Management & Caching

@Transactional deep dive — propagation, isolation levels, rollback rules. Spring Cache abstraction with Caffeine and Redis. Cache eviction strategies.

💻 Programming — Lesson 12 Lesson 13: Transaction Management & Caching

Spring Boot 4: From Basics to Advanced

Part 4: Advanced Features

xdev.asia

Introduction

Transaction management and caching are two important features that help ensure data integrity and improve application performance. This article delves into the operating mechanism, configuration and commonly used patterns in production.


1. @Transactional — Deep Dive

1.1 Mechanism of action

Spring uses AOP Proxy to manage transactions. When method exists @Transactional is called, the proxy will:

Client → Proxy → begin TX → Target Method → commit/rollback TX

Important: Call method @Transactional from within the same class (self-invocation) will not go through the proxy → transactions do not work.

@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 (Spread Transaction)

PropagationDescription
REQUIRED (default)Use existing TX or create new
REQUIRES_NEWAlways create new TX, suspend old TX
NESTEDNested TX, rollback does not affect parent TX
SUPPORTSUse TX if available, do not create new
NOT_SUPPORTEDRun outside TX, suspend TX if available
MANDATORYMust have TX, throw exception otherwise
NEVERThere must be no TX, throw exception if there is
@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 Enable Cache

@Configuration
@EnableCaching
public class CacheConfig { }

2.2 Main Annotations

@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 Configuring 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. Commonly used patterns

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) { }

Summary

  • @Transactional manages transactions via AOP proxy — understand propagation, isolation, rollback rules
  • Self-invocation does not go through a proxy → transactions do not work, need to separate services or use self-injection
  • Spring Cache supports @Cacheable, @CachePut, @CacheEvict, easy to switch from Caffeine (in-memory) to Redis (distributed)

Exercises

  1. Implement money transfer between 2 accounts with @Transactional, ensuring atomicity. Test rollback when balance is insufficient
  2. Configure Caffeine cache for ProductService with TTL 10 minutes, max 500 entries. Measure response time before and after cache
  3. Create an audit log service using REQUIRES_NEW propagation so that the log is not lost when the main transaction rolls back