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