更新表中关于另一个表上的约束的行

时间:2017-12-13 07:37:06

标签: postgresql transactions race-condition

book:
  id: primary key, integer
  title: varchar
  borrowed: boolean
  borrowed_by_user_id: foreign key user.id

user:
  id: primary key, integer
  name: varchar
  blocked: boolean

隔离级别是READ COMMITED,因为它是PostgreSQL中的默认级别(此要求不是来自我)。

我正在使用一个数据库事务来SELECT FOR UPDATE一本书,如果还没有借书的话,可以借给任何用户。这本书被选中FOR UPDATE,所以不能同时借用。

但还有另一个问题。我们不允许向被阻止的用户出借书籍。我们怎样才能确定?即使我们在开始时检查用户是否未被阻止,结果可能也不正确,因为并发事务可能会在检查后阻止用户

例如,管理员面板中的并发事务可以阻止用户。

如何解决这个问题?

  1. 我知道我可以使用SERIALIZABLE。它需要处理错误,是吗?
  2. 我不确定CHECK是如何工作的。你能谈谈更多吗?

1 个答案:

答案 0 :(得分:0)

这实际上是两个问题。

关于图书:

如果您在考虑借出书籍后立即用TestPropertyValues锁定该书,这就是“悲观锁定”的一个例子,并会阻止该书进行所有并发活动。

如果交易非常短,那很好 - 特别是,如果锁定和交易结束之间没有用户交互。

否则你应该使用“乐观锁定”。这可以通过以下几种方式完成:

  1. 使用SELECT ... FOR UPDATE事务隔离。然后更新自您读取数据后已修改的图书将导致序列化错误(请参阅末尾的注释)。

  2. 选择图书时,请记住系统列REPEATABLE READctid的值。然后更新如下:

    xmin

    如果没有更新行,有人必须在你查看之后修改过这本书。

  3. 关于用户:

    三个想法:

    1. 您使用UPDATE books SET ... WHERE id = ... AND ctid = original_ctid AND xmin = original_xmin; 事务隔离(请参阅末尾的说明)。

    2. 您在用户上维护一个包含用户借阅的图书数量的计数器。

      然后你可以有一个像

      这样的检查约束
      SERIALIZABLE

      这样的检查约束在每个语句的末尾进行评估,并且必须产生ALTER TABLE users ADD CHECK (NOT blocked OR books_borrowed = 0); ,否则会引发错误。

      因此,借用书籍的交易或阻止用户的交易必须失败(两个交易都必须修改用户)。

    3. 在将图书借给用户之前,您运行

      TRUE

      如果您获得TRUE,则中止交易,否则借出书籍。

      想要阻止用户的并发事务也需要SELECT blocked FROM users WHERE id = ... FOR UPDATE; 用户,检查是否有任何图书借给该用户。

      这样,就不会发生任何不一致:如果你想阻止一个用户,所有想要向用户出借书籍的并发交易必须要完成,以便你看到它们的效果,或者它们必须等到你完成阻止用户,然后他们将失败。

    4. 请注意更高的隔离级别:

      如果您以SELECT ... FOR UPDATEREPEATABLE READ的隔离级别运行事务,则可能会遇到序列化错误。这些不是程序中的错误,它们是正常的并且是预期的。如果遇到序列化错误,则必须回滚并再次尝试相同的事务。这是你不用担心竞争条件而付出的代价。