如何在Java Spring+Oracle数据库环境下生成无间隙的主键ID
Oracle 多并发场景下无间隙主键实现方案
无间隙主键的核心要求是ID分配必须和业务事务强绑定,因此无法使用Oracle原生序列(序列生成与事务生命周期 decouple,回滚必然产生间隙),以下是两种生产环境可落地的方案,按适用场景区分:
方案一:专用序号控制表 + 行级锁(实时获取ID,适用TPS≤1000的场景)
这是满足硬性无间隙要求的最通用方案,通过事务级锁保证ID分配和业务操作同生共死:
- 新建序号控制表,存储各业务线的当前序号值
CREATE TABLE sequence_generator ( seq_name VARCHAR2(50) PRIMARY KEY, current_value NUMBER(18) NOT NULL, step NUMBER(5) DEFAULT 1 NOT NULL ); -- 初始化业务对应的序列 INSERT INTO sequence_generator(seq_name, current_value, step) VALUES ('NUM_SEQ', 0, 1); COMMIT;
- 编写ID获取函数,通过行级排他锁保证同一时间只有一个事务能拿对应序列的ID
CREATE OR REPLACE FUNCTION get_next_seq(p_seq_name VARCHAR2) RETURN NUMBER IS v_next_val NUMBER; BEGIN -- 行级锁会持有到当前事务提交/回滚,其他事务拿同序列ID会阻塞等待 SELECT current_value + step INTO v_next_val FROM sequence_generator WHERE seq_name = p_seq_name FOR UPDATE; UPDATE sequence_generator SET current_value = v_next_val WHERE seq_name = p_seq_name; RETURN v_next_val; END; /
- Spring 集成方式
在业务事务开启后第一时间调用函数获取ID,再执行后续业务逻辑:
@Repository public interface SeqRepository extends JpaRepository<SequenceGenerator, String> { @Query(value = "SELECT get_next_seq('NUM_SEQ') FROM DUAL", nativeQuery = true) Long getNextNumSeq(); }
如果事务回滚,序号表的更新会同步回滚,不会产生间隙;不同业务线的序列互相独立,不会互相阻塞。
方案二:异步分配序号(高并发场景,允许ID延迟获取)
如果业务允许写入时不立即拿到无间隙序号,仅要求最终序号无间隙,可以用该方案获得极高的并发性能:
- 业务表先用普通Oracle序列作为主键(开启缓存提升写入性能),额外新增
biz_seq字段存储无间隙序号,默认值为NULL - 启动单线程后台任务,按主键/创建时间的升序,给
biz_seq为NULL的记录依次分配连续序号
单线程分配完全不存在并发冲突,也不会有间隙,业务写入逻辑完全不需要加锁,适合高并发写入、序号仅用于事后展示的场景(比如账单编号、工单编号等)
避坑提醒
- 不要尝试用
max(id)+1加全表锁的方式实现,锁粒度远大于序号表的行级锁,并发性能会差几个数量级 - 不要在原生序列上浪费时间,无论怎么调整参数都无法解决事务回滚导致的间隙问题
内容的提问来源于stack exchange,提问作者WattMonki
相关产品推荐
相关产品推荐

