介绍
一个团体希望一起参加。两人请求只订到一个座位并不算成功。你将保存可复用 SQL 语句,为整个团体返回真实确认,或在名额不足时不作任何变更。一次真实唯一键失败将检验名额占用与确认是否属于同一事务。
本独立实验准备了新的工作坊和原始报名,不使用上一实验的文件。你需要掌握上一实验中的事务、行锁和 ROLLBACK 概念,以及基础 PostgreSQL SELECT、INSERT 和 UPDATE。这里会讲解命名客户端参数、整数转换和数据修改型公用表表达式。Workshop Desk 是 shell;Desk A 和 Desk B 是同一台机器上连接同一数据库的独立 psql 连接。作者估计时长包含阅读和普通错误处理,没有初学者计时研究。
创建 /home/labex/project/workshop-desk/reserve.sql。保留提供的工具、表定义、原始报名,以及各工作坊 ID、名称、容量和备注。文件只包含一条 SQL 语句,不含 psql 命令、shell 命令、显式 BEGIN/COMMIT、配置变更或外部资源。它接受四个指定参数,返回实际报名的四个字段。接受等效安全 SQL。这是受控 READ COMMITTED 练习,不涉及支付、自动重试或分布式准入保证。
关联名额占用与报名确认
本步骤中,你将保存一条语句,确保没有实际占用整个团体所需名额时,就不能创建确认。
从 Workshop Desk 开始。./ 运行当前目录中准备好的程序。状态程序读取真实表,不补齐缺失工作:
./desk status
工作坊 201 容量为 6,已有一个原始报名座位,剩余 5 个名额。工作坊 202 有 3 个名额,203 有 4 个。必须保留 original-family|201|1|Original。第一组显示使用 workshop_id|title|capacity|places_left|booked_seats,第二组使用 request_code|workshop_id|seats|desk。
文件接受这些 psql 变量:workshop_id、seats、request_code 和 desk。它们是每次请求独立提供的客户端参数,不是数据库列或永久服务器设置。形式 :'name' 要求 psql 为变量值安全地加上引号并转义,作为 SQL 字符串字面值插入;请求代码中的单引号也会保留。不要替换成不带引号的 :name。PostgreSQL 的 ::integer 将工作坊和正团体人数文本转换为整数。
WITH claimed AS (...) 命名一个公用表表达式,简称 CTE,即此语句使用的中间关系。这里 CTE 执行条件 UPDATE。只有整个请求团体能容纳时,其 RETURNING workshop_id 才提供一行。INSERT ... SELECT ... FROM claimed 使用这条实际返回行。因此空的名额占用结果不会产生 INSERT。最后的 RETURNING 报告实际插入的报名。不要重新读取基表,并期待同一语句能从那里看到数据修改型 CTE 的新值;应使用其 RETURNING 关系。
减法使用请求人数,WHERE 条件检查相同人数。一条语句在隐式事务内运行:名额占用和 INSERT 要么共同成功,要么 SQL 错误使两者一起回滚。在后续双柜台测试中,显式事务会让这同一语句保持未提交,以便观察其锁。
保存完整语句。cat > reserve.sql 用下面文本替换文件。带引号的 here-document 标记 SQL 防止 shell 修改内容。结尾 SQL 独占一行。在 shell Workshop Desk 中运行,而不是任何数据库柜台:
cat > reserve.sql <<'SQL'
WITH claimed AS (
UPDATE workshops
SET places_left = places_left - :'seats'::integer
WHERE workshop_id = :'workshop_id'::integer
AND places_left >= :'seats'::integer
AND :'seats'::integer > 0
RETURNING workshop_id
)
INSERT INTO bookings (request_code, workshop_id, seats, desk)
SELECT :'request_code', workshop_id, :'seats'::integer, :'desk'
FROM claimed
RETURNING request_code, workshop_id, seats, desk;
SQL
使用前读取保存文件。cat 显示内容;拼写错误应在保存源中修正,而不是用另一条命令悄悄掩盖:
cat reserve.sql
desk book 使用原生 psql 参数运行这个确切文件;它不提供参考查询、不创建后备报名,也不修复余额。参数依次为工作坊 ID、正团体人数、唯一请求代码和柜台标签。运行正常两人请求:
./desk book 201 2 normal-group A
实际返回行为:
normal-group|201|2|A
检查真实数据:
./desk status
工作坊 201 现在剩余 3 个名额,已报名 3 个座位。原始家庭与精确的两人报名都保留。仅返回一行不能证明算法可靠:检查还读取真实行、保留原始记录,并在不同隔离数据上调用原样源文件,包括真实竞争连接。这些临时测试数据库绝不修复实际工作台。
SQL 失败时,程序打印原生错误并以非零状态退出,不将其称为名额不足。在交互式显式事务中,先在同一柜台 ROLLBACK。尚未提交的拼写错误不会改变已提交数据。如果不小心提交了一笔新的错误试验报名,按上一实验所教,在一个事务中只移除其精确代码,并仅重算其工作坊余额。例如,在 Desk A 中,下面的恢复只删除假设存在于 201 的 typo-trial;不存在该错误时可安全执行:
BEGIN;
DELETE FROM bookings WHERE request_code = 'typo-trial' AND workshop_id = 201;
UPDATE workshops
SET places_left = capacity -
(SELECT COALESCE(SUM(seats), 0) FROM bookings WHERE workshop_id = 201)
WHERE workshop_id = 201;
COMMIT;
切勿移除 original-family 或任务要求保留的合法报名。检查状态后再判断剩余工作。不要仅因后续命令失败就重跑已成功请求。
测试等待中的团体与确认失败
本步骤中,你将在两个独立原生连接里运行同一文件,让另一工作坊继续运行,并证明失败 INSERT 不会消耗名额。
切换到 Desk A。提示符表明你位于 psql,而非 shell。SQL 以分号结束。反斜杠开头的 psql 命令控制客户端,不加分号。\set 设置当前连接中的客户端变量,不会延续到另一个标签页。\i 读取并执行指定文件。绝对文件名不依赖客户端当前目录。
开始 READ COMMITTED 事务,为初始有三个名额的工作坊 202 提交两人团体:
BEGIN ISOLATION LEVEL READ COMMITTED;
\set workshop_id 202
\set seats 2
\set request_code race-A
\set desk A
\i /home/labex/project/workshop-desk/reserve.sql
A 看到 race-A | 202 | 2 | A 和 INSERT 0 1:语句插入一条代表两个座位的报名行。这还未提交。提示符中的 * 标记打开的事务。
切换到 Desk B,准备独立请求:
BEGIN ISOLATION LEVEL READ COMMITTED;
\set workshop_id 202
\set seats 2
\set request_code race-B
\set desk B
\i /home/labex/project/workshop-desk/reserve.sql
B 等待 A 的行锁,提示符尚未返回。让它保持等待,切换到 Workshop Desk:
./desk waits
Desk B 的 wait_event_type 为 Lock,blocked_by 中包含 A 的实际后端进程 ID。READ COMMITTED 下,A 提交后,等待 UPDATE 会针对更新后的行重新检查名额是否充足。先前可见的余额不是预留。
仍在 Workshop Desk 中,使用相同文件报名另一工作坊:
./desk book 203 2 other-workshop C
它返回 other-workshop|203|2|C,在 A 仍持有 202 时完成。不要引入单一全局锁,无必要地阻塞这个无关工作坊。
切换到 Desk A:
COMMIT;
切换到 Desk B。等待中的文件现在完成,返回零行和 INSERT 0 0。只剩一个名额,无法容纳这个两人团体。不要拆分团体或单独插入确认。结束 B 的事务:
ROLLBACK;
切换到 Workshop Desk:
./desk status
工作坊 202 剩一个名额,恰好有 race-A|202|2|A,不存在 race-B。工作坊 203 剩两个名额,有其两人报名。检查关系,不只看成功退出码。
接下来,在 202 仍有足够名额可占用一个座位时,让 INSERT 失败。主键要求请求代码在整个 bookings 表内唯一,而不只是同一工作坊内唯一。代码 original-family 已属于 201。此处复用应产生 SQLSTATE 23505,即唯一键冲突。这是预期失败,不是无可用名额结果:
./desk book 202 1 original-family A
文件执行报告原生错误,并以状态 3 退出。立即捕获该状态,避免其他 shell 命令替换 $?,即上一命令退出状态:
printf 'Previous native exit: %s\n' "$?"
预期为 Previous native exit: 3。非零状态将错误与成功零行结果区分。语句的隐式事务必须撤销先前名额占用。再次读取明细:
./desk status
工作坊 202 仍有一个名额,201 上的 original-family 不变。占用后使用 ON CONFLICT DO NOTHING 等捷径,可能隐藏 INSERT 失败却提交扣减。这不是原子拒绝:计数会不再匹配实际报名。检查器在有足够名额时测试此情况,避免空占用结果意外让错误测试通过。
使用新请求代码合法报名最后一个名额:
./desk book 202 1 after-error A
预期为 after-error|202|1|A。现在名额已用尽,再请求一个两人团体:
./desk book 202 2 too-late B
程序打印 No booking returned. Inspect the real ledger before drawing a conclusion.。调用以零状态退出,因为 SQL 有效但没有返回报名;它不能留下 too-late 行,也不能改变余额。
./desk status
最终新增报名为 normal-group(201、2、A)、race-A(202、2、A)、other-workshop(203、2、C)和 after-error(202、1、A),另有未改变的原始家庭。没有 race-B 或 too-late。剩余名额分别为 3、0 和 2。
如果用 Ctrl+C 取消等待文件,SQLSTATE 57014 会中止其显式事务。如前所练,重试前在同一数据库标签页 ROLLBACK。先检查已提交明细:另一个柜台可能已经成功。交互式 SQL 错误与普通零行结果需要不同判断。
总结
保存的 SQL 将实际条件名额占用转化为恰好一份完整团体确认。两个原生连接演示了等待和重新判断,同时另一工作坊继续运行。重复请求代码造成真实 INSERT 失败,并回滚其名额占用;后续合法请求随后使用了仍可用的名额。可复用文件和实际明细保留原始数据,并区分名额不足与 SQL 错误。



