在你的职业生涯中,你会遇到薛定谔的猫问题,这种情况有时有效,有时无效。竞争条件是这些挑战之一(是的,只是其中之一!)。
在这篇博文中,我将展示一个真实的示例,演示如何重现问题并讨论使用 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
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
交易级别使用咨询锁来实现这一点。这种类型的锁完全由应用程序控制。您可以在这里找到更多相关信息。
需要注意的是,锁可以在会话级别和事务级别应用。您可以探索此处提供的各种功能。在我们的例子中,我们将使用 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;
写入倾斜场景,可能非常棘手。有大量的研究和不同的方法来解决这些问题,所以如果您好奇的话,一定要查看一些论文和文章。
这些问题可能会在现实生活中出现,例如当多人尝试在活动中预订同一个座位或在剧院购买同一个座位时。它们往往随机出现,并且很难弄清楚,特别是如果这是您第一次与它们打交道。当您遇到竞争条件时,重要的是要研究哪种解决方案最适合您的具体情况。我将来可能会做一个基准测试来比较不同的方法并为您提供更多见解。我希望这篇文章对您有所帮助。请记住,有一些工具可以帮助解决这些问题,而且您并不是唯一面临这些问题的人!
亚姆塞基
/
开发者
dev.to 博客文章的实现
以上就是处理竞争条件:一个实际示例的详细内容,更多请关注其它相关文章!