ZVVQ代理分享网

处理竞争条件:一个实际示例(处于竞争劣势英

作者:zvvq博客网
导读在你的职业生涯中,你会遇到薛定谔的猫问题,这种情况有时有效,有时无效。竞争条件是这些挑战之一(是的,只是其中之一!)。 在这篇博文中,我将展示一个真实的示例,演示如

在你的职业生涯中,你会遇到薛定谔的猫问题,这种情况有时有效,有时无效。竞争条件是这些挑战之一(是的,只是其中之一!)。

在这篇博文中,我将展示一个真实的示例,演示如何重现问题并讨论使用 postgresql 的可序列化事务隔离和咨询锁来处理竞争条件的策略。

受到“设计数据密集型应用程序”第 7 章 - 事务“弱隔离级别”的启发

带有实际示例的 github 存储库

应用程序

此应用程序管理医院医生的值班轮班。为了关注竞争条件问题,让我们简化我们的场景。我们的应用程序围绕这个表进行解析:

1

2

3

4

5

6

create table shifts (

id serial primary key,

doctor_name text not null,

shift_id integer not null,

on_call boolean not null default false

);

我们有一条关键的业务规则:

每个班次必须始终有至少一名医生待命。

正如您可能已经猜到的,实现简单的 api 可能会导致竞争条件场景。考虑这个假设的情况:

杰克和约翰在同一班次期间都在医院待命。几乎同时,他们决定请假。一个成功了,但另一个依赖于有关有多少医生轮班的过时信息。结果,两人最终都下班了,这违反了业务规则,并在没有值班医生的情况下离开了特定的班次:

1

2

3

4

5

6

7

john --begin------doctors on call: 2-------leave on call-----commit--------> (t)

\                 \                      \             \

\                 \                      \             \

database ------------------------------------------------------------------> (t)

/               /                      /              /

/               /                      /              /

jack ------begin------doctors on call: 2-----leave on call----commit-------> (t)

重现问题

该应用程序是一个用 golang 实现的简单 api。查看 github 存储库,了解有关如何运行和执行脚本以重现此竞争条件场景的说明。总之,您需要:

启动服务器:yarn nxserve hospital-shifts 运行 k6 测试来重现竞争条件场景:yarn nx test hospital-shifts

测试尝试同时取消两名医生,使用不同的方法访问端点:shiftid=1使用咨询锁,shiftid=2使用可序列化事务隔离,shiftid=3是天真没有并发控制的实现

k6结果会输出自定义指标来指示哪个shiftid违反了业务规则:

1

2

3

4

✓ at least one doctor on call for shiftid=1

✓ at least one doctor on call for shiftid=2

✗ at least one doctor on call for shiftid=3

↳  36% — ✓ 123 / ✗ 217

您将需要 yarn、go、k6 和 docker 等工具,或者您可以使用 devbox 来更轻松地设置存储库依赖项。 解决竞争条件 当我们的应用程序根据陈旧数据做出决策时,就会出现问题。如果两项交易几乎同时运行并且两者都试图叫停医生轮班,则可能会发生这种情况。一笔交易按预期成功,但另一笔交易由于依赖过时的信息,也错误地成功。我们怎样才能防止这种不良行为呢?有几种方法可以实现这一点,我将探索 postgresql 支持的两个选项,尽管在其他数据库管理系统中也可以找到类似的解决方案。 可串行化事务隔离 可序列化快照隔离自动检测并防止异常情况,例如我们的应用程序演示的写入偏差。我不会探讨事务隔离背后的理论,但它是许多流行数据库管理系统中的常见主题。您可以通过搜索快照隔离来找到很好的材料,例如 postgresql 官方文档中有关事务隔离的内容。此外,这是多年前提出此解决方案的论文。空谈很便宜,所以让我们看一下代码:首先启动事务并设置隔离级别为serialized:

1

2

3

4

// init transaction with serializable isolation level

tx, err := db.begintxx(c.request().context(),    &sql.txoptions{

isolation: sql.levelserializable,

})

然后,继续执行操作。在我们的例子中它执行这个函数:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

create or replace function update_on_call_status_with_serializable_isolation(shift_id_to_update int, doctor_name_to_update text, on_call_to_update boolean)

returns void as $$

declare

on_call_count int;

begin

-- check the current number of doctors on call for this shift

select count() into on_call_count from shifts s where s.shift_id = shift_id_to_update and s.on_call = true;

if on_call_to_update = false and on_call_count = 1 then

raise exception [serializableisolation] cannot set on_call to false. at least one doctor must be on call for this shiftid: %, shift_id_to_update;

else

update shifts s

set on_call = on_call_to_update

where s.shift_id = shift_id_to_update and s.doctor_name = doctor_name_to_update;

end if;

end;

$$ language plpgsql;

每当由于并发执行而出现不一致的情况时,可序列化隔离级别将允许一个事务成功,并会自动回滚其他事务并显示此消息,因此您可以安全地重试:

1

error:  could not serialize <a style="color:f60; text-decoration:underline;" href="https://www.zvvq.cn/zt/16380.html" target="_blank">access</a> due to read/write dependencies among transactions

您可以在 updatewithserializedisolation 函数中找到完整的示例。 咨询锁 确保执行业务规则的另一种方法是显式锁定特定班次的资源。我们可以在

交易级别使用咨询锁来实现这一点。这种类型的锁完全由应用程序控制。您可以在这里找到更多相关信息。

需要注意的是,锁可以在会话级别和事务级别应用。您可以探索此处提供的各种功能。在我们的例子中,我们将使用 pg_try_advisory_xact_lock(key bigint) → boolean,它在提交或回滚后自动释放锁:

1

2

3

4

5

6

7

8

9

10

11

begin;

-- attempt to acquire advisory lock and handle failure with exception

if not pg_try_advisory_xact_lock(shift_id_to_update) then

raise exception [advisorylock] could not acquire advisory lock for shift_id: %, shift_id_to_update;

end if;

-- perform necessary operations

-- commit will automatically release the lock

commit;

这是我们应用程序中使用的完整函数:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

-- Function to Manage On Call Status with Advisory Locks, automatic release when the trx commits

CREATE OR REPLACE FUNCTION update_on_call_status_with_advisory_lock(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN)

RETURNS VOID AS $$

DECLARE

on_call_count INT;

BEGIN

-- Attempt to acquire advisory lock and handle failure with NOTICE

IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN

RAISE EXCEPTION [AdvisoryLock] Could not acquire advisory lock for shift_id: %, shift_id_to_update;

END IF;

-- Check the current number of doctors on call for this shift

SELECT COUNT() INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE;

IF on_call_to_update = FALSE AND on_call_count = 1 THEN

RAISE EXCEPTION [AdvisoryLock] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %, shift_id_to_update;

ELSE

UPDATE shifts s

SET on_call = on_call_to_update

WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update;

END IF;

END;

$$ LANGUAGE plpgsql;

您可以在 updatewithadvisorylock 函数中找到完整的示例。 结论 处理竞争条件,例如我们讨论的

写入倾斜场景,可能非常棘手。有大量的研究和不同的方法来解决这些问题,所以如果您好奇的话,一定要查看一些论文和文章。

这些问题可能会在现实生活中出现,例如当多人尝试在活动中预订同一个座位或在剧院购买同一个座位时。它们往往随机出现,并且很难弄清楚,特别是如果这是您第一次与它们打交道。当您遇到竞争条件时,重要的是要研究哪种解决方案最适合您的具体情况。我将来可能会做一个基准测试来比较不同的方法并为您提供更多见解。我希望这篇文章对您有所帮助。请记住,有一些工具可以帮助解决这些问题,而且您并不是唯一面临这些问题的人!

亚姆塞基 / 开发者

dev.to 博客文章的实现

以上就是处理竞争条件:一个实际示例的详细内容,更多请关注其它相关文章!