文章目录
-
- 一、序列基础:语法、用法与内部结构
-
- [1.1 序列的创建与基本操作](#1.1 序列的创建与基本操作)
- [1.2 SERIAL 与 BIGSERIAL 的本质](#1.2 SERIAL 与 BIGSERIAL 的本质)
- [1.3 序列的内部存储](#1.3 序列的内部存储)
- [1.4 使用建议](#1.4 使用建议)
- 二、序列的核心特性与事务语义
-
- [2.1 序列值不回滚](#2.1 序列值不回滚)
- [2.2 CACHE 机制:性能与跳跃的权衡](#2.2 CACHE 机制:性能与跳跃的权衡)
- 三、高并发下的核心陷阱
-
- [3.1 陷阱一:序列争用(Sequence Contention)](#3.1 陷阱一:序列争用(Sequence Contention))
- [3.2 陷阱二:主从切换导致 ID 回退(仅特定配置)](#3.2 陷阱二:主从切换导致 ID 回退(仅特定配置))
- [3.3 陷阱三:批量插入性能瓶颈](#3.3 陷阱三:批量插入性能瓶颈)
- 四、性能优化策略
-
- [4.1 增大 CACHE 值](#4.1 增大 CACHE 值)
- [4.2 使用多个序列分片(Sharding)](#4.2 使用多个序列分片(Sharding))
- [4.3 升级到 GENERATED AS IDENTITY(PostgreSQL 10+)](#4.3 升级到 GENERATED AS IDENTITY(PostgreSQL 10+))
- 五、极端高并发场景:替代方案评估
-
- [5.1 方案一:应用层 ID 生成器(Snowflake 类)](#5.1 方案一:应用层 ID 生成器(Snowflake 类))
- [5.2 方案二:UUIDv7(趋势有序 UUID)](#5.2 方案二:UUIDv7(趋势有序 UUID))
- [5.3 方案三:混合主键(内部 SERIAL + 外部 UUID)](#5.3 方案三:混合主键(内部 SERIAL + 外部 UUID))
- 六、监控与诊断
-
- [6.1 监控序列使用情况](#6.1 监控序列使用情况)
- [6.2 检测 ID 跳跃](#6.2 检测 ID 跳跃)
- [6.3 性能压测建议](#6.3 性能压测建议)
本文将深入剖析 PostgreSQL 序列的内部机制、事务语义、并发控制原理,并系统性地揭示高并发环境下的典型陷阱,最后提供经过生产验证的优化策略与替代方案。适用于 PostgreSQL 10 及以上版本。
一、序列基础:语法、用法与内部结构
在 PostgreSQL 中,序列(Sequence)是生成唯一递增整数的核心机制,广泛用于实现自增主键(如 SERIAL、BIGSERIAL)。尽管其使用简单、性能优异,但在高并发、分布式、故障恢复等复杂场景下,序列的行为可能引发一系列隐蔽问题------包括 ID 跳跃、缓存竞争、事务回滚导致的空洞,甚至成为系统瓶颈。
1.1 序列的创建与基本操作
序列是一个独立的数据库对象,可通过 SQL 创建:
sql
-- 手动创建序列
CREATE SEQUENCE order_id_seq
START WITH 1
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9223372036854775807
CACHE 1
NO CYCLE;
-- 使用序列生成值
SELECT nextval('order_id_seq'); -- 返回下一个值
SELECT currval('order_id_seq'); -- 返回当前会话最后一次 nextval 的值
SELECT lastval(); -- 返回当前会话最后一次任何序列的值
1.2 SERIAL 与 BIGSERIAL 的本质
SERIAL 并非真实数据类型,而是 PostgreSQL 提供的语法糖:
sql
CREATE TABLE orders (id SERIAL PRIMARY KEY, ...);
等价于:
sql
CREATE SEQUENCE orders_id_seq;
CREATE TABLE orders (
id INTEGER NOT NULL DEFAULT nextval('orders_id_seq'),
...
);
ALTER SEQUENCE orders_id_seq OWNED BY orders.id;
SERIAL→INTEGER+ 序列BIGSERIAL→BIGINT+ 序列
1.3 序列的内部存储
序列信息存储在系统表 pg_sequence 和 pg_class 中。关键字段包括:
last_value:序列当前值(注意:不是"已分配的最大值")log_cnt:预分配缓存中的剩余数量is_called:是否已调用过nextval
可通过以下查询查看:
sql
SELECT * FROM pg_sequences WHERE sequencename = 'order_id_seq';
1.4 使用建议
- 接受 ID 非连续:业务逻辑勿依赖 ID 连续性;
- 高并发必设 CACHE :
CACHE 1000起,根据故障容忍度调整; - 避免频繁小批量插入:合并为大事务或使用 COPY;
- 监控序列争用:关注 LWLock 等待事件;
- 分布式系统慎用纯序列:考虑 UUIDv7 或 Snowflake;
- 优先使用 IDENTITY 列:替代 SERIAL,更符合标准;
- 主从环境确保 WAL 持久化:防止 ID 回退(通常默认满足)。
二、序列的核心特性与事务语义
理解序列的行为,必须明确其与事务的关系。
2.1 序列值不回滚
关键特性 :nextval() 产生的值不会因事务回滚而回收。
sql
BEGIN;
SELECT nextval('seq'); -- 返回 100
ROLLBACK;
SELECT nextval('seq'); -- 返回 101,100 已永久消耗
原因 :
为避免多个事务在 nextval 上串行化(严重降低并发),PostgreSQL 在调用 nextval 时立即持久化序列状态,不参与事务回滚。
影响:
- ID 存在"空洞"(gaps)是正常现象;
- 不能依赖 ID 连续性做业务逻辑(如"第 N 个用户");
- 审计或合规场景需额外记录逻辑序号。
2.2 CACHE 机制:性能与跳跃的权衡
CACHE n 参数控制每次从磁盘读取多少个值到内存:
CACHE 1(默认):每次nextval都更新磁盘,保证最小跳跃,但 I/O 高;CACHE 100:一次分配 100 个值,后续 99 次nextval无需磁盘 I/O。
示例:
sql
CREATE SEQUENCE seq CACHE 10;
-- 第一次 nextval:分配 1~10,返回 1
-- 第二次~第十次:返回 2~10(无磁盘写)
-- 第十一次:分配 11~20,返回 11
故障影响 :
若数据库崩溃,已缓存但未使用的值(如 2~10)将丢失,导致 ID 跳跃。
✅ 建议 :高并发写入场景应设置
CACHE 1000或更高,以减少序列争用。
三、高并发下的核心陷阱
3.1 陷阱一:序列争用(Sequence Contention)
当大量会话同时调用 nextval,即使有 CACHE,仍可能在以下环节竞争:
- 获取序列锁(轻量级锁)
- 更新共享内存中的序列状态
表现:
- CPU 等待时间增加(
LWLock等待) nextval延迟升高- 吞吐量无法线性扩展
验证方法:
sql
-- 查看 LWLock 等待
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE wait_event_type = 'LWLock'
GROUP BY 1, 2;
-- 若出现 'XidGenLock' 或 'ProcArrayLock',可能与序列相关
3.2 陷阱二:主从切换导致 ID 回退(仅特定配置)
在流复制(Streaming Replication)环境中:
- 主库生成 ID:1000
- 备库通过
pg_recvlogical或应用 WAL 同步 - 若主库崩溃,备库升主
问题 :
若原主库在故障前已缓存 ID(如 1001~2000),但未写入 WAL,则新主库可能从 1000 重新开始分配,导致ID 重复。
⚠️ 注意:标准物理复制(WAL-based)不会 出现此问题,因为
nextval会写入 WAL。但若使用逻辑复制或自定义 ID 服务,则需警惕。
3.3 陷阱三:批量插入性能瓶颈
使用 INSERT ... SELECT nextval(...) 批量插入时,每行调用一次 nextval,即使有缓存,仍存在函数调用开销。
低效写法:
sql
INSERT INTO t (id, name)
SELECT nextval('seq'), name FROM source_table;
-- 每行调用 nextval,无法向量化
高效写法:
sql
INSERT INTO t (id, name)
SELECT row_number() OVER () + (SELECT last_value FROM seq) - 1, name
FROM source_table;
-- 但需手动更新序列,且不安全(并发冲突)
更安全的方式仍是逐行 nextval,或改用 GENERATED AS IDENTITY(见后文)。
四、性能优化策略
4.1 增大 CACHE 值
这是最直接有效的优化:
sql
ALTER SEQUENCE order_id_seq CACHE 10000;
效果:
- 减少磁盘 I/O 和锁竞争
- 单节点可支撑 10万+/秒 的 ID 生成
权衡:
- 故障时最多丢失
CACHE个 ID - 对于要求"尽量连续"的场景,可接受适度跳跃
4.2 使用多个序列分片(Sharding)
将 ID 生成分散到多个序列,由应用层路由:
sql
-- 创建 16 个序列
CREATE SEQUENCE seq_0 CACHE 1000;
CREATE SEQUENCE seq_1 CACHE 1000;
...
CREATE SEQUENCE seq_15 CACHE 1000;
-- 应用根据 user_id % 16 选择序列
SELECT nextval('seq_' || (user_id % 16));
优势:
- 消除单点争用
- 线性扩展 ID 生成能力
代价:
- ID 不再全局单调递增(但每个分片内有序)
- 增加应用复杂度
4.3 升级到 GENERATED AS IDENTITY(PostgreSQL 10+)
IDENTITY 列是 SQL 标准,比 SERIAL 更规范:
sql
CREATE TABLE orders (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
...
);
优势:
- 自动管理序列,无需手动绑定
- 支持
OVERRIDING SYSTEM VALUE插入指定值 - 未来兼容性更好
性能:底层仍使用序列,优化策略相同。
五、极端高并发场景:替代方案评估
当单实例序列成为瓶颈(>50万 TPS),需考虑架构级方案。
5.1 方案一:应用层 ID 生成器(Snowflake 类)
- 原理:各应用节点独立生成 ID,包含时间戳 + 节点 ID + 序列号
- 优点:完全去中心化,无数据库依赖
- 缺点 :
- 时钟回拨问题
- 需维护节点 ID 分配
- ID 非严格递增(跨节点)
PostgreSQL 集成:
- 主键用
BIGINT - 应用生成 ID 后直接插入
- 数据库仅做唯一性校验(需唯一索引)
5.2 方案二:UUIDv7(趋势有序 UUID)
如前文所述,UUIDv7 兼具全局唯一与时间有序性:
sql
CREATE TABLE t (id UUID PRIMARY KEY DEFAULT uuid7());
适用场景:
- 无法部署中心化 ID 服务
- 接受 16 字节主键
- 需要天然分片能力(按 ID 前缀)
5.3 方案三:混合主键(内部 SERIAL + 外部 UUID)
- 内部用
BIGSERIAL保证 JOIN 性能 - 外部 API 暴露
UUID字段 - 两者通过唯一索引关联
平衡点:兼顾性能与分布式需求。
六、监控与诊断
6.1 监控序列使用情况
sql
-- 查看序列当前值与缓存
SELECT schemaname, sequencename, last_value, cache_size
FROM pg_sequences;
-- 查看序列被哪些表使用
SELECT t.relname AS table_name, a.attname AS column_name
FROM pg_class s
JOIN pg_depend d ON d.refobjid = s.oid
JOIN pg_class t ON d.objid = t.oid
JOIN pg_attribute a ON (d.objid, d.refobjsubid) = (a.attrelid, a.attnum)
WHERE s.relkind = 'S' AND s.relname = 'order_id_seq';
6.2 检测 ID 跳跃
通过对比 nextval 间隔发现异常跳跃:
sql
-- 记录每次分配的 ID 和时间
CREATE TABLE id_audit (id BIGINT, created_at TIMESTAMPTZ DEFAULT NOW());
-- 触发器或应用层插入
INSERT INTO id_audit (id) VALUES (nextval('seq'));
-- 分析跳跃
SELECT id, lag(id) OVER (ORDER BY created_at) AS prev_id,
id - lag(id) OVER (ORDER BY created_at) AS gap
FROM id_audit
WHERE id - lag(id) OVER (ORDER BY created_at) > 1;
6.3 性能压测建议
使用 pgbench 模拟高并发序列调用:
bash
# 自定义脚本 seq.sql
\set id nextval('test_seq')
SELECT :id;
# 运行压测
pgbench -c 100 -j 10 -T 60 -f seq.sql mydb
观察 TPS 与 CPU 等待事件。
总结:PostgreSQL 序列是一个精巧而高效的 ID 生成机制,其设计在性能、一致性、可用性之间取得了良好平衡。然而,"简单"不等于"无脑使用"。在高并发、分布式、强一致性要求的场景下,开发者必须深入理解其内部行为,主动规避陷阱,并根据业务需求选择合适的优化路径或替代方案。
记住:序列的价值不在于生成连续数字,而在于以最小代价提供全局唯一标识。要把握这一核心。