序列(Sequence)是生成唯一 ID 的利器,但用不好就是并发瓶颈。我见过一个系统,序列设了 NOCACHE,1000 并发插入时,enq: SQ - contention 占 DB Time 的 35%,TPS 只有 800。改成 CACHE 1000 后,争用消失,TPS 涨到 12000。这篇文章把序列的底层原理(SEQ$ 基表、缓存机制)、CACHE vs NOCACHE 的性能差异、ORDER vs NOORDER 的 RAC 影响、以及序列 GAP 问题拆清楚。
1 序列不是内存对象,是基表+缓存
Oracle 的序列信息存在系统基表 SEQ$ 里。当会话调用 seq.NEXTVAL 时,如果缓存里有值,直接返回;如果缓存用完了,会话去更新 SEQ$ 的 HIGHWATER(增加缓存大小),然后把新值加载到内存。这个"去基表拿值"的过程需要获取 SQ 锁(Sequence Enqueue),如果多个会话同时耗尽缓存,就会争用 SQ 锁,等待事件是 enq: SQ - contention。
CACHE 的作用就是减少访问 SEQ$ 的频率。CACHE 20 意味着每次更新 SEQ$ 时,一次性拿 20 个值到内存,后续 19 次 NEXTVAL 不需要访问基表。CACHE 1000 意味着每 1000 次才访问一次 SEQ$。NOCACHE 意味着每次 NEXTVAL 都要访问 SEQ$,高并发下 SQ 锁争用爆炸。
2 ORDER vs NOORDER:RAC 环境下的序列一致性
ORDER 保证序列值按请求顺序分配,即使在 RAC 多节点环境下也是全局有序的。实现方式是每次 NEXTVAL 都同步到其他节点,代价是全局缓存融合(Global Cache Fusion)开销巨大。NOORDER 不保证全局有序,每个节点有自己的缓存,性能最好。绝大多数业务不需要全局有序的序列(比如订单 ID 不需要严格递增,只需要唯一),所以 RAC 环境一定要用 NOORDER。
3 序列 GAP:不是 bug,是特性
序列值不连续(GAP)的原因:事务回滚(拿了序列值但 INSERT 回滚了)、实例崩溃(缓存里的值丢了)、CACHE 刷新(ALTER SEQUENCE 导致缓存失效)、导入导出(IMP/IMPDB 可能跳过序列)。很多财务系统要求 ID 严格连续,用序列就错了——序列天生不保证连续。需要连续 ID 的,得用表+行锁(MAX(id)+1),但性能极差。
4 实验:CACHE 大小对并发性能的影响
实验环境:Oracle 19c,单实例。测试表 seq_test,1000 并发会话同时插入。
-- 创建三种序列
CREATE SEQUENCE seq_nocache NOCACHE NOCYCLE;
CREATE SEQUENCE seq_cache20 CACHE 20 NOCYCLE;
CREATE SEQUENCE seq_cache1000 CACHE 1000 NOCYCLE;
-- 测试存储过程
CREATE OR REPLACE PROCEDURE test_seq(p_seq_name VARCHAR2) AS
BEGIN
FOR i IN 1..1000 LOOP
EXECUTE IMMEDIATE 'INSERT INTO seq_test(id, seq_name) VALUES ('||p_seq_name||'.NEXTVAL, '''||p_seq_name||''')';
COMMIT;
END LOOP;
END;
/
-- 100 个会话同时执行:
-- 会话 1-100:EXEC test_seq('seq_nocache');
-- 会话 101-200:EXEC test_seq('seq_cache20');
-- 会话 201-300:EXEC test_seq('seq_cache1000');
-- 监控等待事件
SELECT event, total_waits, time_waited_micro/1000 time_ms
FROM v$system_event
WHERE event LIKE '%SQ%';
-- 监控 TPS
-- 用 AWR 或自定义脚本统计每秒插入数
实验结果:NOCACHE 序列,enq: SQ - contention 每秒 8000 次,1000 并发下 TPS 仅 950。CACHE 20 序列,SQ 争用每秒 400 次,TPS 4200。CACHE 1000 序列,SQ 争用每秒 8 次,TPS 11800。CACHE 从 20 提到 1000,性能提升 2.8 倍;NOCACHE 到 CACHE 1000,提升 12 倍。
但 CACHE 1000 的代价是:实例崩溃时,最多丢 1000 个序列值。对于要求严格连续的业务(如发票号),这不可接受。但对于订单 ID、流水号,GAP 无所谓。
5 那个财务系统要求连续 ID 的悲剧
客户是财务系统,要求凭证号必须连续,不能跳号。开发一开始用序列,发现经常有 GAP,被会计骂。后来改成"SELECT MAX(voucher_no)+1 FROM vouchers FOR UPDATE",确实连续了,但高并发时 enq: TX - row lock contention 占 60%,TPS 只有 300。最后妥协方案:数据库层用 CACHE 1000 序列保证性能和唯一性,应用层在提交前检查连续性,把不连续的号留到月底"调账"时统一填补。虽然麻烦,但平衡了性能和业务需求。
【踩坑笔记】序列 CACHE 值别设太大,否则实例重启后 GAP 太大,财务可能找你麻烦。一般 OLTP 设 100-1000,RAC 环境每个节点独立缓存,CACHE 要设更大(比如 2000),减少跨节点 SQ 争用。另外,ALTER SEQUENCE 会导致缓存刷新,生产环境没事别乱改。
6 总结:序列设计的三条军规
第一,高并发必须设 CACHE,NOCACHE 是自杀。第二,RAC 必须设 NOORDER,ORDER 是性能毒 药。第三,如果业务要求严格连续,别用序列,用表+锁(但要接受性能代价),或者应用层补号。最后送一句话:序列是并发世界的"发号机",发号机快不快,看缓存大不大;号连不连续,看你能不能接受意外。