管理数据库并发性的最佳方式?
Best way of managing database concurrency?
我在处理并发时遇到了问题。
在下面的示例中,两个用户 A 和 B 编辑同一张发票并对其进行不同的更改。如果他们两个同时点击保存我希望其中一个成功,另一个失败。否则生成的发票将是不受欢迎的 "merged invoice"。
这是在 PostgreSQL 中测试的示例(但我认为这个问题应该与数据库无关):
create table invoice (
id int primary key not null,
created date
);
create table invoice_line (
invoice_id int,
line numeric(6),
amount numeric(10,2),
constraint fk_invoice foreign key (invoice_id) references invoice(id)
);
insert into invoice(id, created) values (123, '2018-03-17');
insert into invoice_line (invoice_id, line, amount) values (123, 1, 24);
insert into invoice_line (invoice_id, line, amount) values (123, 2, 26);
所以发票的初始行是:
invoice_id line amount
---------- ---- ------
123 1 24
123 2 26
现在,用户 A 编辑发票,删除第 2 行并单击“保存”:
-- transaction begins
set transaction isolation level serializable;
select * from invoice where id = 123; -- #1 will it block the other thread?
delete invoice_line where invoice_id = 123 and line = 2;
commit; -- User A would expect the invoice to only include line 1.
同时用户 B 编辑发票并添加第 3 行,然后单击保存:
-- transaction begins
set transaction isolation level serializable;
select * from invoice where id = 123; -- #2 will this wait the other thread?
insert into invoice_line (invoice_id, line, amount) values (123, 3, 45);
commit; -- User B would expect the invoice to include lines 1, 2, and 3.
不幸的是,两个事务都成功了,我得到了合并的行(损坏状态):
invoice_id line amount
---------- ---- ------
123 1 24
123 3 45
既然这不是我想要的,我有什么选择来控制并发?
这不是数据库并发问题。数据库的 ACID 属性与完成事务有关,同时保持数据库的完整性。在您描述的情况下,交易是正确的,数据库正在正确处理它们。
你想要的是一种锁定机制,本质上是一种信号量,保证在任何时候只有一个用户可以对数据进行写访问。您可能能够依赖数据库锁定机制,在锁定失败时进行捕获。
但是,我建议使用其他两种方法中的一种。如果您对仅在应用程序逻辑中进行的更改感到满意,那么将锁定机制放在那里。有一个用户可以 "lock" table 或记录的地方;那就不要让任何人碰它。
你可以更进一步。您可以要求用户获得 "ownership" 的 table 以进行更改。然后你可以实现一个失败的触发器,除非用户是进行更改的人。
而且,您可能会想到其他解决方案。我真正想指出的是,您的 use-case 超出了 RDBMS 默认执行的范围(因为它们会让两个事务都成功完成)。因此,您需要为任何数据库(我熟悉的)添加额外的逻辑。
一般而言,发票行项目不应在过帐后进行编辑或删除。如果客户需要冲销费用,通常的做法是添加一个记入金额的新交易,可能带有一个 cross-reference 字段,其中包含要冲销的行项目的 ID。这种方法的优点是 (1) 您可以修改客户的余额,而无需返回并重新预订任何先前的报表期间,并且 (2) 您不会 运行 陷入像这样的并发问题,这很难解决。
如果发票尚未过帐,您仍然不允许编辑行项目。相反,您将取消之前的发票并创建一个包含所有新行项目的新发票。这再次避免了手头的并发问题。
我在处理并发时遇到了问题。
在下面的示例中,两个用户 A 和 B 编辑同一张发票并对其进行不同的更改。如果他们两个同时点击保存我希望其中一个成功,另一个失败。否则生成的发票将是不受欢迎的 "merged invoice"。
这是在 PostgreSQL 中测试的示例(但我认为这个问题应该与数据库无关):
create table invoice (
id int primary key not null,
created date
);
create table invoice_line (
invoice_id int,
line numeric(6),
amount numeric(10,2),
constraint fk_invoice foreign key (invoice_id) references invoice(id)
);
insert into invoice(id, created) values (123, '2018-03-17');
insert into invoice_line (invoice_id, line, amount) values (123, 1, 24);
insert into invoice_line (invoice_id, line, amount) values (123, 2, 26);
所以发票的初始行是:
invoice_id line amount
---------- ---- ------
123 1 24
123 2 26
现在,用户 A 编辑发票,删除第 2 行并单击“保存”:
-- transaction begins
set transaction isolation level serializable;
select * from invoice where id = 123; -- #1 will it block the other thread?
delete invoice_line where invoice_id = 123 and line = 2;
commit; -- User A would expect the invoice to only include line 1.
同时用户 B 编辑发票并添加第 3 行,然后单击保存:
-- transaction begins
set transaction isolation level serializable;
select * from invoice where id = 123; -- #2 will this wait the other thread?
insert into invoice_line (invoice_id, line, amount) values (123, 3, 45);
commit; -- User B would expect the invoice to include lines 1, 2, and 3.
不幸的是,两个事务都成功了,我得到了合并的行(损坏状态):
invoice_id line amount
---------- ---- ------
123 1 24
123 3 45
既然这不是我想要的,我有什么选择来控制并发?
这不是数据库并发问题。数据库的 ACID 属性与完成事务有关,同时保持数据库的完整性。在您描述的情况下,交易是正确的,数据库正在正确处理它们。
你想要的是一种锁定机制,本质上是一种信号量,保证在任何时候只有一个用户可以对数据进行写访问。您可能能够依赖数据库锁定机制,在锁定失败时进行捕获。
但是,我建议使用其他两种方法中的一种。如果您对仅在应用程序逻辑中进行的更改感到满意,那么将锁定机制放在那里。有一个用户可以 "lock" table 或记录的地方;那就不要让任何人碰它。
你可以更进一步。您可以要求用户获得 "ownership" 的 table 以进行更改。然后你可以实现一个失败的触发器,除非用户是进行更改的人。
而且,您可能会想到其他解决方案。我真正想指出的是,您的 use-case 超出了 RDBMS 默认执行的范围(因为它们会让两个事务都成功完成)。因此,您需要为任何数据库(我熟悉的)添加额外的逻辑。
一般而言,发票行项目不应在过帐后进行编辑或删除。如果客户需要冲销费用,通常的做法是添加一个记入金额的新交易,可能带有一个 cross-reference 字段,其中包含要冲销的行项目的 ID。这种方法的优点是 (1) 您可以修改客户的余额,而无需返回并重新预订任何先前的报表期间,并且 (2) 您不会 运行 陷入像这样的并发问题,这很难解决。
如果发票尚未过帐,您仍然不允许编辑行项目。相反,您将取消之前的发票并创建一个包含所有新行项目的新发票。这再次避免了手头的并发问题。