Ответ
Прямое чтение-изменение-запись в конкурентной среде ведет к потере обновлений (race condition).
Проблемный подход (на примере JPA):
@Transactional
public void incrementViews(Long articleId) {
Article article = articleRepository.findById(articleId).orElseThrow();
article.setViews(article.getViews() + 1); // Значение может быть устаревшим
articleRepository.save(article);
}
Корректные решения:
-
Атомарное обновление на уровне БД (Наиболее надежное):
@Modifying @Query("UPDATE Article a SET a.views = a.views + 1 WHERE a.id = :id") void incrementViews(@Param("id") Long articleId);База данных гарантирует атомарность этой операции.
-
Оптимистичная блокировка (Optimistic Locking):
@Entity public class Article { @Id private Long id; private Long views; @Version // Добавляет версию для контроля изменений private Integer version; }При конфликте JPA выбросит
OptimisticLockException. -
Использование внешнего хранилища (Redis):
INCR article:views:{articleId}Команда
INCRв Redis атомарна и высокопроизводительна. -
Пессимистичная блокировка (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.