← Назад к вопросам
Можно ли реализовать кэш в базе данных?
2.2 Middle🔥 201 комментариев
#Базы данных и SQL#Кэширование и NoSQL
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Можно ли реализовать кэш в базе данных?
Краткий ответ
Да, можно и часто нужно. Кэш в БД (database-level caching) — это практика хранения часто запрашиваемых данных в отдельной таблице или структуре БД для снижения нагрузки и ускорения доступа. Однако это не замена памяти приложения (in-memory cache), а скорее дополнение.
Когда нужен кэш в БД
Сценарии использования:
- Распределённые системы — несколько инстансов приложения, один кэш для всех
- Долгоживущие данные — кэш должен пережить перезагрузку приложения
- Данные огромного объёма — которые не влезут в памяти приложения
- Необходимость персистентности — когда потеря кэша недопустима
- Синхронизация между сервисами — кэш как единая система истины
Архитектура кэширования
Трёхуровневая схема:
┌─────────────────┐
│ Application │ Level 1: In-Memory Cache (Redis, Caffeine)
│ Memory Cache │ Самый быстрый, но местный для одного инстанса
└────────┬────────┘
│ (miss)
↓
┌─────────────────┐
│ Distributed │ Level 2: Distributed Cache (Redis, Memcached)
│ Cache (Redis) │ Быстро, но требует сетевой задержки
└────────┬────────┘
│ (miss)
↓
┌─────────────────┐
│ Database │ Level 3: Database (PostgreSQL, MySQL)
│ Cache Table │ Медленнее, но гарантированно есть
└────────┬────────┘
│ (miss)
↓
┌─────────────────┐
│ Original DB │ Level 4: Source of Truth
│ Main Data │ Самый медленный, но основной источник
└─────────────────┘
Способ 1: Таблица кэша в БД
Простая реализация:
// Модель кэша
@Entity
@Table(name = "cache_data")
public class CacheEntry {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(nullable = false, unique = true)
private String cacheKey;
@Column(columnDefinition = "TEXT")
private String cachedValue;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "expires_at")
private LocalDateTime expiresAt;
@Column(name = "hit_count")
private Integer hitCount = 0;
}
// Репозиторий
@Repository
public interface CacheEntryRepository extends JpaRepository<CacheEntry, String> {
Optional<CacheEntry> findByCacheKey(String cacheKey);
}
// Сервис
@Service
public class DatabaseCacheService {
@Autowired
private CacheEntryRepository cacheRepository;
public Optional<String> get(String key) {
return cacheRepository.findByCacheKey(key)
.filter(entry -> entry.getExpiresAt().isAfter(LocalDateTime.now()))
.map(entry -> {
entry.setHitCount(entry.getHitCount() + 1);
cacheRepository.save(entry);
return entry.getCachedValue();
});
}
public void put(String key, String value, Duration ttl) {
CacheEntry entry = cacheRepository.findByCacheKey(key)
.orElse(new CacheEntry());
entry.setCacheKey(key);
entry.setCachedValue(value);
entry.setCreatedAt(LocalDateTime.now());
entry.setExpiresAt(LocalDateTime.now().plus(ttl));
entry.setHitCount(0);
cacheRepository.save(entry);
}
public void invalidate(String key) {
cacheRepository.findByCacheKey(key).ifPresent(cacheRepository::delete);
}
}
Способ 2: Кэш с TTL (Time-To-Live)
С автоматическим истечением:
@Service
public class CachedUserService {
@Autowired
private UserRepository userRepository;
@Autowired
private CacheRepository cacheRepository;
private static final Duration CACHE_TTL = Duration.ofMinutes(15);
public User getUserWithCache(Long userId) {
String cacheKey = "user_" + userId;
// Пытаемся достать из кэша
Optional<String> cached = cacheRepository.findValidCache(cacheKey);
if (cached.isPresent()) {
return jsonDeserialize(cached.get(), User.class);
}
// Достаём из основной БД
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
// Кэшируем результат
cacheRepository.save(
cacheKey,
jsonSerialize(user),
CACHE_TTL
);
return user;
}
}
@Repository
public interface CacheRepository extends JpaRepository<CacheEntry, String> {
@Query("SELECT c.value FROM CacheEntry c WHERE c.key = ?1 AND c.expiresAt > CURRENT_TIMESTAMP")
Optional<String> findValidCache(String key);
@Modifying
@Query("DELETE FROM CacheEntry WHERE expiresAt <= CURRENT_TIMESTAMP")
void deleteExpiredEntries();
}
Способ 3: Кэш результатов запросов
С использованием аннотаций Spring:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Cacheable(
value = "products",
key = "#id",
unless = "#result == null",
cacheManager = "databaseCacheManager"
)
public Product getProduct(Long id) {
System.out.println("Запрос в БД для product: " + id);
return productRepository.findById(id).orElse(null);
}
@Cacheable(
value = "productsByCategory",
key = "#category",
cacheManager = "databaseCacheManager"
)
public List<Product> getProductsByCategory(String category) {
return productRepository.findByCategory(category);
}
@CacheEvict(value = "products", key = "#id")
public void updateProduct(Long id, Product product) {
productRepository.save(product);
}
@CacheEvict(value = {"products", "productsByCategory"}, allEntries = true)
public void clearAllCache() {
System.out.println("Кэш очищен");
}
}
Способ 4: Materialised View (в некоторых БД)
PostgreSQL пример:
-- Создаём материализованное представление
CREATE MATERIALIZED VIEW user_statistics_cache AS
SELECT
u.id,
u.username,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent,
MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;
-- Индекс для быстрого поиска
CREATE INDEX idx_user_stats_id ON user_statistics_cache(id);
-- Обновляем кэш когда нужно
REFRESH MATERIALIZED VIEW CONCURRENTLY user_statistics_cache;
Использование в Java:
@Repository
public interface UserStatisticsRepository extends JpaRepository<UserStatisticsView, Long> {
Optional<UserStatisticsView> findByUsername(String username);
}
@Service
public class UserAnalyticsService {
@Autowired
private UserStatisticsRepository statisticsRepository;
public UserStatistics getUserStats(String username) {
return statisticsRepository.findByUsername(username)
.map(view -> new UserStatistics(
view.getId(),
view.getOrderCount(),
view.getTotalSpent()
))
.orElse(null);
}
}
Способ 5: Гибридный подход (БД + In-Memory)
@Service
public class HybridCacheService {
@Autowired
private CacheRepository dbCache;
private final ConcurrentHashMap<String, CacheValue> memoryCache =
new ConcurrentHashMap<>();
public Optional<String> get(String key) {
// Уровень 1: Память
CacheValue cached = memoryCache.get(key);
if (cached != null && cached.isValid()) {
return Optional.of(cached.getValue());
}
// Уровень 2: БД
Optional<String> dbCached = dbCache.findValidCache(key);
if (dbCached.isPresent()) {
// Кэшируем в памяти
memoryCache.put(key, new CacheValue(
dbCached.get(),
System.currentTimeMillis() + 300000 // 5 минут
));
return dbCached;
}
return Optional.empty();
}
public void put(String key, String value) {
// Сохраняем и в БД, и в памяти
memoryCache.put(key, new CacheValue(
value,
System.currentTimeMillis() + 300000
));
dbCache.save(key, value);
}
private static class CacheValue {
private final String value;
private final long expiresAt;
CacheValue(String value, long expiresAt) {
this.value = value;
this.expiresAt = expiresAt;
}
boolean isValid() {
return System.currentTimeMillis() < expiresAt;
}
String getValue() {
return value;
}
}
}
Сравнение подходов
| Подход | Скорость | Персистентность | Синхронизация | Сложность |
|---|---|---|---|---|
| In-Memory (Caffeine) | ⭐⭐⭐⭐⭐ | ❌ | ❌ | Низкая |
| Redis | ⭐⭐⭐⭐ | ⚠️ | ✅ | Средняя |
| Database Cache Table | ⭐⭐⭐ | ✅ | ✅ | Средняя |
| Materialized View | ⭐⭐⭐ | ✅ | ⚠️ (ручное обновление) | Высокая |
| Hybrid (DB + Memory) | ⭐⭐⭐⭐ | ✅ | ✅ | Высокая |
Важные моменты
1. Управление версионированием кэша:
@Entity
public class CacheEntry {
// ...
@Version
private Long version; // Оптимистичная блокировка
}
2. Очистка устаревшего кэша:
@Scheduled(fixedDelay = 60000) // каждую минуту
public void cleanExpiredCache() {
cacheRepository.deleteExpiredEntries();
logger.info("Expired cache entries deleted");
}
3. Мониторинг использования кэша:
long hitRate = hitCount / totalRequests * 100;
if (hitRate < 50) {
logger.warn("Cache hit rate too low: " + hitRate + "%");
}
Заключение
Кэш в БД полезен, когда:
- Нужна синхронизация между несколькими инстансами
- Данные должны пережить перезагрузку приложения
- Требуется персистентность и надёжность
- В БД есть встроенные механизмы (Materialized View)
Но помни:
- БД медленнее памяти приложения
- Лучше использовать гибридный подход (память + БД)
- Правильное управление TTL критично
- Мониторь hit rate кэша