Author: baotiao
前面介绍过 PostgreSQL Buffer Access 5 Rules 里面的 Pin 机制, 其实 HeapTable + Pin 机制还带来了其他和 InnoDB 的区别.
InnoDB Persist cursor 是用来保留查询遍历 btree 过程中 index tree cursor 的位置.
为什么需要 persist cursor?
因为有多种场景下, 比如在执行大查询时, 查到一行以后, 需要将数据返回给 server 层处理. 这时, 你不能在返回指向 Record 的指针 index tree cursor 后依然持有该 Page 的锁 (Latch).
如果锁不放开, 在上层处理数据的期间, 这个 Page 就无法被修改.
如果锁放开, 这个 Page 有可能被修改了, 那么指向 Record 的指针就失效了, 需要重新定位这个 index tree cursor.
所以有了 btr_pcur_restore_position() 这个操作, 对应的把 pcur position 保存下来的操作就是 store_position() 操作
比如在 row_ins_clust_index_entry_low, row_upd_sec_index_entry_low 这些路口函数里面, 都需要申请
btr_pcur_t pcur;
然后通过 btr_pcur_open(index, entry, PAGE_CUR_LE, mode, &pcur, &mtr);
把 pcur 指向 btree 的指定位置
可以认为, InnoDB 的 Persistent Cursor 设计和 PostgreSQL 的 Heaptable + Pin 机制走了两条不同的路.
我们都会遇到一个问题: 在执行大查询时, 查到一行以后, 需要将数据返回给上层处理. 这时, 你不能在返回指向 Record 的指针后依然持有该 Page 的锁 (Latch). 如果锁不放开, 在上层处理数据的期间, 这个 Page 就无法被修改.
因此, MySQL InnoDB 的做法是:
所以 InnoDB 的逻辑很符合直观: 读完一行并返回给上层处理期间, 把 Page 的锁放掉, 允许其他事务对该 Page 进行修改. 当要处理下一行数据时, 判断 Page 是否被修改过: 如果没有修改, 就复用位置信息; 如果修改了, 就重新走一遍遍历 B-Tree 的逻辑.
相比之下, PostgreSQL 的 Heaptable + Pin 机制走了另一条路. 它保证 Record 所在的 Page 位置不会发生改变. 通过给 Page 加一个 Pin 约束, 如果有人想修改 Page (比如进行整理, 重组等操作), 只要该 Page 被 Pin 住, 就不允许发生 Record 位置的偏移.
InnoDB IOT 对比 PostgreSQL HeapTable
由于 PostgreSQL 这种位置修改的频率是批量的, 没有 MySQL 那么频繁, 所以它选择了另外一条路: 把 Record 的位置定住. 如果后台 Vacuum 发现这个 Page 被 Pin 住了, 就跳过它, 直接去处理下一个 Page.
总结来看, 选择这两套策略主要有以下原因:
PostgreSQL 的 Heaptable + Pin 和 InnoDB 的 Persist Cursor 机制相比, 带来的一个优势是可以做到全链路的读取不需要拷贝.
InnoDB 读路径上的拷贝
比如在最常见的 row_search_mvcc 里面
row_search_mvcc 的核心序列是这样:
1. latch 住 leaf page
2. cursor 定位到 record, 拿 rec_t 裸指针
3. MVCC 可见性判断
4. 还持着 latch 的时候, 调 row_sel_store_mysql_rec, 把 record 转成 MySQL row format, copy 到 prebuilt->row_buf
5. 释放 latch (或 btr_pcur_store_position 存位置)
6. 把 row 返回 server
关键在第 4 步和第 5 步的顺序: InnoDB 是先 copy 出来, 才释放 latch. copy 出来是为了尽快可以把 page latch 释放掉, 减少锁的占用. 一旦 latch 放掉, buffer 里那条 record 随时可能被改 / 搬走, 所以必须趁还持 latch, 先把数据 copy 到一块 latch 之外的内存 (row_buf), 之后才敢放 latch.
这里即使不存在 MySQL row format 和 InnoDB row format 格式不一致的问题, 也需要有这样的拷贝, 核心还是因为这个 Page latch 释放了以后, 这个 pcur 存的 record 的内容有可能被 inplace update.
之前跑 TPCC 的经验, 在 oltp_readonly 没有明显热点的场景下, 这里的内存拷贝是一个瓶颈点.
PostgreSQL 读路径上的拷贝
没有. heap_getnextslot 返回的 slot 里挂 BufferHeapTuple, t_data 直接指 buffer 字节; executor 节点之间传 slot 不 copy; 访问列走 heap_getattr 直接解 t_data 后面的字节. visibility check 找到哪个版本可见, 就读那条 tuple 的 t_data, 老版本本来就在 heap 里原地, 不需要重建.
只有显式物化点才 copy: Sort / HashJoin build / Material 节点进 tuplestore, WITH HOLD cursor 在 commit 时物化, trigger 访问 OLD/NEW, TOAST 列 detoast, 以及最终发给客户端. 单一 SELECT 链条上的过滤, 投影, join 不 copy.
当然 Pin 不是白来的. reader 持 pin 期间, vacuum 在那个 page 上拿不到 cleanup lock, 该 page 的物理整理被推迟 (page 级, non-aggressive vacuum 跳过即可). 另外老版本堆在 heap 里, 要靠 vacuum 回收. PG 用读路径的零拷贝换来了 vacuum 这一侧的负担.