使用 Hibernate 和 Postgres 的并发实体 read/update

Concurrent entities read/update with Hibernate and Postgres

我有一个使用 Java、Spring、Hibernate、Postgres 编写的应用程序。

它跟踪用户的登录尝试。如果用户在不到 1 小时内从同一 IP 地址进行了 5 次以上的无效尝试 - 此 IP 地址将被阻止。

我有以下实体 class 用于存储有关登录尝试的信息:

@Entity
public class BlockedIp {

private String ipAddress;
private Date blockDate;
private Date unblockDate;
private Date lastAttemptDate;
private Integer wrongAttemptsCount;
...}

首先,当应用收到登录请求时 - 它会检查 IP 地址是否已被阻止 (blockDate != null)。然后在 returns 特殊响应代码给用户。

如果应用收到登录请求且凭据错误:

如果应用收到登录请求且凭据正确 - 它会将 wrongAttemptsCount 重置为零,因此用户最多可以再次犯 5 次错误:)

问题是当一些用户尝试从同一个 IP 同时登录时。 例如,wrongAttemptsCount = 4,因此用户只能进行最后一次尝试。我们有 3 个传入请求,它们都使用了错误的凭据。理论上,只有第一个请求会通过,但另外两个请求会被阻止。实际上,当然,所有从数据库中得到的wrongAttemptsCount等于4,所以它们都将被处理为非阻塞。

那么,如果可能的话,我有哪些选择可以解决这个问题并且性能损失最小? 我为我的查询考虑了 SELECT FOR UPDATE 语句,但我的同事说这会严重影响性能。 他还提出要查​​看@Version 注释。是不是真的值得吗?是不是快多了? 也许有人可以提供其他选择?

乐观锁定产生最好的性能,也prevents lost updates in multi-request conversations,它会防止多个并发事务在没有通知新行版本的情况下更新同一行。

只有一项交易会通过,其他交易会出现陈旧状态异常。您必须了解 locks are acquired even if you don't explicitly request it so. Every modified row takes a lock, it's just that the transactional write-behind cache post-将实体状态转换到当前事务的末尾。

SELECT FOR UPDATE,与任何 pessimistic locking mechanism 一样采用显式锁定。您还可以使用 PESSIMISTIC_READ 获取共享锁,因为 PostgreSQL 也支持。

PESSIMISTIC_READ 会阻止其他事务获取你的 User 实体上的共享锁,但如果没有乐观锁定,你仍然可以有 5 次以上的失败尝试。当前事务释放锁后,其他竞争事务将获取新释放的锁并保存新的失败逻辑尝试。 READ_COMMITTED 会发生这种情况,但 REPEATABLE_READ or SERIALIZABLE 会阻止这种情况,但提高事务隔离级别确实会降低应用程序的可扩展性。

总而言之,使用乐观锁定并相应地处理陈旧状态异常。