【问题标题】:Form submissions lose idempotency when submitted rapidly in a row表单提交在连续快速提交时失去幂等性
【发布时间】: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


【解决方案1】:

只需使用Rack::Throttle 而不是重新发明轮子。

# config/application.rb
require 'rack/throttle'

class Application < Rails::Application
  config.middleware.use Rack::Throttle::Interval
end

这会在来自同一客户端的请求之间应用 1 秒的最小间隔。您可以使用min: 选项对其进行自定义。

这可以与javascript debouncing/throttling 结合使用,这将进一步减少您的服务器(和客户端)的负载。

我在本地主机上使用一个简单的 Puma 3.7 服务器。我在 印象是大多数服务器会收到一个请求,执行它,然后 继续下一个请求,但这几乎就像有某种类型的 并行处理。

Puma 是多线程的。虽然您可以将其配置为单线程,但 Ruby 主要是阻塞 IO,因此单线程 Web 服务器在负载下表现不佳。

我的问题是:这怎么可能?每个后续记录都有一个 ID 值比上一条记录大一,所以它不像 服务器不知道以前的记录。那么,如果 请求发送非常快,唯一的哈希要求是 忽略?

比赛条件。您正在做的是应用程序级别的唯一性验证,您可以在其中对数据库运行查询,然后在数据库中插入一行。

如果两个(或更多)进程同时响应重复请求,则两者都将通过 Submission.find_by(hash: params[:hash]) 查询获得绿灯,然后继续插入记录。

这是任何语言/框架中的well known issue with application validations,可以通过在数据库中添加唯一索引来解决,这将导致数据库拒绝重复的插入语句。这将在 Rails 中引发数据库驱动程序错误。

但我仍然认为这应该在中间件级别处理。

【讨论】:

    猜你喜欢
    • 2019-07-23
    • 2017-06-07
    • 2014-12-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-02
    • 2013-07-17
    • 2012-06-19
    • 1970-01-01
    相关资源
    最近更新 更多