Mats Kindahl:构建表访问方法,第8部分:缓存失效
内容提要
本文介绍PostgreSQL表访问方法中缓存失效的实现:后端缓存表arena映射,若其他后端删除或截断表,旧内存不会释放。作者利用relcache失效消息注册回调,在表变更时移除缓存条目;但回调中不能立即解除映射,只能记录待处理句柄,待事务结束时再解除,以避免扫描中途使用失效映射。
延伸解读
缓存失效的触发时机与延迟处理
PostgreSQL 在事务开始、获取新锁或后端追赶失效消息时处理 relcache 失效消息。回调中不能立即解除映射,因为扫描可能正持有指向该映射的缓存条目副本。作者将待解除的句柄存入 stale_handles 列表,等事务结束再统一解除,避免扫描中途使用失效映射。
当前事务新建 arena 的特殊保护
回调中通过 memtable_is_new_arena 检查句柄是否属于当前事务待清理的 arena。因为新 pg_class 行在 finalize 写入 relfilenode 之前就被失效,若直接移除缓存条目,后续 finalize 将找不到条目。该检查依赖 Part 5 中为事务中止注册的清理项来识别新 arena。
解除映射前的占用检查
事务结束时,对每个 stale handle 先调用 memtable_handle_in_use 检查是否有缓存条目重新使用了同一段。在 TRUNCATE 场景中,回调在 TRUNCATE 中途运行,同一事务后续 INSERT 可能已映射新 arena,因此不能盲目解除。已由 memtable_release_arena 解除的映射会被 dsm_find_mapping 跳过。
锁机制提供的额外安全保障
DROP 和 TRUNCATE 需要 ACCESS EXCLUSIVE 锁,与扫描持有的锁冲突,因此其他事务无法在扫描进行时删除或截断表。同一会话中若有活跃扫描,TRUNCATE 会被拒绝。这保证了映射不会在扫描使用期间被解除,但空闲在事务中的会话会持有锁,阻止 DROP 和 TRUNCATE 发生。
Q&A
PostgreSQL 表访问方法中,为什么需要缓存失效机制?
因为每个后端会在自己的缓存中保存表 arena 的映射。如果另一个后端删除或截断表,旧 arena 的内存不会立即释放,直到持有映射的后端断开连接。这可能导致连接池中长期保留大量已删除表的内存,因此需要在表变更时使缓存失效。
如何利用 relcache 失效消息来清理表访问方法的缓存?
通过 CacheRegisterRelcacheCallback 注册回调函数。当 pg_class 中的行发生变化(如 DROP 或 TRUNCATE)时,Postgres 会发送 relcache 失效消息,回调函数收到关系 OID(或 InvalidOid 表示全部失效)后,从缓存中移除对应的条目,以便下次使用时重新附加当前 arena。
为什么在 relcache 失效回调中不能立即解除 arena 映射?
因为回调可能在关系正在使用时被调用,例如扫描中途或游标打开时。扫描会保留缓存条目的副本,该副本指向映射,如果立即解除映射,扫描可能访问已失效的内存。因此回调只移除缓存条目并将过期的句柄记录到列表中,待事务结束时再解除映射。
事务结束时如何安全地解除过期 arena 的映射?
在事务回调中,遍历 stale_handles 列表,对每个句柄调用 dsm_find_mapping 查找映射。如果找到且该句柄未被任何缓存条目重新使用(通过 memtable_handle_in_use 检查),则调用 dsm_detach 解除映射。最后释放列表。
memtable_is_new_arena 函数的作用是什么?
它用于识别当前事务中新建的 arena。在 DDL 命令完成前,新的 pg_class 行可能已被失效,如果回调直接移除缓存条目,后续的 finalize 操作将找不到该条目。该函数通过检查 pending_cleanups 列表中是否有对应句柄的 XACT_EVENT_ABORT 事件项来判断是否为当前事务新建的 arena,从而避免误移除。
缓存失效机制对读写性能有什么影响?
没有在读写行的热路径上增加任何操作。已存在的缓存条目照常返回,失效工作仅在关系变更时和事务结束时进行,因此对常规读写性能无影响。