Postgres 序列用一个缓存发号
PostgreSQL 序列缓存为数据库后端预留数值,不属于某个应用对象,也不只属于一笔事务。理解这个边界,才能解释为什么序列保存的位置会领先于真正返回的号码,为什么重新连接也会留下空洞,以及为什么调小 CACHE 不能让标识连续。首先要区分分配器状态,与应用真正成功提交的业务记录,不能把两者当成同一个计数器。 三个不同的数字 一个新建的升序序列使用 CACHE 5 时,第一次 nextval 可以返回一,而 last_value 已记录五。后者是预留边界,不是已插入行数,也不表示一到五都已经交给应用。currval 表示当前会话最近为该序列建立的值,其他连接取号不会替换它。监控时若不说明这些字段的作用域,就很容易把预留数量误当成业务数量。 同一后端后续取号,可以使用已经预留的数值。PostgreSQL 实现中既有后端局部的快速路径,也有补充缓存时的缓冲锁;把每次 nextval 都说成对普通表记录加一把行锁并不准确。缓存减少反复访问序列状态,但不能据此认定某个应用的插入延迟主要来自序列竞争。修改参数前,应先测量真实瓶颈,而不是根据取号机制猜测整体性能。 会话寿命也影响空号 即使正常退出,会话结束时没有用完的预留值也会被放弃。因此,相同取号速率下,频繁建立数据库会话的服务,可能比使用长期连接池的服务放弃更多预留。缓存跟随服务器会话;应用看到一个连接对象,并不足以说明底层数据库会话是否一直存在,连接复用策略也是实验条件的一部分。 不同会话还可以分别持有不同区间。一个先返回一,另一个返回六,第一个随后再返回二。这是分配交错,不代表提交顺序,也不能证明这些值已经丢失。即使 CACHE 为一,事务仍可能先取号后提交,顺序与另一个事务相反。提交之前写入的时间戳同样不能自动提供真实提交顺序,不能只给字段换个名字就获得新的保证。 坏掉的样子 不要把 CACHE 当成崩溃后最大跳号数的公式。多个会话可以同时持有预留,而实现还会把一部分未来序列进度提前写入日志。事务回滚、走冲突处理分支的插入,以及取到但没有使用的值,都会增加空洞。缺号本身既不能证明丢了业务数据,也不能证明没有丢;发现故障时,应核对已提交记录和恢复证据,再判断是否只是分配器行为。 正常运行期间,非循环分配在管理员没有把序列重置回已用范围时,会提供不同数值。但循环、手工重置,以及显式导入编号,都可能改变这个前提。表仍然需要主键或唯一约束保证存储完整性。减小缓存不能代替这些约束,不能找回已经删除的记录,也不会自动让业务编号流程具备可审计性。在数据库外持久使用一个号码之前,应先提交取号事务;崩溃可能使尚未提交的序列变化是否保留变得不确定。 调整序列设置时,还要考虑活跃会话已经拿到的预留值。不要为了补洞,在并发写入期间把序列随意重置到表中最大编号。如果业务确实要求已签发号码连续,应先明确签发、回滚、取消和外部发布的边界,再评估事务计数器。没有经过实际负载测量,就不存在可以普遍承诺的最佳缓存大小或吞吐提升倍数。 已返回的二与从未返回的四、五是不同空号来源。历史原样脚本仅覆盖先后单用户后端,不证明崩溃跳号或并发。 快速验证 下面的无套接字练习,使用两个先后运行的单用户后端,访问同一个新初始化的可丢弃集群。以非 root 账户执行脚本,并把 PostgreSQL 18 的 initdb 与 postgres 放入 PATH。绝不能让单用户模式访问正在运行的集群。该模式具有隐式超级用户权限,不能验证并发客户端、真实进程间锁竞争或故障切换;本例只研究缓存预留的生命周期。 ...