Postgres 中跨表的乐观并发控制
Optimistic concurrency control across tables in Postgres
我正在寻找一种方法来管理 Postgres 中多个 table 的乐观并发控制。我还试图将业务逻辑 排除在数据库之外 。我有一个 table 设置是这样的:
CREATE TABLE master
(
id SERIAL PRIMARY KEY NOT NULL,
status VARCHAR NOT NULL,
some_value INT NOT NULL,
row_version INT NOT NULL DEFAULT(1)
)
CREATE TABLE detail
(
id SERIAL PRIMARY KEY NOT NULL,
master_id INT NOT NULL REFERENCES master ON DELETE CASCADE ON UPDATE CASCADE,
some_data VARCHAR NOT NULL
)
每当更新行时,master.row_version
会自动由触发器递增。
客户端应用程序执行以下操作:
- 从
master
table 中读取一条记录。
- 根据记录的值计算一些业务逻辑,这可能包括涉及用户交互的几分钟延迟。
- 根据步骤 2 中的逻辑将记录插入
detail
table。
如果 master.row_version
的值自第 1 步读取记录后发生变化,我希望第 3 步被拒绝。乐观并发控制在我看来是正确的答案(唯一的答案?),但是我不确定如何像这样跨两个 table 管理它。
我正在考虑在 master
table 中的相关记录上使用 row-level lock 的 Postgres 函数可能是可行的方法。但我不确定这是否是我的 best/only 选项,或者它看起来像什么(我对 Postgres 语法有点陌生)。
鉴于客户端应用程序是用 C# 编写的,我正在使用 Npgsql。不知道里面有没有什么可以帮到我的?如果可能的话,我想避免使用函数,但我正在努力寻找一种方法来直接使用 SQL,并且匿名代码块(至少在 Npgsql 中)不支持 [=我需要 40=] 次操作。
如果您想使用乐观并发控制,则锁定已失效,请参阅主题 the Wikipedia article:
OCC assumes that multiple transactions can frequently complete without
interfering with each other. While running, transactions use data
resources without acquiring locks on those resources.
您可以使用更复杂的 INSERT
语句。
如果</code>是原来的<code>row_version
而</code>和<code>
是master_id
和some_data
要插入detail
,运行
WITH m(id) AS
(SELECT CASE WHEN master.row_version =
THEN
ELSE NULL
END
FROM master
WHERE master.id = )
INSERT INTO detail (master_id, some_data)
SELECT m.id, FROM m
如果 row_version
已更改,这将尝试将 NULL
作为 detail.id
插入,这将导致
ERROR: null value in column "id" violates not-null constraint
您可以将其转化为更有意义的错误消息。
从那以后我得出的结论是行锁可以用在 "typical" 悲观并发控制方法中,但是当与行版本结合使用时可以产生 "hybrid" 一些方法有意义的好处。
毫不奇怪,悲观、乐观或"hybrid"并发控制的选择取决于应用程序的需要。
悲观并发控制
典型的悲观并发控制方法可能如下所示。
- 开始数据库事务。
- 读取(并锁定)来自
master
table 的记录。
- 执行业务逻辑。
- 插入一条记录到
detail
table.
- 提交数据库事务。
如果第 3 步的业务逻辑是 long-运行,这种方法可能是不可取的,因为它会导致 long-运行 事务(通常不利),以及 long-运行 锁定 master
中的记录,否则可能会导致并发问题。
乐观并发控制
仅使用乐观并发控制的方法可能看起来更像这样。
- 从
master
table. 中读取记录(包括行版本)
- 执行业务逻辑。
- 开始数据库事务。
- 在
master
table(乐观并发控制检查)中记录的增量行版本。
- 插入一条记录到
detail
table.
- 提交数据库事务。
在这种情况下,数据库事务保持较短的时间,任何(隐式)行锁也是如此。 但是,master
table中记录的row version增量可能有点误导并发操作。想象一下这种情况下的几个并发操作,它们将开始在乐观并发检查上失败,因为行版本已经增加,即使记录上有意义的属性没有改变。
混合并发控制
"hybrid" 方法同时使用悲观锁定和(某种)乐观锁定,就像这样。
- 从
master
table. 中读取记录(包括行版本)
- 执行业务逻辑。
- 开始数据库事务。
- 根据其 ID 和 行版本从
master
table 重新读取记录(某种乐观并发控制检查)AND 锁定行。
- 插入一条记录到
detail
table.
- 提交数据库事务。
如果第4步获取记录失败,应该是乐观并发控制检查失败。该记录自第 1 步以来已更改,因此业务逻辑不再有效。
和典型的悲观并发控制场景一样,这里涉及到一个事务和一个显式的行锁,但是事务+锁的持续时间不再包括执行业务逻辑所需要的时间。
和乐观并发控制场景一样,记录需要版本。但不同之处在于版本没有更新,这意味着依赖于该行版本的其他操作不会受到影响。
混合方法示例
混合方法可能有利的示例:
一个博客有 post
table 和 comment
table。仅当 post.comments_locked
标志为 false
时,才能将评论添加到 post。添加评论的过程可以使用混合方法,确保用户可以并发添加评论而不会出现任何并发异常。
博客的所有者可以编辑他们的 post
,在这种情况下,可以采用传统的乐观并发控制方法。博客的所有者可以有一个漫长的运行 编辑过程,不会受到用户添加评论的影响。当 post
更新到数据库时,版本将增加,这意味着任何正在进行的评论添加操作都将失败,但可以通过重新获取 post
从数据库中记录并重试该过程。
我正在寻找一种方法来管理 Postgres 中多个 table 的乐观并发控制。我还试图将业务逻辑 排除在数据库之外 。我有一个 table 设置是这样的:
CREATE TABLE master
(
id SERIAL PRIMARY KEY NOT NULL,
status VARCHAR NOT NULL,
some_value INT NOT NULL,
row_version INT NOT NULL DEFAULT(1)
)
CREATE TABLE detail
(
id SERIAL PRIMARY KEY NOT NULL,
master_id INT NOT NULL REFERENCES master ON DELETE CASCADE ON UPDATE CASCADE,
some_data VARCHAR NOT NULL
)
每当更新行时,master.row_version
会自动由触发器递增。
客户端应用程序执行以下操作:
- 从
master
table 中读取一条记录。 - 根据记录的值计算一些业务逻辑,这可能包括涉及用户交互的几分钟延迟。
- 根据步骤 2 中的逻辑将记录插入
detail
table。
如果 master.row_version
的值自第 1 步读取记录后发生变化,我希望第 3 步被拒绝。乐观并发控制在我看来是正确的答案(唯一的答案?),但是我不确定如何像这样跨两个 table 管理它。
我正在考虑在 master
table 中的相关记录上使用 row-level lock 的 Postgres 函数可能是可行的方法。但我不确定这是否是我的 best/only 选项,或者它看起来像什么(我对 Postgres 语法有点陌生)。
鉴于客户端应用程序是用 C# 编写的,我正在使用 Npgsql。不知道里面有没有什么可以帮到我的?如果可能的话,我想避免使用函数,但我正在努力寻找一种方法来直接使用 SQL,并且匿名代码块(至少在 Npgsql 中)不支持 [=我需要 40=] 次操作。
如果您想使用乐观并发控制,则锁定已失效,请参阅主题 the Wikipedia article:
OCC assumes that multiple transactions can frequently complete without interfering with each other. While running, transactions use data resources without acquiring locks on those resources.
您可以使用更复杂的 INSERT
语句。
如果</code>是原来的<code>row_version
而</code>和<code>
是master_id
和some_data
要插入detail
,运行
WITH m(id) AS
(SELECT CASE WHEN master.row_version =
THEN
ELSE NULL
END
FROM master
WHERE master.id = )
INSERT INTO detail (master_id, some_data)
SELECT m.id, FROM m
如果 row_version
已更改,这将尝试将 NULL
作为 detail.id
插入,这将导致
ERROR: null value in column "id" violates not-null constraint
您可以将其转化为更有意义的错误消息。
从那以后我得出的结论是行锁可以用在 "typical" 悲观并发控制方法中,但是当与行版本结合使用时可以产生 "hybrid" 一些方法有意义的好处。
毫不奇怪,悲观、乐观或"hybrid"并发控制的选择取决于应用程序的需要。
悲观并发控制
典型的悲观并发控制方法可能如下所示。
- 开始数据库事务。
- 读取(并锁定)来自
master
table 的记录。 - 执行业务逻辑。
- 插入一条记录到
detail
table. - 提交数据库事务。
如果第 3 步的业务逻辑是 long-运行,这种方法可能是不可取的,因为它会导致 long-运行 事务(通常不利),以及 long-运行 锁定 master
中的记录,否则可能会导致并发问题。
乐观并发控制
仅使用乐观并发控制的方法可能看起来更像这样。
- 从
master
table. 中读取记录(包括行版本)
- 执行业务逻辑。
- 开始数据库事务。
- 在
master
table(乐观并发控制检查)中记录的增量行版本。 - 插入一条记录到
detail
table. - 提交数据库事务。
在这种情况下,数据库事务保持较短的时间,任何(隐式)行锁也是如此。 但是,master
table中记录的row version增量可能有点误导并发操作。想象一下这种情况下的几个并发操作,它们将开始在乐观并发检查上失败,因为行版本已经增加,即使记录上有意义的属性没有改变。
混合并发控制
"hybrid" 方法同时使用悲观锁定和(某种)乐观锁定,就像这样。
- 从
master
table. 中读取记录(包括行版本)
- 执行业务逻辑。
- 开始数据库事务。
- 根据其 ID 和 行版本从
master
table 重新读取记录(某种乐观并发控制检查)AND 锁定行。 - 插入一条记录到
detail
table. - 提交数据库事务。
如果第4步获取记录失败,应该是乐观并发控制检查失败。该记录自第 1 步以来已更改,因此业务逻辑不再有效。
和典型的悲观并发控制场景一样,这里涉及到一个事务和一个显式的行锁,但是事务+锁的持续时间不再包括执行业务逻辑所需要的时间。
和乐观并发控制场景一样,记录需要版本。但不同之处在于版本没有更新,这意味着依赖于该行版本的其他操作不会受到影响。
混合方法示例
混合方法可能有利的示例:
一个博客有 post
table 和 comment
table。仅当 post.comments_locked
标志为 false
时,才能将评论添加到 post。添加评论的过程可以使用混合方法,确保用户可以并发添加评论而不会出现任何并发异常。
博客的所有者可以编辑他们的 post
,在这种情况下,可以采用传统的乐观并发控制方法。博客的所有者可以有一个漫长的运行 编辑过程,不会受到用户添加评论的影响。当 post
更新到数据库时,版本将增加,这意味着任何正在进行的评论添加操作都将失败,但可以通过重新获取 post
从数据库中记录并重试该过程。