はじめに
トランザクション管理とキャッシュは、データの整合性を確保し、アプリケーションのパフォーマンスを向上させる 2 つの重要な機能です。この記事では、運用メカニズム、構成、および運用環境で一般的に使用されるパターンについて詳しく説明します。
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 を使用するか、新しい TX を作成します。 |
| REQUIRES_NEW | 常に新しい TX を作成し、古い 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 (分散) への切り替えが簡単です
演習
- @Transactional を使用して 2 つのアカウント間の送金を実装し、原子性を確保します。残高不足時のロールバックをテストする
- ProductService のカフェイン キャッシュを TTL 10 分、最大 500 エントリで構成します。キャッシュの前後の応答時間を測定する
- メイン トランザクションがロールバックするときにログが失われないように、REQUIRES_NEW 伝播を使用して監査ログ サービスを作成します。