← Назад к вопросам

Можно ли реализовать кэш в базе данных?

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 кэша
Можно ли реализовать кэш в базе данных? | PrepBro