一批一次拿的 id,减少竞争但不会根绝跳号。

Postgres 序列用一个缓存发号

一个序列在锁下逐个返回数字,缓存则把一大批小锁调用,变成偶发的大幅跳跃。只这一个设计决定——用短锁保正确、用批量换速度——就解释了序列带来的所有意外:重启后的空号、并发下的交错编号、审计敏感表里对不上的号码。没有一条是缺陷,全都源于把它当成高速唯一发号器,而不是里程表。 实际影响可以写成一句直白的契约:序列保证同一序列内唯一、数值大致递增、支持高并发;不保证连续、不保证跨会话单调、不保证已分配未用的号码在崩溃后存活。按真实契约建模,序列就从操心清单里消失;按想象中的里程表建模,每个空号都会变成一次事故。 每次拿一个数字都加锁 序列保存一个计数器,nextval 在其上加一行锁并把计数值推进,于是每次调用都竞争同一个值。在高并发插入时,这里会成为热点。每个后端都要短暂串行地经过同一行去领号,加多少 worker 都突破不了这个上限。锁很短——加一就放——但每秒几万次插入时,再短的串行也是串行。 所以批量导入和并发写入总是最先感受到序列的存在。表可以并行插,索引可以分裂,计数器一次只接待一个领号的人。性能分析通常也能证实:锁等待堆在序列对象上,别处都在空转。序列成了瓶颈时,解法不是更快的锁,而是少去几趟——这正是缓存要买的东西。 缓存换速度、留空洞 开了缓存,调用方一次把一整套拿回家,只在补货时才回到序列,锁的往返大幅减少。但一次重启会把这批还没用完的部分丢掉,所以缺号是设计上的结果。缓存设一百,意味着一百个号码才做一次加锁访问,而不是一个一访——竞争降两个数量级,代价是每次崩溃或切换最多丢一百个号。 缓存大小按插入速率定,别按审美定。缓存为一,空号最少但每次都要加锁;缓存一千,序列基本退出并发舞台,但每次重启能跳一千个号。多数在线交易负载落在二三十到一百之间:锁从性能画像里消失,空号小到没人察觉。用修改序列语句调整,这个设置长在序列自己身上,所以随备份走,从库行为也一致。 序列值还不属于事务快照。事务拿到号码后即使随后回滚,序列也不会倒退;多个会话同时调用时,较大的号码也不代表较早提交。主备切换、批量导入和失败重试都会留下正常的间隙。把序列列当作不可变的技术标识,时间顺序另用提交时间或事件编号表达。若财务流程确实要求连续票号,单独建立窄范围的提交计数器,并接受那里必须串行的成本,别让主键承担两种互相冲突的承诺。 当它是唯一值,别当连续号 序列产生的是唯一 id,绝不是无缝的 1、2、3,缺口完全可以接受。只有真正必须连续的地方才去保号;序列只当作 id 生成器使。空号有三个来源:回滚的事务、重启丢掉的缓存批、并发会话交错领号——三个都正常,一个都挡不住,除非交出并发或崩溃安全。 连续编号是法规或业务硬要求的地方——发票号、排队票——就显式另建机制:提交时在一个短事务里推进的独立计数表,只在这条窄路上承受串行代价。主键用高速且有空号的序列,对外展示的编号用慢而无缝的计数器。把两份工作塞进一个机制,等于逼自己在速度和连续之间二选一,而两种负载其实哪边都不想要对方的答案。 坏掉的样子 经典故障是审计恐慌:切换主库后财务发现发票号跳了几百,怀疑丢了数据。其实什么都没丢——备库提升时开了新的缓存窗口,旧的那批缓存随着主库一起走了。修法是沟通加隔离:文档里写明代理键会跳号,绝不把序列裸值暴露到会被人读出连续含义的地方。号码要见人,就给它配显式的无缝计数器,别用大缓存的序列顶。 第二种故障更隐蔽:两个会话领到交错的号,应用代码却假设编号靠后等于提交靠后。并发下会话甲先领到一百零一、后提交,会话乙后领到一百零二、先提交。按编号排序就排错了历史。提交顺序重要时,按提交时间戳排序,永远别按序列值。 快速验证 验证缓存行为与空号来源: SHOW SERVER_VERSION; SELECT cache_size FROM pg_sequences WHERE sequencename = 'orders_id_seq'; BEGIN; SELECT nextval('orders_id_seq'); ROLLBACK; SELECT last_value FROM orders_id_seq; 回滚照样推进序列,证明空号是结构使然,不是故障所致。 ...

八月 2, 2027 · 1 分钟 · XB Field Notes
已分配的序列值可能留下空洞,存储行的唯一性由主键单独保证。

序列分配数值,不随事务回滚回收

PostgreSQL 序列分配数值,并不以调用方事务最终提交为前提。这让并发场景中的普通标识分配更实用,也意味着空洞是正常现象。一个序列号只说明发生过分配,不能证明对应行存在、事务已经提交,或所有更小的数值都已经使用。把取号与业务记录的生命周期分开,才能正确解释缺号和顺序。 原子分配也有前提 nextval 会原子推进序列。普通非循环序列在没有被重置到已用范围的情况下,能为并发调用方分配不同数值。这不代表实现完全不使用锁,也不代表任何管理操作都不会影响它。CYCLE 和显式重置都可能重新使用数值,因此,已存储行的唯一性仍应由表上的 PRIMARY KEY 或 UNIQUE 约束负责。 序列也有有限边界。非循环序列无法在范围内继续分配时会报错,不会自动扩张成无限整数。应明确选择底层类型和步长;高频标识接近配置上限时,要关注剩余容量。即使序列使用更宽的类型,也不会自动扩宽接收它的表字段,不能只检查分配器而忽略目标列的类型限制。 identity 和 serial 列通过序列提供默认值。序列负责分配,字段约束负责决定哪些行允许写入。显式导入的标识可能让分配器落后于表中数据。修复这种错位时,要考虑活跃写入者和已经预留的数值,不能在业务仍并发写入时,随意根据一次 max 查询执行 setval,然后假设风险已经消失。 空洞与顺序是不同属性 事务回滚不会收回 nextval 已分配的数值。取号后没有使用、插入中止,或 INSERT 走了 ON CONFLICT 分支,都可能产生空洞。删除表中一行也不会让序列倒退。因此,单凭编号缺失,不能认定记录丢失或序列损坏;需要结合事务和业务记录查明实际含义。 CACHE 会按会话预留一批数值。如果一个会话保留一到十,另一个保留十一到二十,外部观察的调用结果就可能是一、十一、二。会话结束时,未使用的预留值也可能被放弃。缓存提高分配效率,却会影响连续性和全局观察顺序;即使没有批量缓存,分配先后也不等于事务提交先后。 setval 对序列状态的修改不会因调用方回滚而撤销,其他会话还可能持有原先缓存的数值,所以重置活跃分配器不是安全的消除空洞方法。如果业务要求连续编号,应明确设计事务内分配与取消语义。这可能需要串行访问一个专用计数器,但不意味着必须锁住业务表中的每一行。不能把普通技术主键与这种业务编号混为一谈。 坏掉的样子 currval 保存指定序列的会话局部值,通常由 nextval 建立。本会话尚未建立这个值时,调用会报错;is_called 为真的 setval 也会修改该会话值。其他连接取过号,并不能为当前连接初始化它。使用连接池时,如果 nextval 与 currval 落到不同数据库会话,原本看似连续的两步操作就可能失败。 ...

四月 12, 2027 · 2 分钟 · XB Field Notes