GBase 8c 序列取值在分布式业务里的几个风险点

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
相关推荐
淡定一生23331 小时前
spark 3.3+ 之BloomFilter Runtime Filter
大数据·分布式·spark
霑潇雨1 小时前
原生 Zookeeper 实现分布式锁案例
java·分布式·zookeeper·云原生·maven
Francek Chen1 小时前
【大数据存储与管理】云数据库:02 云数据库产品
大数据·数据库·分布式·云计算·云数据库
学Linux的语莫1 小时前
消息队列 MQ 怎么选?RabbitMQ实操
分布式·rabbitmq
原来是猿1 天前
服务端高并发分布式结构演进之路
分布式
LoneEon1 天前
Kafka集群搭建指南:KRaft模式彻底摒弃Zookeeper
分布式·kafka·centos
薪火铺子1 天前
分布式锁深度实战:从 Redis 到 Zookeeper 深度解析
redis·分布式·zookeeper
学习中.........1 天前
高并发架构下的 Kafka 与消息队列核心机制
分布式·kafka
Han.miracle1 天前
分布式部署项目
分布式