news 2026/9/4 7:06:52

PostgreSQL 并发约束实战(第 1 篇):排他约束如何挡住并发重复预约

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL 并发约束实战(第 1 篇):排他约束如何挡住并发重复预约

两个用户同时预订会议室 A 的 10:00—11:00。两个请求都先查询“没有冲突”,随后各自插入一条预约,两个事务也都成功提交。SQL 没报错,唯一键没有重复,会议室却被卖了两次。

PostgreSQL 真正形成差异的地方,不是比另一种数据库多几个数据类型,而是能把“同一资源的时间段不得重叠”变成提交时持续成立的数据库约束。

这篇文章只证明这一个判断。它不证明 PostgreSQL 在所有 OLTP 场景都优于 MySQL,也不证明所有业务规则都应该塞进数据库。

先查再插,为什么挡不住并发

最常见的预约逻辑分两步:

SELECT1FROMroom_bookingWHEREroom_id=101ANDslot&&tstzrange('2026-09-01 10:00+08','2026-09-01 11:00+08','[)');-- 没查到冲突后再执行INSERTINTOroom_booking(room_id,slot,booked_by)VALUES(101,tstzrange('2026-09-01 10:00+08','2026-09-01 11:00+08','[)'),'alice');

这段逻辑在单会话下没有问题,在并发下却存在检查与写入之间的空窗:事务 A 和 B 都能在对方提交前看到“没有预约”,然后分别插入不同主键。普通唯一约束只能识别某个值是否重复,无法表达两个时间范围是否相交。

把查询和插入写在同一个 Read Committed 事务里也没有自动补上这个空窗。事务边界只能规定一组操作怎样提交,不能凭空知道业务不变量是什么。

把“不重叠”写成数据库能执行的规则

以下实验仅用于 PostgreSQL 18.6 测试库。btree_gist是 PostgreSQL 自带扩展,需要当前用户有创建扩展的权限;实验会创建表和 GiST 索引,不应直接复制到生产库。

CREATEEXTENSIONIFNOTEXISTSbtree_gist;CREATETABLEroom_booking(booking_idbigintGENERATED ALWAYSASIDENTITYPRIMARYKEY,room_idbigintNOTNULL,slot tstzrangeNOTNULL,booked_bytextNOTNULL,CHECK(NOTisempty(slot)),EXCLUDEUSINGgist(room_idWITH=,slotWITH&&));

tstzrange把开始和结束时间组合为一个有边界语义的值。[)表示包含开始、不包含结束,因此 10:00—11:00 与 11:00—12:00 可以首尾相接。

排他约束声明:任意两行不能同时满足room_id相等且slot重叠。官方文档说明,排他约束通过索引实现;这里btree_gistbigint的相等比较提供 GiST 操作符类,范围类型本身支持重叠操作符&&

先验证串行结果:

INSERTINTOroom_booking(room_id,slot,booked_by)VALUES(101,tstzrange('2026-09-01 10:00+08','2026-09-01 11:00+08','[)'),'alice');INSERTINTOroom_booking(room_id,slot,booked_by)VALUES(101,tstzrange('2026-09-01 10:30+08','2026-09-01 11:30+08','[)'),'bob');

第二条语句应以 SQLSTATE23P01,即exclusion_violation失败。这个结果证明数据库拒绝了已存在的重叠预约,但还没有证明并发提交时同样安全。

两个并发事务才是主实验

清空实验数据后打开两个会话:

TRUNCATEroom_booking RESTARTIDENTITY;

会话 A:

BEGIN;INSERTINTOroom_booking(room_id,slot,booked_by)VALUES(101,tstzrange('2026-09-01 10:00+08','2026-09-01 11:00+08','[)'),'alice');-- 暂不提交

会话 B:

BEGIN;INSERTINTOroom_booking(room_id,slot,booked_by)VALUES(101,tstzrange('2026-09-01 10:30+08','2026-09-01 11:30+08','[)'),'bob');

会话 B 此时通常会等待,因为数据库还不知道会话 A 最终提交还是回滚。回到会话 A:

COMMIT;

会话 A 提交后,会话 B 应收到23P01并进入事务失败状态:

ROLLBACK;

最终验收不是“其中一条 INSERT 报错”,而是业务不变量仍成立:

SELECTbooking_id,room_id,slot,booked_byFROMroom_bookingORDERBYbooking_id;

结果应只有 Alice 的一条预约。若会话 A 改为ROLLBACK,会话 B 则可以继续成功,这说明等待不是固定拒绝,而是根据并发事务的最终状态裁决。

这个实验能证明什么

  • 相同会议室的重叠时间段不能同时提交;
  • 约束在并发写入路径上生效,不依赖应用先查询到对方记录;
  • 失败是可识别的23P01,应用可以把它转换成“时间段已被占用”。

这个实验不能证明什么

  • 不能证明排他约束在任意数据量和并发下都满足业务延迟;
  • 不能证明跨数据库、外部支付或消息发送也获得原子性;
  • 不能证明所有预约规则都能写成范围重叠;容量、清洁间隔、优先级和审批可能需要额外建模;
  • 不能用本机一次实验外推生产吞吐。

实验结束后清理对象:

DROPTABLEroom_booking;

只有确认测试库中没有其他对象依赖btree_gist时,才考虑执行DROP EXTENSION btree_gist;保留扩展本身通常不影响数据正确性。

四种方案面对同一条业务规则

共同前提是:多个应用实例并发写入,同一会议室的时间段不得重叠,冲突必须在提交前被识别。

方案冲突怎样被发现成立条件代价与失效边界
应用先查再插应用查询现有预约只有单写者,或另有可靠串行化机制多实例并发存在竞态;重试也可能再次撞上
锁定一行资源记录所有写者先锁同一会议室行所有入口严格遵循相同锁顺序热门资源会串行等待;遗漏入口就失效
Serializable + 完整事务重试PostgreSQL 检测不可串行化依赖所有相关读写使用 Serializable,应用按40001重试完整事务冲突高时回滚增加;外部副作用必须可重放或延后
范围类型 + 排他约束数据库在写入和提交路径检查重叠不变量可表达为操作符关系,所需 GiST 操作符类存在维护索引有写入成本;复杂规则仍需其他设计

排他约束不是“最高级”的方案,而是这个不变量最直接的表达。若规则是“同一资源永远只能有一个占用区间”,约束比依赖每个调用方正确加锁更稳。若规则跨多次查询、多个聚合条件且无法表达为约束,Serializable 更合适。若热点会议室的冲突极高,主动锁定资源行可能让等待更可预测。

PostgreSQL 的分界线到底在哪里

仅仅列出 MVCC、JSONB、数组、范围类型和丰富索引,仍然是在做功能清单。真正影响选型的是这些能力能否共同维护业务不变量:

业务时间段 → 范围类型保存边界语义 → && 操作符定义重叠 → GiST 索引寻找潜在冲突 → 排他约束进入并发写入裁决 → 提交后的数据继续满足不重叠规则

这条链才是 PostgreSQL 在此场景中的价值。范围类型负责表达,操作符负责比较,索引负责访问,约束负责拒绝非法状态,事务负责决定并发结果何时可见。它们不是四个孤立功能。

但这个结论有明确边界:

  • 预约写入吞吐极高且允许异步冲突处理时,事件流或分区化架构可能更适合;
  • 跨多个独立数据库的规则不能靠单库排他约束解决;
  • 搜索相关性、海量列式聚合和无限文档灵活性仍可能需要专用系统;
  • 使用数据库约束并不免除压测、监控、错误映射和容量规划。

所以“选择 PostgreSQL”不能从“它功能更多”推出。更可靠的判断是:业务是否存在必须在并发提交时持续成立、又能由 PostgreSQL 类型、操作符和约束准确表达的不变量。

上线前真正要验证的五件事

  1. 用生产相同的时区、边界约定和数据分布验证[)是否符合业务;
  2. 并发压测冲突率、等待时间、23P01比例和成功吞吐;
  3. 确认所有写入入口都经过同一约束,批量导入和迁移脚本也不例外;
  4. 23P01映射为可理解的业务冲突,不把它当系统故障无限重试;
  5. 同时核对最终预约结果,而不是只看事务成功数和数据库错误数。

面试表达

PostgreSQL 的差异不只是类型更多。它能把tstzrange的重叠语义、GiST 访问方法和排他约束组合起来,让并发预约在提交路径上持续满足“不重叠”。代价是索引维护和冲突等待;无法表达为约束的跨行规则,再考虑显式锁或 Serializable,并重试完整事务。

官方资料

  • PostgreSQL 18.6 Release Notes
  • Range Types
  • Exclusion Constraints
  • btree_gist
  • Transaction Isolation
  • Serialization Failure Handling
  • PostgreSQL 18.6 源码标签 REL_18_6
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 7:06:47

PostgreSQL MVCC 机制实战(第 2 篇):一次 UPDATE 为什么留下两个行版本

订单表逻辑行数没有增长,每天却执行数千万次状态刷新;一段时间后 Heap 和索引都变大,查询读到的仍然只有每个订单的最新一行。问题不在重复 INSERT,而在 PostgreSQL 的 UPDATE 会写入新行版本,旧版本要等到不再被任何相…

作者头像 李华
网站建设 2026/9/4 7:05:31

基于STM32F4的模拟调制信号识别与参数估计系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:04:30

香烟盒目标检测数据集实战:YOLO训练避坑指南

简介:本资源是面向YOLO系列算法目标检测任务的专用香烟盒子数据集,适用于计算机视觉初学者、算法工程师及工业质检场景开发者,可直接用于YOLOv5/v7/v8/v9/v10/v11等主流版本的模型训练、验证与测试。压缩包共961个文件,包含320张高…

作者头像 李华
网站建设 2026/9/4 7:03:17

OneRec开源框架:多源信息融合如何破解推荐系统数据孤岛难题

简介:OneRec是一个面向推荐系统研发者的开源多源信息融合算法框架,专为解决工业场景中数据孤岛问题而设计,适用于具备Python与推荐系统基础的中高级开发者及算法工程师。它支持行为数据、多信号(显式/隐式)、长短期兴趣…

作者头像 李华