У меня есть приложение, написанное с Java, Spring, Hibernate, Postgres.
Он отслеживает попытки входа пользователя в систему. Если пользователь имеет более 5 недопустимых попыток с одного и того же IP-адреса менее чем за 1 час - этот IP-адрес будет заблокирован.
У меня есть следующий класс сущностей для хранения информации о попытках входа в систему:
@Entity
public class BlockedIp {
private String ipAddress;
private Date blockDate;
private Date unblockDate;
private Date lastAttemptDate;
private Integer wrongAttemptsCount;
...}
Во-первых, когда приложение получает запрос на вход - он проверяет, заблокирован ли IP-адрес (blockDate! = Null). Затем в результате возвращается специальный код ответа пользователю.
Если приложение получает запрос на вход, а учетные данные неверны:
Если приложение получает запрос на вход и учетные данные верны - он сбрасывает неправильные значенияAttemptsCount на ноль, поэтому пользователь может сделать до 5 ошибок снова :)
Проблема заключается в том, что некоторые пользователи пытаются войти в систему одновременно с одного и того же IP-адреса. Например, wrongAttemptsCount = 4, поэтому пользователь может иметь только одну последнюю попытку. И у нас есть 3 входящих запроса, и все они имеют неправильные учетные данные. Теоретически, только первый запрос будет проходить, но два других будут заблокированы. На практике, конечно, все ошибаются. Атрибут "Захват" равен 4 от базы данных, поэтому все они будут обрабатываться как незаблокированные.
Итак, какие варианты я должен решить эту проблему и с минимальной потерей производительности, если это возможно? Я думал о инструкции SELECT FOR UPDATE для моего запроса, но мой коллега сказал, что это серьезно повлияет на производительность. Также он предложил посмотреть аннотацию @Version. Это действительно стоит? Это намного быстрее? Может быть, кто-то может предложить другие варианты?
Оптимистическая блокировка дает лучшую производительность, а также предотвращает потерянные обновления в многопользовательских цепочках и предотвращает обновление нескольких параллельных транзакций одной и той же строки без уведомления о новой версии строки.
Пройдет только одна транзакция, а другие получат исключение из состояния устаревших состояний. Вы должны понимать, что блокировки приобретаются, даже если вы явно не запрашиваете это. Каждая модифицированная строка занимает блокировку, а именно, что транзакционный буфер с последующей записью переносит переход состояния объекта в конец текущей транзакции.
SELECT FOR UPDATE
, как и любой пессимистический механизм блокировки, использует явные блокировки. Вы также можете использовать PESSIMISTIC_READ
для получения общей блокировки, так как PostgreSQL
поддерживает это тоже.
PESSIMISTIC_READ
не позволит другим транзакциям получить общую блокировку вашего объекта User
, но без оптимистической блокировки вы все равно можете иметь более 5 попыток с ошибкой. После того, как текущая транзакция освободит блокировку, другая конкурирующая транзакция приобретет недавно выпущенную блокировку и в любом случае сохранит новую неудачную логическую попытку. Это происходит для READ_COMMITTED
но это предотвращается с помощью REPEATABLE_READ или SERIALIZABLE, но увеличение уровня изоляции транзакций действительно может снизить масштабируемость приложения.
В целом, используйте оптимистичную блокировку и обработку исключений устаревшего состояния соответственно.
Version
(для этого в моей сущности ввести какое-то специальное поле), 2) перехватитьStaleStateException
во всех методах, которые сохраняют \ обновить эту сущность, 3) если возникнет такая исключительная ситуация - снова извлечь сущность чтобы получить его новое состояние и повторить все шаги моего алгоритма. Я прав?