在业务系统里,为每条记录分配一个不重复的标识是基础需求。关系型数据库通常给出两条路径:使用序列函数产出递增整数,或使用UUID生成无序唯一字符串。二者在存储、性能与扩展模型上差异明显,选型前需要先弄清原理。

一、序列函数生成唯一序列号
序列(Sequence)是数据库维护的一个独立计数器对象,它通过原子操作保证每次调用都返回唯一且递增的值。以PostgreSQL为例,可以用CREATE SEQUENCE定义一个序列,然后在插入数据时通过nextval函数获取下一个值。这种方式生成的序列号是纯数字,体积小、插入B+树索引时不会产生随机写,因此写入性能很好。
下面是一段PostgreSQL中创建序列并使用的示例:
-- 创建从1开始、步长为1的序列
CREATE SEQUENCE order_seq START 1 INCREMENT 1;
-- 建表时引用序列作为默认值
CREATE TABLE orders (
id BIGINT DEFAULT nextval('order_seq') PRIMARY KEY,
order_no TEXT,
created_at TIMESTAMP
);
-- 插入数据,id自动从序列获取
INSERT INTO orders (order_no, created_at)
VALUES ('A001', NOW());
序列函数的优势在于简单高效,数据库自己保证并发安全,不需要应用层做任何处理。但它也有明显短板:序列通常绑定单个数据库实例,在主从复制或分库分表环境中,不同节点若各自维护序列,就可能生成重复号;另外,业务量暴涨时单一序列可能成为写入瓶颈。
为缓解单点问题,一些团队会采用步长错开策略,比如节点A的序列INCREMENT设为2、起始1,节点B起始2,这样两节点各取奇偶值避免冲突。不过这种办法在节点数变多时配置繁琐,且序列值容易被外部推测,不适合做对外的订单号。
二、UUID生成唯一序列号
UUID(通用唯一识别码)是一个128位的标识符,标准格式以36字符字符串呈现,例如550e8400-e29b-41d4-a716-446655440000。它不依赖中心数据库,而是结合时间戳、网卡MAC、随机数等因子计算,理论上重复概率极低。SQL中多数现代数据库都内置了UUID函数,如MySQL的UUID()、PostgreSQL的gen_random_uuid()。
使用UUID做主键或序列号的写法非常直接:
-- PostgreSQL生成UUID主键
CREATE TABLE user_log (
log_id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
content TEXT,
ts TIMESTAMP
);
-- 插入时无需指定log_id
INSERT INTO user_log (content, ts)
VALUES ('login success', NOW());
-- MySQL中直接用UUID()
INSERT INTO session (sid, info) VALUES (UUID(), 'test');
UUID最大的好处是去中心化,应用任意节点都能独立生成,天然适配微服务与多数据中心。但它也有代价:字符串占16字节(二进制)或36字节(文本),比 BIGINT 的8字节大不少;并且因为无序,新插入的主键值随机分散在索引各处,会导致InnoDB等聚簇索引频繁页分裂,写入吞吐不如序列。
为降低UUID的索引碎片,业界推出过UUIDv1按时间排序、以及Snowflake式有序UUID。如果必须在SQL层使用,可优先选用数据库提供的有序UUID扩展,或者在应用层用雪花算法算好再写入,避免纯随机UUID拖慢表。
三、两种方案对比与选型
我们把核心差异整理成下表,方便对照:
| 维度 | 序列函数 | UUID |
|---|---|---|
| 唯一性保障 | 单库内绝对唯一,跨库需人工规划 | 全局极高概率唯一 |
| 存储空间 | 8字节(BIGINT) | 16字节二进制或36字节文本 |
| 写入性能 | 顺序写,索引友好 | 随机写,易页分裂 |
| 分布式支持 | 弱,需步长等妥协方案 | 强,无中心依赖 |
| 可读性 | 短数字,便于排查 | 长字符串,不直观 |
从表中可以看出,如果系统仍是单库且追求极致写入,序列函数是首选;当业务已经分库分表,或需要离线终端也能发号,UUID更稳妥。实践中也有混合做法:数据库用序列做内部主键,对外暴露的订单号则用UUID或雪花算法生成,兼顾性能与安全。
另外要注意,不论用哪种方式,都应在表上建立唯一约束,防止程序bug或手动改数据引入重复。序列本身不防人为指定相同值插入,UUID虽碰撞率低但仍建议留一道约束兜底。
四、应用层结合SQL的注意事项
在代码里调用序列或UUID时,尽量让数据库自己填默认值,而不是先查号再插。例如用MyBatis时可写
<insert id="addOrder">
INSERT INTO orders (order_no, created_at)
VALUES (#{orderNo}, #{createdAt})
</insert>
上述写法依赖表的DEFAULT nextval或gen_random_uuid,减少一次往返。若用JDBC手动拿序列,也要放在同一个事务里,避免并发时号被跳过造成浪费。对于UUID,如果数据库版本较老没有内置函数,可以在Java侧用UUID.randomUUID().toString()生成后传入,但需确认字段类型是UUID或CHAR(36)。
最后提醒,序列号不等于业务流水号。序列函数给的1、2、3容易暴露销量,对外下单接口最好映射一层不可推测的编码,而UUID虽不可推测却太长,用户抄写易错,所以面向前端的单号往往要再做格式化,比如取UUID中段加日期前缀,这也是为什么很多系统内部用序列、外部用衍生号。