GBase 8c 序列取值在分布式业务里的几个风险点
我最近看 GBase 8c 对象和 SQL 行为相关资料时,对序列这类"小对象"重新留意了一下。序列平时不太容易被当成重点,很多时候只是给主键、流水号、任务批次号提供一个递增值。但真正落到分布式业务里,序列并不只是 nextval 这么简单。缓存、回滚、并发、迁移、业务排序这些因素叠在一起,很容易让开发和运维对它产生误解。
我自己理解下来,序列最适合解决的是"生成唯一值",不适合承诺"业务连续号"。如果业务把序列当成严格连续、无空洞、可按大小直接代表业务先后顺序的编号,后面迟早会遇到解释成本。
序列不是事务内可回滚的普通数据
现场最常见的误解是:事务回滚后,序列值也应该回退。这个理解在很多数据库里都不成立,在 GBase 8c 里也不应该这么设计业务。序列值一旦被取出,就应当视为已经消耗。事务失败、应用异常、会话断开,都可能导致序列出现空洞。
一个简单的模拟就能说明问题:
sql
CREATE SEQUENCE seq_recon_batch
START WITH 1000
INCREMENT BY 1
CACHE 20;
BEGIN;
SELECT nextval('seq_recon_batch') AS batch_id;
ROLLBACK;
SELECT nextval('seq_recon_batch') AS next_batch_id;
从业务视角看,中间那个编号没有落到正式表里,但它确实已经被序列消耗了。如果财务流水、票据号、监管报送编号要求严格连续,就不能直接把普通序列值当最终编号。
| 业务诉求 | 能否直接依赖序列 | 原因 | 建议 |
|---|---|---|---|
| 技术主键唯一 | 可以 | 序列适合生成唯一值 | 允许空洞 |
| 批次号唯一 | 可以 | 只要不要求连续 | 记录生成来源 |
| 业务展示流水 | 谨慎 | 用户可能关注连续性 | 明确编号规则 |
| 财务票据连续号 | 不建议 | 回滚和缓存会产生空洞 | 单独设计号段管理 |
| 按编号判断业务时间 | 不建议 | 并发下不可靠 | 使用业务时间字段 |
我实际排查时一般会先问清楚:这个编号是给数据库看的,还是给业务审计看的。前者可以偏技术实现,后者就要把连续性和可解释性单独设计。
CACHE 提升效率,也会放大空洞感
序列通常支持缓存。缓存能减少频繁访问序列对象的开销,在高并发写入时有价值。但缓存也会带来一个现象:节点、会话或进程异常时,未使用完的缓存值可能不再继续使用,业务看到的编号跳跃会更明显。
sql
CREATE SEQUENCE seq_order_event
START WITH 1
INCREMENT BY 1
CACHE 100;
SELECT nextval('seq_order_event');
SELECT nextval('seq_order_event');
如果业务只要求唯一,CACHE 100 没问题;如果业务人员看到编号从 105 跳到 201 就来追问,就说明当初没有把"序列可能跳号"讲清楚。
| CACHE 设置 | 适合场景 | 优点 | 风险 |
|---|---|---|---|
CACHE 1 |
对跳号敏感、并发较低 | 跳跃感较弱 | 性能开销更高 |
CACHE 20 |
普通业务主键 | 性能和可解释性折中 | 异常时仍可能空洞 |
CACHE 100+ |
高并发写入、纯技术主键 | 减少取值开销 | 编号跳跃更明显 |
| 过大缓存 | 不建议随意使用 | 短期吞吐可能好 | 故障后解释成本高 |
我个人更倾向于把 CACHE 当成性能参数,而不是编号规则参数。它能改善取值效率,但不能改变序列的本质:只保证按规则生成值,不保证业务连续。
分布式场景里不要用序列值推断全局先后
GBase 8c 常用于分布式业务场景。并发写入时,多个会话可能同时取序列值。即使序列值整体递增,也不代表事务提交顺序、业务发生顺序完全和序列大小一致。
比如两个会话:
sql
-- 会话 A
BEGIN;
SELECT nextval('seq_trade_log'); -- 返回 2001
-- 中间处理较慢,暂不提交
-- 会话 B
BEGIN;
SELECT nextval('seq_trade_log'); -- 返回 2002
INSERT INTO trade_log(id, trade_id, created_at)
VALUES (2002, 'T202604270002', now());
COMMIT;
-- 会话 A 后提交
INSERT INTO trade_log(id, trade_id, created_at)
VALUES (2001, 'T202604270001', now());
COMMIT;
最终表里 id=2002 的记录可能先提交,id=2001 后提交。如果报表按 id 排序,就可能和实际提交顺序不一致。这个问题平时不明显,一旦业务拿编号做时间判断,就会暴露。
我更建议把排序和审计交给明确字段:
sql
CREATE TABLE trade_log (
id bigint PRIMARY KEY,
trade_id varchar(64) NOT NULL,
created_at timestamp NOT NULL DEFAULT now(),
commit_tag varchar(32),
src_system varchar(32)
);
-- 查询时按业务时间或写入时间排序,而不是只按 id
SELECT trade_id, id, created_at, src_system
FROM trade_log
WHERE created_at >= timestamp '2026-04-01 00:00:00'
ORDER BY created_at, id;
序列字段可以辅助排序,但不应该替代业务时间字段。尤其在补录、重放、批处理导入场景里,序列大小和业务发生时间更不应该混在一起。
迁移和初始化时要处理当前值
序列相关问题在迁移适配里也常见。表数据迁过来了,但序列当前值没同步,应用插入新数据时就可能撞主键。这个问题不属于性能问题,也不是锁问题,而是对象状态没有一起迁移。
排查方式很直接:先看目标表最大值,再看序列下一次取值是否大于现有最大值。
sql
-- 检查业务表现有最大主键
SELECT MAX(id) AS max_id
FROM app_order;
-- 查看序列下一值,注意这会消耗一个序列值
SELECT nextval('seq_app_order') AS next_id;
如果发现序列值落后,就需要在维护窗口内调整。不同现场会采用不同方式,核心原则是让下一次取值大于目标表最大值,并保留调整记录。
sql
-- 示例:按目标表最大值重新设置序列
SELECT setval('seq_app_order',
(SELECT COALESCE(MAX(id), 0) + 1 FROM app_order),
false);
这里要特别注意验证环境和生产环境的差异。有的测试库经过多轮导入导出,序列值可能很大;有的生产库清理过历史数据,最大主键和序列当前值又不完全对应。不要凭环境经验直接套命令。
| 检查项 | SQL/动作 | 风险 | 建议 |
|---|---|---|---|
| 表最大主键 | SELECT MAX(id) |
只看单表,漏分区或历史表 | 明确数据范围 |
| 序列下一值 | nextval |
会消耗一个值 | 在维护窗口验证 |
| 序列归属 | 查对象定义 | 多表误用一个序列 | 梳理依赖 |
| 调整记录 | 保存 setval 前后值 | 后续说不清 | 写入变更单或记录表 |
| 应用重启 | 连接池可能缓存逻辑 | 旧逻辑继续写入 | 调整后做插入验证 |
多表共用序列要谨慎
有些系统喜欢多个表共用一个序列,理由是"全局唯一"。这种做法不是不能用,但需要明确语义。如果只是为了避免不同表主键重复,很多时候没有必要;如果是为了跨表事件全局排序,又会遇到前面说的提交顺序问题。
我更倾向于按业务域拆分序列,避免一个序列承担太多含义。
sql
CREATE SEQUENCE seq_order_id START WITH 1 INCREMENT BY 1 CACHE 50;
CREATE SEQUENCE seq_refund_id START WITH 1 INCREMENT BY 1 CACHE 50;
CREATE SEQUENCE seq_recon_batch_id START WITH 100000 INCREMENT BY 1 CACHE 10;
这样做的好处是排查范围清楚。订单表主键异常就看订单序列,对账批次异常就看批次序列,不会因为一个全局序列被多个业务同时消耗,导致编号跳跃解释不清。
常见坑
| 常见坑 | 现场表现 | 我的判断 | 处理建议 |
|---|---|---|---|
| 认为回滚会退回序列 | 编号有空洞 | 序列值已消耗 | 接受空洞或另设连续号 |
| CACHE 设置过大 | 编号跳跃明显 | 异常或重启后缓存未用完 | 按业务可接受度调整 |
| 用 id 判断时间顺序 | 排序和业务时间不一致 | 并发提交顺序不同 | 增加业务时间字段 |
| 迁移后未调整序列 | 新插入主键冲突 | 序列当前值落后 | 对齐最大值后验证 |
| 多表共用序列 | 编号增长难解释 | 消耗来源太多 | 按业务域拆分 |
| 手工插入指定 id | 后续 nextval 撞值 | 序列不知道手工值 | 插入后同步 setval |
结尾总结
序列在 GBase 8c 里是一个很实用的对象,但我不太愿意把它当成业务编号的万能方案。它擅长生成唯一值,不擅长承担严格连续、严格排序、强审计解释这类业务承诺。分布式环境里,并发、缓存、回滚和迁移都会让序列行为看起来"不连续",但这并不代表异常。
从落地角度看,我会在设计阶段就把技术主键和业务编号分开。技术主键可以用序列,业务连续号要单独设计规则;序列可以辅助定位记录,但排序要看业务时间;迁移时不仅要迁表数据,也要迁序列状态。这样后续排查时,编号问题才不会变成一团混在一起的解释题。
参考资料
text
[1] GBase 8c SQL参考指南 https://www.gbase.cn/docs/gbase-8c/category/sql%E5%8F%82%E8%80%83%E6%8C%87%E5%8D%97
[2] GBase 8c 文档介绍 https://www.gbase.cn/docs/gbase-8c/%E6%AC%A2%E8%BF%8E/
[3] GBase 社区优质文章区 https://www.gbase.cn/community/section/11