GBase 8c 序列用在业务流水号上要留几道边界

GBase 8c 序列用在业务流水号上要留几道边界

我最近看 GBase 8c 序列对象资料时,发现序列很容易被低估。很多系统里它只是 nextval 后面跟一个主键字段,看起来不复杂。但真正落到分布式业务和迁移改造现场,序列经常引出几个解释成本很高的问题:为什么编号跳号,为什么回滚后值没回来,为什么设置了 cache 后不保序,为什么不能拿它直接当业务发生顺序。

我自己理解下来,GBase 8c 的序列更适合承担"生成唯一值"的职责,不适合承担"连续业务流水号"的职责。这个边界如果一开始没说清楚,后面财务、审计、运营对账时,就会把数据库正常行为理解成数据异常。

序列不是业务连续号

从资料和实践看,GBase 8c 可以通过 serial 类型定义标识字段,也可以显式创建 sequence,再把字段默认值设置为 nextval('seqname')。这种方式对技术主键很友好,简单、并发友好、生成成本低。

sql 复制代码
-- 方式一:使用 serial 定义标识字段
CREATE TABLE app.t_order_event (
    id serial,
    order_no varchar(64),
    event_time timestamp,
    event_type varchar(32)
);

-- 方式二:显式创建序列,再绑定默认值
CREATE SEQUENCE app.seq_order_event_id CACHE 100;

CREATE TABLE app.t_order_event2 (
    id int NOT NULL DEFAULT nextval('app.seq_order_event_id'),
    order_no varchar(64),
    event_time timestamp,
    event_type varchar(32)
);

ALTER SEQUENCE app.seq_order_event_id OWNED BY app.t_order_event2.id;

但我不会把这个 id 直接展示给业务当"每日流水号"或"凭证号"。原因不是 GBase 8c 特有的问题,而是序列机制本身就不是为了连续编号设计的。事务回滚、缓存、并发连接、会话预取、故障切换,都可能让某些值被消耗但最终没有落到业务表里。

使用目标 是否适合用序列 原因
技术主键 适合 只要求唯一,不要求连续
内部批次号 基本适合 可接受跳号时可以使用
财务凭证号 谨慎 通常要求连续、可解释
对外订单号 不建议裸用 容易暴露业务量和产生跳号争议
事件排序依据 不建议单独使用 序列大小不等于提交顺序

真正需要连续号的场景,我更倾向于单独设计编号服务或业务号表,并把"预占、作废、补号、审计"规则写清楚。用序列做技术主键,再由业务层生成对外编号,会更稳。

cache 的收益和代价要同时写进设计

序列支持 cache 后,可以减少频繁取值的开销。资料里也提到,一旦定义 cache,序列可能产生空洞,并且不能保序。这个点在压测里通常表现为性能收益,在验收里却经常变成疑问。

sql 复制代码
CREATE SEQUENCE app.seq_pay_log_id
    START WITH 1
    INCREMENT BY 1
    CACHE 200;

缓存值越大,性能上的收益可能越明显,但故障或连接释放后可见的跳号也可能越明显。我的做法是把 cache 作为业务属性,而不是只由 DBA 单独决定。

场景 cache 建议 说明
高并发技术主键 可以适当增大 只看唯一性,跳号可接受
低并发配置表 CACHE 1 或较小值 性能压力不大,减少解释成本
对账敏感表 尽量不用序列作业务号 避免跳号争议
临时批处理表 可较大 生命周期短,便于吞吐

我会在设计评审里直接写一句:该字段为技术主键,允许不连续,不作为业务顺序和业务编号依据。这个说明比后面反复解释跳号要省事得多。

回滚不会把 nextval 退回去

这是开发侧最容易误解的一点。很多人以为事务回滚后,数据库状态都回到了之前,序列值也应该退回。实际使用中,nextval 一旦取过,序列就向前走了,事务回滚不会把这个值自动归还。

可以用一个小例子说明。

sql 复制代码
CREATE SEQUENCE app.seq_demo CACHE 1;

BEGIN;
SELECT nextval('app.seq_demo');  -- 假设返回 1
ROLLBACK;

SELECT nextval('app.seq_demo');  -- 很可能返回 2,而不是 1

这不是数据丢失,而是序列为了并发性能做出的机制选择。如果业务要求"失败不能占号",就不该直接用序列满足这个要求。可以考虑在业务提交后再生成正式号,或者生成后保留作废记录。

我在现场一般会把编号分成三类:

编号类型 失败是否允许占号 推荐实现
技术 ID 允许 sequence/serial
业务申请号 视规则而定 sequence + 作废状态
财务凭证号 通常不允许随意跳 独立号段管理 + 审计

很多争议其实不是数据库能力问题,而是没有提前定义"失败是否占号"。

迁移时要检查起始值和归属关系

从 Oracle、PostgreSQL 或其他系统迁到 GBase 8c 时,序列迁移不能只看 DDL。真正容易出问题的是目标库序列当前值小于已有数据最大值,导致新插入时报主键冲突。还有一种是序列没有和目标列建立归属关系,后续删除表或字段时遗留孤儿对象。

迁移后我会跑几类检查。

sql 复制代码
-- 找出表中已有最大值
SELECT max(id) FROM app.t_order_event2;

-- 查看序列下一值,注意 nextval 会推进序列,生产上谨慎直接执行
SELECT nextval('app.seq_order_event_id');

-- 根据已有最大值修正序列
SELECT setval('app.seq_order_event_id', 50000000, true);

-- 建立序列和列的归属关系
ALTER SEQUENCE app.seq_order_event_id OWNED BY app.t_order_event2.id;

如果不想在生产上直接 nextval 推进序列,可以优先从元数据和迁移脚本里核对,再在维护窗口做一次可控验证。

检查项 可能后果 建议动作
序列当前值小于表内最大 ID 新插入主键冲突 迁移后 setval
sequence 未设置 owned by 删除表后遗留对象 补充归属关系
多列共用同一序列 编号混杂,审计困难 拆分序列
cache 设置过大 跳号解释成本增加 按业务调小
兼容模式差异 DDL 行为不一致 建库前确认 DBCOMPATIBILITY

兼容模式也要提前确认。GBase 8c 建库时可以选择不同兼容模式,不同模式下某些语法和类型行为会有差别。序列对象虽然常见,但迁移脚本里如果混用了不同数据库风格的自增写法,最好在测试库完整跑一遍。

不要让多个业务对象共用一个序列

资料里也提示过,虽然数据库不限制一个序列只能为一列产生默认值,但最好不要多列共用一个序列。我非常认同这个建议。多个表共用同一序列,短期看省对象,长期看排障很不方便。

例如下面这种写法,我一般会要求改掉。

sql 复制代码
CREATE SEQUENCE app.seq_global_id CACHE 100;

CREATE TABLE app.t_order (
    id int DEFAULT nextval('app.seq_global_id'),
    order_no varchar(64)
);

CREATE TABLE app.t_refund (
    id int DEFAULT nextval('app.seq_global_id'),
    refund_no varchar(64)
);

问题不在于不能生成唯一值,而在于后续审计、容量估算、迁移修正都会变复杂。订单表突然跳过一大段,可能是退款表消耗了号段;退款表排查编号缺口,又要去看订单写入峰值。技术上能跑,管理上不清晰。

我更倾向于一表一序列,或者至少一类业务一序列。

sql 复制代码
CREATE SEQUENCE app.seq_order_id CACHE 100;
CREATE SEQUENCE app.seq_refund_id CACHE 50;

对象多一点没关系,可维护性会好很多。

序列监控可以很简单

序列很少被放到常规监控里,但大库运行久了以后,最大值风险也要看。特别是仍使用 int 类型的老表,随着业务增长,序列接近上限时再改字段类型,成本会很高。

sql 复制代码
-- 示例:检查序列对象及相关表字段
SELECT
    sequence_schema,
    sequence_name,
    data_type,
    start_value,
    minimum_value,
    maximum_value,
    increment
FROM information_schema.sequences
WHERE sequence_schema NOT IN ('pg_catalog','information_schema')
ORDER BY sequence_schema, sequence_name;

如果环境版本或兼容模式下 information_schema.sequences 信息不完整,也可以结合系统表和 \ds\d 这类 gsql 元命令辅助查看。

我还会把重点序列纳入容量评估:

text 复制代码
序列名:app.seq_order_id
绑定字段:app.t_order.id
字段类型:int
当前最大业务ID:812345678
每日增长:约 120 万
风险判断:两年内接近 int 上限,需要改造为 bigint

比起真正撞到上限再停机处理,提前改字段类型、同步改应用映射、压测回归会稳很多。

小结

GBase 8c 序列很好用,但它的定位要清楚:唯一值生成器,不是连续业务号生成器。cache、回滚、并发、迁移、兼容模式都会影响它在业务侧的表现。我的经验是,设计阶段把"是否允许跳号、是否对外展示、是否作为排序依据、是否允许多对象共用"这几件事说清楚,后面的问题会少很多。

数据库对象越小,越容易被忽略;但序列这种小对象,一旦和业务规则混在一起,解释成本往往不小。

参考资料

text 复制代码
GBase 8c 创建和管理序列(二) https://www.modb.pro/db/473142
GBase 8c 创建和管理序列(三) https://www.modb.pro/db/473143
GBase 8c 兼容模式使用说明 https://www.gbase.cn/community/post/4011
GBase 8c 数据库使用 https://www.gbase.cn/docs/gbase-8c/03%20%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BD%BF%E7%94%A8
相关推荐
2301_779622412 小时前
如何睡眠等待_DBMS_LOCK.SLEEP与DBMS_SESSION暂停当前会话
jvm·数据库·python
零壹AI实验室2 小时前
DeepSeek本地部署:从零开始,把大模型跑在自己电脑上
服务器·网络·人工智能·电脑
2303_821287382 小时前
CSS中如何实现绝对定位元素的等比缩放_利用宽高百分比
jvm·数据库·python
西柚小萌新2 小时前
【计算机常识】--使用 Gitea 在本地/内网搭建 Git 私有服务器
服务器·git·gitea
java修仙传2 小时前
实习日志:完成算法调用总接口并修复联调问题
java·开发语言·数据库
OceanBase数据库官方博客2 小时前
你的数据库是否为 Agent 准备好?
数据库·oracle
2303_821287382 小时前
如何用 Object.defineProperty 为现有对象添加拦截器
jvm·数据库·python
weixin_459753942 小时前
PHP源码运行需要独立显卡吗_显卡对PHP执行有无影响【解答】
jvm·数据库·python
CLX05052 小时前
如何自动同步SQL异构表数据_利用触发器实现实时数据复制
jvm·数据库·python