簡介
事務管理和快取是有助於確保資料完整性和提高應用程式效能的兩個重要功能。本文深入探討了其運作機制、配置以及生產中常用的模式。
1.@Transactional — 深入探討
1.1 作用機制
Spring使用AOP代理來管理事務。當方法存在時 @Transactional 被調用時,代理將:
Client → Proxy → begin TX → Target Method → commit/rollback TX
重要:呼叫方法
@Transactional來自同一個類別(自調用)的將不透過代理→事務不起作用。
@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 傳播(傳播交易)
| 傳播 | 描述 |
|---|---|
| 必要(預設) | 使用現有 TX 或建立新 |
| REQUIRES_NEW | 要求始終建立新的 TX,暫停舊的 TX |
| 嵌套 | 嵌套TX,回滾不影響父TX |
| 支援 | 如果可用,請使用 TX,不要建立新的 |
| 不支援 | 在 TX 之外運行,如果可用則暫停 TX |
| 強制 | 必須有TX,否則拋出例外 |
| 從來沒有 | 必須沒有TX,有則拋出異常 |
@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 隔離級別
@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 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 唯讀優化
// 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 快取抽象
2.1 啟用快取
@Configuration
@EnableCaching
public class CacheConfig { }
2.2 主要註釋
@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 設定咖啡因快取(記憶體中)
// 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快取(分散式)
# 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. 常用模式
3.1 快取旁路模式
Read: App → Cache (hit?) → Yes → return
No → DB → Update Cache → return
Write: App → DB → Evict Cache
3.2 條件緩存
// 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) { }
總結
- @Transactional 透過 AOP 代理程式管理事務 — 了解傳播、隔離、回溯規則
- 自呼叫不經過代理→交易不起作用,需要分離服務或使用自註入
- Spring Cache支援@Cacheable、@CachePut、@CacheEvict,方便從Caffeine(記憶體中)切換到Redis(分散式)
練習
1.透過@Transactional實現2個帳戶之間的轉賬,保證原子性。測試餘額不足時回滾 2. 為 ProductService 配置 Caffeine 緩存,TTL 為 10 分鐘,最多 500 個條目。測量快取前後的反應時間 3.使用REQUIRES_NEW傳播建立稽核日誌服務,以便主事務回滾時日誌不遺失