【问题标题】:locking rows on rails update to avoid collisions. (Postgres back end)锁定轨道上的行更新以避免冲突。 (Postgres 后端)
【发布时间】:2017-09-29 17:17:06
【问题描述】:

所以我的模型对象上有一个方法,当行中的二进制字段从 null 更新为 true 时,它​​会创建一个唯一的序列号。它的实现是这样的:

class AnswerHeader < ApplicationRecord
  before_save :update_survey_complete_sequence, if: :survey_complete_changed?

  def update_survey_complete_sequence
    maxval =AnswerHeader.maximum('survey_complete_sequence')
    self.survey_complete_sequence=maxval+1
  end
end

我的问题是我需要锁定什么,以便同时更新两行不会导致两行具有相同的survey_complete_sequence?

如果可以锁定单行而不是整个表,那就太好了,因为这是用户经常访问的表。

【问题讨论】:

  • 有什么原因不能创建 Postgres 序列并让数据库处理它?
  • 我可以猜到,但我不喜欢将应用程序逻辑分散到各处(即一些在 rails 应用程序中,一些在数据库本身中,等等)。似乎是一个令人头疼的维护问题。
  • 对任何有我不需要的答案的人来说,我真的需要一个“可靠的来源”,我只需要一个有效的解决方案。

标签: ruby-on-rails rails-activerecord rails-models


【解决方案1】:

如果您想在应用程序逻辑本身中处理此问题,而不是让数据库处理此问题。您使用 rails with_lock 函数,该函数将创建事务在所选行(在您的情况下为单行)上获取行级数据库锁

【讨论】:

    【解决方案2】:

    你需要锁定什么

    在您的情况下,您必须锁定包含最大 survey_complete_sequence 的行,因为这是每个查询在获取所需值时都会查找的行。

    maxval =AnswerHeader.maximum('survey_complete_sequence')
    

    是否可以锁定单行而不是整个表

    您的场景没有这样的特定锁。但是你可以使用 Postgresql 的SELECT FOR UPDATErow-level locking

    在没有实际操作的情况下获取行上的排他锁 修改行,使用 SELECT FOR UPDATE 选择行。

    您可以在 rails 中使用pessimistic locking 并指定您将使用哪个锁。

    调用 lock('some locking Clause') 以使用特定于数据库的锁定 您自己的子句,例如“LOCK IN SHARE MODE”或“FOR UPDATE NOWAIT”

    这是一个如何从 Rails 官方指南本身实现这一目标的示例

    Item.transaction do
      i = Item.lock("LOCK IN SHARE MODE").find(1)
      ...
    end
    

    使用锁的关系通常包装在事务中以防止出现死锁情况。

    所以你需要做的是 -

    1. SELECT FOR UPDATE 锁应用于包含maximum('survey_complete_sequence') 的行
    2. 从该行获取您需要的值
    3. 使用收到的值更新您的 AnswerHeader

    【讨论】:

    • 行锁帮不了他。即使该行被锁定,他的应用程序也会从该行或任何其他行读取 maxval。
    • 应用程序只有在锁成功的情况下才会获取值,当像AnswerHeader.lock('SELECT FOR UPDATE').maximum('survey_complete_sequence')这样使用时。你是在说别的吗?还是我说的不是正在发生的事情?
    • 因此,如果他锁定包含当前最大值的行 Y,这如何防止两个并行线程从 X 或 Y 读取此最大值并尝试创建具有相同最大值或最大值-1 的两个不同行或最大值+1?对不起,如果我没有正确理解。
    • 假设两个并发线程 X 和 Y 正在数据库中搜索最大值 survey_complete_sequence。现在,如果他们找到最大值,那么这两个线程将是同一行。因此,当他们都尝试读取该值时,首先他们尝试使用SELECT_FOR_UPDATE 模式锁定行。现在假设 X 首先获得了锁定,这意味着 Y 等待,直到锁定被释放,才应用 相同的锁定,这将阻止它读取它。所以现在 X 将完成剩下的工作。当事务完成时,X 释放锁,然后 Y 将完成它的工作。
    【解决方案3】:

    我相信你应该看看咨询锁。它确保相同的代码块不会同时在两台机器上执行,同时仍然保持表对其他业务开放。

    它使用数据库,但不会锁定您的表。

    您可以像这样使用名为“with_advisory_lock”的 gem:

    Model.with_advisory_lock("ADVISORY_LOCK_NAME") do
      # Your code
    end
    

    https://github.com/ClosureTree/with_advisory_lock

    它不适用于 SQLite。

    【讨论】:

      【解决方案4】:

      如果您使用的是 postgress,也许Sequenced 可以帮助您,而无需在数据库级别定义序列。

      survey_complete_sequence 是否应该是增量的?如果没有,也许随机化一个 bigint?

      【讨论】:

      • 他需要在每次更新表时增加的最大值。随机的 bigint 不会有帮助,也不会锁定行或整个表。
      【解决方案5】:

      您可能不想锁定表,即使您锁定了当前正在更新的行,您的 maxval 所基于的行也可供另一个更新读取并生成其序列#。

      除非您有一个巨大的表格并且每毫秒(大约数千)都有大量更新,否则这在现实生活中应该不是问题。但如果这个想法困扰您,您可以继续在“survey_complete_sequence”列的表中添加唯一索引。 DB 错误将传播到您可以在应用程序中处理的 Rails 异常。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-07-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-25
        • 2019-09-19
        相关资源
        最近更新 更多