PostgreSQL SERIAL在单插入语句中是否保证无间隙?并发ID是否连续?
这个问题问到点子上了——在高并发场景下,自增ID的连续性经常是业务里容易踩坑的点,咱们得结合不同数据库的具体实现来拆解:
首先直接给你一个核心结论:绝大多数主流数据库的默认配置下,你没法保证单条语句插入的X条记录ID是严格连续的A到A+X-1,甚至会出现你担心的「其他线程插的ID夹在你的批次中间」的情况,不过你说的「线程1占A+100、线程2占A+99」这种倒序情况几乎不会发生,因为自增ID本质是全局递增的,只是并发分配会打乱批次内的连续性。
接下来分数据库细说:
MySQL(InnoDB引擎)
MySQL的自增ID机制和innodb_autoinc_lock_mode这个参数密切相关:
- 默认配置(innodb_autoinc_lock_mode=1):对于「可预测行数的批量插入」(比如明确写了
INSERT ... VALUES (...), (...), ...共X条),数据库会一次性提前分配好X个连续的自增ID给这条语句,这时候不管其他线程怎么并发插入,你的这X条记录的ID肯定是连续的——因为分配ID的操作是原子性的,不会被打断。但如果是「不可预测行数的插入」(比如INSERT ... SELECT或者LOAD DATA),数据库会用逐行分配ID的方式,这时候其他线程的插入就会插进来,导致你的ID不连续。 - 并发模式(innodb_autoinc_lock_mode=2):这个模式下锁粒度更细,为了提升并发性能,即使是可预测行数的批量插入,也会逐行分配ID,这时候其他线程的插入就会打断你的ID连续性,你的X条记录ID可能会夹杂其他线程的ID。
PostgreSQL(SERIAL/BIGSERIAL)
PostgreSQL的自增ID是靠独立的「序列(Sequence)」实现的,每次插入记录时会调用nextval()获取下一个序列值。这里的关键是:nextval()是全局原子操作,但单条语句插入多条记录时,会多次调用nextval(),而这些调用之间是可以被其他线程的nextval()插进来的。
举个例子:你用单条语句插入10条记录,刚获取到第1个ID(比如100),另一个线程的插入语句也获取了ID101,然后你的语句继续获取ID102,结果你的10条记录ID就是100、102、103...中间夹了一个其他线程的101,完全没法保证连续。
另外,PostgreSQL的序列默认有缓存(CACHE参数),如果数据库重启,缓存里未使用的序列值会丢失,就算没有并发也可能出现ID断号的情况。
通用建议
不管用哪种数据库,我都强烈建议你不要依赖自增ID的连续性来实现业务逻辑:
- 自增ID的设计初衷是给每条记录一个唯一标识,不是用来做连续编号的;
- 并发、数据库重启、事务回滚(比如插入后回滚,ID不会回退)都会导致ID断号;
- 如果业务需要连续的编号,最好单独维护一个编号生成器(比如用单独的表存当前编号,加锁获取),不要和数据库自增ID绑定。
内容的提问来源于stack exchange,提问作者Robert Pankowecki

