【发布时间】:2020-02-17 02:21:03
【问题描述】:
我遇到了一个问题,人们双击我的表单提交按钮并提交了多次,所以我只想允许一次提交。我的第一个想法是 javascript,但这不是绝对即时的,我想要 100% 保证工作的东西。
我的解决方案是使提交具有幂等性,通过为每个表单加载其自己的哈希,并在每次提交时检查该哈希是否已经存在,如果存在,则不执行任何操作。
这就是我的代码的要点:
#form
@hash = SecureRandom.urlsafe_base64(20)
f.hidden_field :hash, value: @hash
f.submit "submit"
#controller
existing = Submission.find_by(hash: params[:hash])
if existing.nil?
#enter new Submission into the database with hash: params[:hash]
如果我在提交之间留出一些时间,这会起作用,但是当我双击提交按钮时,两条记录会输入到我的数据库中,具有相同的哈希值。
我在本地主机上使用一个简单的 Puma 3.7 服务器。我的印象是大多数服务器会收到一个请求,执行它,然后继续下一个请求,但这几乎就像是在进行某种类型的并行。
我的问题是:这怎么可能?每个后续记录的 ID 值都比前一个记录大一,所以服务器并不知道前一个记录。那么,如果请求发送得非常快,那么如何忽略唯一哈希要求呢?同样,如果我稍后再尝试使用相同的哈希,则不会发生任何事情,正如预期的那样。
【问题讨论】:
-
"我在 localhost 上使用一个简单的 Puma 3.7 服务器。我的印象是大多数服务器会收到一个请求,执行它,然后继续下一个请求,但这几乎就像有某种类型的并行性正在发生。”这正是正在发生的事情。 Puma 不是单线程的。
-
@max 显然,但有趣的是,为什么幂等检查机制在允许插入时失败,这些插入具有相同的 id/hash-code,似乎在保持数据库一致方面存在缺陷“跨越”所有参与线程(好像脏缓存/脏数据库数据标志传播无法保持所有个人的数据一致,本质上是顺序的,原子数据库检查?)。顺便说一句,像糟糕的鼠标按钮这样的小问题可以简单地触发几次 [submit] 事件,只需一次人工点击,所以 Alea acta est
-
@user3666197 我认为这种方法失败的主要原因是竞争条件。考虑如果您有两个相隔一毫秒的请求 - 第一个请求运行
existing = Submission.find_by(hash: params[:hash]),但在它插入记录之前,第二个请求也执行相同的查询并且找不到任何记录。然后两个进程都插入一条记录。这是应用程序验证的一个众所周知的问题,可以通过在数据库中添加唯一索引或使用事务来解决。 thoughtbot.com/blog/the-perils-of-uniqueness-validations -
@max 恕我直言,对于正确的事务处理应该零怀疑,在需要的地方使用显式锁。微秒不是错误制定测试的借口。事务模式更新处理,在必要时锁定,保护数据库内容的一致性,不是吗?因此,先到先得,1) 锁定数据库,2) 测试,3) 更新,当且仅当目前不存在时,4) 释放锁定 - 这就是使 RDMS 数据一致和事务安全的原因(没有非法注入 DUPes)。在这里,由于 multi-[submit]-s 的性质,一个测试可以锁定但一个包含最新数据的迷你表
-
@user3666197 不确定我是否完全理解这一点。锁定不会涉及锁定整个表并具有阻止任何其他进程插入非重复数据的讨厌的副作用吗?
标签: ruby-on-rails ruby server puma concurrent-processing