Как корректно реализовать увеличение счетчика просмотров статьи в условиях высокой конкурентности?

«Как корректно реализовать увеличение счетчика просмотров статьи в условиях высокой конкурентности?» — вопрос из категории Базы данных, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Прямое чтение-изменение-запись в конкурентной среде ведет к потере обновлений (race condition).

Проблемный подход (на примере JPA):

@Transactional
public void incrementViews(Long articleId) {
    Article article = articleRepository.findById(articleId).orElseThrow();
    article.setViews(article.getViews() + 1); // Значение может быть устаревшим
    articleRepository.save(article);
}

Корректные решения:

  1. Атомарное обновление на уровне БД (Наиболее надежное):

    @Modifying
    @Query("UPDATE Article a SET a.views = a.views + 1 WHERE a.id = :id")
    void incrementViews(@Param("id") Long articleId);

    База данных гарантирует атомарность этой операции.

  2. Оптимистичная блокировка (Optimistic Locking):

    @Entity
    public class Article {
        @Id
        private Long id;
        private Long views;
        @Version // Добавляет версию для контроля изменений
        private Integer version;
    }

    При конфликте JPA выбросит OptimisticLockException.

  3. Использование внешнего хранилища (Redis):

    INCR article:views:{articleId}

    Команда INCR в Redis атомарна и высокопроизводительна.

  4. Пессимистичная блокировка (Pessimistic Locking):

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Article a WHERE a.id = :id")
    Article findByIdForUpdate(@Param("id") Long articleId);

    Используется SELECT ... FOR UPDATE. Эффективно, но может снижать пропускную способность.

Рекомендация: Для счетчиков предпочтительны атомарные операции в БД или Redis.