Summary
- In real systems:
- 80% → Cache-Aside + Eviction
- High-scale → Add these:
- Stampede protection
- Two-level cache
- Event invalidation
- Spring mainly supports:
- Cache-Aside (natively)
- Partial Write-Through
- Eviction patterns
- @Cacheable, @CachePut, @CacheEvict are mainly Cache-Aside tools
- Advanced patterns require custom logic or cache provider features
- High-scale systems often combine:
- Cache-Aside + Eviction
- Two-Level Cache
- Stampede Protection
- Event-Driven Invalidation
- Spring annotations alone are not enough for advanced caching—you end up:
- Using Caffeine / Redis features directly
- Or writing custom cache layers
Read-Heavy Strategies
- Cache-Aside - Implemented by App
- Read-Through - Implemented by Cache Provider
- Refresh-Ahead - Implemented by Cache Provider
Write-Heavy Strategies
- Write-Through - Implemented by Cache Provider
- Write-Behind (aka Write-Back) - Implemented by Cache Provider
- Write-Around - Implemented by App
1. Cache-Aside (Lazy Loading)
App reads from cache → if miss → load from DB → put in cache. Cache is not responsible for loading; application does it.
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow();
}
}
2. Write-Through
Write goes to cache and DB synchronously. Cache always up-to-date.
@CachePut(value = "users", key = "#user.id")
public User saveUser(User user) {
return userRepository.save(user);
}
3. Read-Through
Cache itself loads data (app doesn’t call DB directly). App only talks to cache provider. Cache abstracts loading logic. Provider like Hazelcast / Redis with loader.
4. Write-Behind
Write goes to cache → DB updated asynchronously later. Very fast writes.
public void saveUser(User user) {
cache.put(user.getId(), user);
asyncExecutor.submit(() -> {
userRepository.save(user);
});
}
5. Refresh-Ahead
Cache refreshes entries before expiration to avoid cache miss spikes. Not supported via Spring annotations.
Caffeine.newBuilder()
.refreshAfterWrite(Duration.ofMinutes(5))
.build(key -> loadFromDb(key));
6. Cache Eviction / Invalidation
Explicitly remove/update cache when data changes.
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
7. Write-Around
Writes go directly to DB, cache updated only on read. Prevents cache from being updated on writes. Cache becomes stale by design. Relies on future reads to populate.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CacheManager cacheManager;
public void createOrder(Order order) {
orderRepository.save(order); // cache not updated
}
@Cacheable(value = "userOrders", key = "#userId")
public List getOrdersForUser(Long userId) {
return orderRepository.findByUserId(userId);
}
}
8. Negative Caching Control
Cache “not found” results. Example: user not found → cache null. Prevents repeated DB hits. Key insight: unless="#result == null" avoids caching null values.
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
9. Two-Level Cache
L1 (in-memory) + L2 (distributed like Redis). L1: Caffeine, L2: Redis. Must combine manually.
10. Cache Stampede Protection (Önbellek yığılması)
Prevent many threads from hitting DB on same miss. Only one thread fetches DB; others wait or use cache.
11. Read-Repair
If stale data detected → fix cache during read. Not supported via @Cacheable.
public User getUser(Long id) {
User cached = cache.get(id);
if (cached != null && isStale(cached)) {
User fresh = userRepository.findById(id).orElse(null);
cache.put(id, fresh); // repair
return fresh;
}
if (cached != null) {
return cached;
}
User fresh = userRepository.findById(id).orElse(null);
cache.put(id, fresh);
return fresh;
}
12. Event-Driven Cache Invalidation
Use events (Kafka, etc.) to invalidate/update cache entries.
Hiç yorum yok:
Yorum Gönder