PostgreSQL堆元组插入时BlockNumber获取的线程安全问题
PostgreSQL插入流程中的线程安全与关系扩展疑问解答
前置背景
执行以下SQL会在PostgreSQL中插入一条堆元组:
CREATE TABLE my_table ( id SERIAL PRIMARY KEY, value varchar(20) ); INSERT into my_table(id, value) VALUES (1,'hey')
插入的大致步骤为:
- 获取ROW EXCLUSIVE LOCK
- 若relcache未缓存,则从系统目录表获取Relation Data
- 选择合适的BlockNumber用于插入
- 将数据写入对应Page
其中步骤3在非批量插入(BulkInsertState为false)时,通过RelationGetTargetBlock方法获取目标块,其定义如下:
#define RelationGetTargetBlock(relation) \ ( (relation)->rd_smgr != NULL ? (relation)->rd_smgr->smgr_targblock : InvalidBlockNumber )
问题1:RelationGetTargetBlock非线程安全,多写入者无锁更新targblock会导致什么问题?
PostgreSQL默认采用多进程架构,每个数据库连接对应独立的后端进程,进程间内存空间完全隔离,RelationData和SMgrRelationData是进程本地缓存,常规场景下不存在跨进程的并发修改问题。但如果是同一进程内的多线程场景(如自定义扩展使用线程),无锁操作会引发以下问题:
- 竞态条件导致重复块选择:多个线程同时读取
smgr_targblock,其中一个线程更新目标块后,其他线程仍基于旧值尝试插入已满的块,触发块空间检查失败,需要重新选择块,增加不必要的开销。 - 更新丢失:并发修改
smgr_targblock时,一个线程的更新可能被另一个线程覆盖,导致后续插入持续尝试写入错误的块,需要多次重试才能找到可用块。
问题2:多写入场景下,RelationSetTargetBlock是否存在线程安全问题?
- 常规多进程场景下,每个后端进程独立维护自己的
smgr_targblock,进程间无共享内存修改,因此不存在线程安全问题。 - 同一进程内的多线程场景中,
RelationSetTargetBlock直接修改SMgrRelationData的smgr_targblock字段,未加锁时会出现更新丢失的竞态问题:多个线程同时修改该字段,后续线程的修改会覆盖之前的结果,导致目标块记录混乱,插入操作频繁重试或选择错误块。
问题3:多写入时获取过时目标块,是否会导致关系持续被过度扩展?
会出现短暂的过度扩展,但不会持续发生,原因如下:
- 当进程尝试插入到过时的目标块时,会先检查块剩余空间,发现已满后调用
RelationAddBlocks扩展新块。高并发场景下,多个进程可能同时执行此操作,导致多个空块被创建。 - 但
RelationGetBufferForTuple方法在目标块不可用时,会重新扫描关系的最后几个块寻找可用空间,后续插入会优先使用这些已创建的空块,不会持续扩展新块。 - 这种过度扩展仅出现在高并发插入的峰值阶段,后续VACUUM操作可以清理未使用的空块(表未被锁定为不可截断状态时)。
内容的提问来源于stack exchange,提问作者scameeer
相关产品推荐
相关产品推荐

