根据另一个 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
无法同时借阅
但是还有一个问题。我们不允许将书籍借给被阻止的用户。我们如何确定这一点?即使我们在开始时检查用户是否未被阻止,结果也可能不正确,因为并发事务可能会在检查 之后阻止用户。
例如,用户可能会被管理员面板的并发事务阻止。
如何解决这个问题?
- 我知道我可以使用
SERIALIZABLE
。它需要处理错误,是吗?
- 我不确定
CHECK
是如何工作的。能详细说一下吗?
这其实是两个问题。
关于书籍:
如果一考虑借出去就用SELECT ... FOR UPDATE
锁定这本书,这就是“悲观锁定”的例子,会阻塞所有并发activity的书。
如果事务很短,那很好——具体来说,如果在锁定和事务结束之间没有用户交互。
否则你应该使用“乐观锁定”。这可以通过多种方式完成:
使用REPEATABLE READ
事务隔离。然后更新一本在你读取它的数据后被修改过的书会导致序列化错误(见最后的注释)。
选择书籍时,记住系统栏ctid
和xmin
的值。然后更新如下:
UPDATE books SET ...
WHERE id = ...
AND ctid = original_ctid AND xmin = original_xmin;
如果没有行得到更新,那么在您查看本书后一定有人对其进行了修改。
关于用户:
三个想法:
你使用了SERIALIZABLE
事务隔离(见末尾注释)
您为用户维护一个计数器,其中包含用户借阅的书籍数量。
然后你可以有一个像
这样的检查约束
ALTER TABLE users ADD CHECK (NOT blocked OR books_borrowed = 0);
这样的检查约束在每个语句的末尾进行评估,并且必须产生 TRUE
,否则会抛出错误。
所以要么借书的交易要么屏蔽用户的交易都失败了(两个交易都必须修改用户)。
在将图书借给用户之前,您 运行
SELECT blocked FROM users WHERE id = ... FOR UPDATE;
如果得到 TRUE,则中止交易,否则将书借出。
想要阻止用户的并发事务也必须 SELECT ... FOR UPDATE
在用户上并且 只有这样 检查是否有任何书籍借给该用户。
这样,就不会发生不一致:如果你想阻止一个用户,所有想要借书给该用户的并发交易都必须完成,这样你才能看到它们的效果,或者它们必须等到您已完成阻止用户,他们将因此失败。
关于更高隔离级别的注意事项:
如果您 运行 隔离级别为 REPEATABLE READ
或 SERIALIZABLE
的事务,您可能会遇到 序列化错误 。这些不是您程序中的错误,它们是正常的并且是可以预料的。如果遇到序列化错误,则必须回滚并再次尝试相同的事务。这是您为不必担心竞争条件而付出的代价。
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
无法同时借阅
但是还有一个问题。我们不允许将书籍借给被阻止的用户。我们如何确定这一点?即使我们在开始时检查用户是否未被阻止,结果也可能不正确,因为并发事务可能会在检查 之后阻止用户。
例如,用户可能会被管理员面板的并发事务阻止。
如何解决这个问题?
- 我知道我可以使用
SERIALIZABLE
。它需要处理错误,是吗? - 我不确定
CHECK
是如何工作的。能详细说一下吗?
这其实是两个问题。
关于书籍:
如果一考虑借出去就用SELECT ... FOR UPDATE
锁定这本书,这就是“悲观锁定”的例子,会阻塞所有并发activity的书。
如果事务很短,那很好——具体来说,如果在锁定和事务结束之间没有用户交互。
否则你应该使用“乐观锁定”。这可以通过多种方式完成:
使用
REPEATABLE READ
事务隔离。然后更新一本在你读取它的数据后被修改过的书会导致序列化错误(见最后的注释)。选择书籍时,记住系统栏
ctid
和xmin
的值。然后更新如下:UPDATE books SET ... WHERE id = ... AND ctid = original_ctid AND xmin = original_xmin;
如果没有行得到更新,那么在您查看本书后一定有人对其进行了修改。
关于用户:
三个想法:
你使用了
SERIALIZABLE
事务隔离(见末尾注释)您为用户维护一个计数器,其中包含用户借阅的书籍数量。
然后你可以有一个像
这样的检查约束ALTER TABLE users ADD CHECK (NOT blocked OR books_borrowed = 0);
这样的检查约束在每个语句的末尾进行评估,并且必须产生
TRUE
,否则会抛出错误。所以要么借书的交易要么屏蔽用户的交易都失败了(两个交易都必须修改用户)。
在将图书借给用户之前,您 运行
SELECT blocked FROM users WHERE id = ... FOR UPDATE;
如果得到 TRUE,则中止交易,否则将书借出。
想要阻止用户的并发事务也必须
SELECT ... FOR UPDATE
在用户上并且 只有这样 检查是否有任何书籍借给该用户。这样,就不会发生不一致:如果你想阻止一个用户,所有想要借书给该用户的并发交易都必须完成,这样你才能看到它们的效果,或者它们必须等到您已完成阻止用户,他们将因此失败。
关于更高隔离级别的注意事项:
如果您 运行 隔离级别为 REPEATABLE READ
或 SERIALIZABLE
的事务,您可能会遇到 序列化错误 。这些不是您程序中的错误,它们是正常的并且是可以预料的。如果遇到序列化错误,则必须回滚并再次尝试相同的事务。这是您为不必担心竞争条件而付出的代价。