根据另一个 table 的约束更新 table 中的一行

Update a row in a table respecting a constraint on another table

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 是如何工作的。能详细说一下吗?

这其实是两个问题。

关于书籍:

如果一考虑借出去就用SELECT ... FOR UPDATE锁定这本书,这就是“悲观锁定”的例子,会阻塞所有并发activity的书。

如果事务很短,那很好——具体来说,如果在锁定和事务结束之间没有用户交互。

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

  1. 使用REPEATABLE READ事务隔离。然后更新一本在你读取它的数据后被修改过的书会导致序列化错误(见最后的注释)。

  2. 选择书籍时,记住系统栏ctidxmin的值。然后更新如下:

    UPDATE books SET ...
    WHERE id = ...
      AND ctid = original_ctid AND xmin = original_xmin;
    

    如果没有行得到更新,那么在您查看本书后一定有人对其进行了修改。

关于用户:

三个想法:

  1. 你使用了SERIALIZABLE事务隔离(见末尾注释)

  2. 您为用户维护一个计数器,其中包含用户借阅的书籍数量。

    然后你可以有一个像

    这样的检查约束
    ALTER TABLE users ADD CHECK (NOT blocked OR books_borrowed = 0);
    

    这样的检查约束在每个语句的末尾进行评估,并且必须产生 TRUE,否则会抛出错误。

    所以要么借书的交易要么屏蔽用户的交易都失败了(两个交易都必须修改用户)。

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

    SELECT blocked FROM users WHERE id = ... FOR UPDATE;
    

    如果得到 TRUE,则中止交易,否则将书借出。

    想要阻止用户的并发事务也必须 SELECT ... FOR UPDATE 在用户上并且 只有这样 检查是否有任何书籍借给该用户。

    这样,就不会发生不一致:如果你想阻止一个用户,所有想要借书给该用户的并发交易都必须完成,这样你才能看到它们的效果,或者它们必须等到您已完成阻止用户,他们将因此失败。

关于更高隔离级别的注意事项:

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