目录
tag{(16821,16384,37721),1,3}表示16821号表空间的16384号数据库的37721号表的空闲空间映射(0为关系本体main分支,2位VM分支)的第3页。
缓冲表为散列表,通过散列函数,可根据缓冲区标签tag找到对应的buffer_id,即数据在缓冲池中的槽位号。散列表通过分离链接方法解决冲突。
缓冲区描述符层包括与之相关的缓冲池槽位的元数据信息,包括buffer_id,缓冲区标签tag,当前访问页面的进程数refcount,访问次数usage_count,标志脏位的flags,指向下一个描述符的freeNext(所以缓冲区描述符层也是一个链表),以及context_lock与io_in_progress_lock两个锁,后面会介绍。
缓冲区描述符有三种状态:
空:缓冲池槽中无页面,refcount与usage_count均为0。
钉住:当前页面正在被访问,refcount与usage_count均大于等于1。
未钉住:存在该页面,但目前没有进程访问,refcount为0,usage大于等于1(因为至少将其载入缓冲池槽的进程使其加了1)。
其中空缓冲区描述符组成了一个freelist链表。
缓冲池有多个槽位组成,是一个数组,每个槽位为8k,正好可加载一个磁盘页面。
该锁为缓冲区管理器同步化的一部分,与SQL语句与操作中的锁无关。
维护缓冲表的锁,分为读取的共享锁与增删的独占锁。
为了不影响其他事务对缓冲表的读写,提高并发量,pg将散列表划分为16组,每组一个锁,一次锁多个桶。
内容锁:读取页面时的共享锁以及独占锁,独占锁在以下几种情况下分配:
IO进行锁
进程加载、写入页面时分配。
自旋锁
检查或更新元数据信息字段时获取,如更新refcount等。9.6版后,使用原子操作替代自旋锁。
通过Tag1与散列函数,找到散列表上的桶,
获取桶所在区的缓冲表共享锁,在该桶中找到tag1对应的buffer_id1(一个桶中可能有多个buffer_id)。
钉住buffer_id1的缓冲区描述符,将ref_count与usage_count分别加1.
释放共享锁,访问buffer_id1的缓冲池槽。
1.通过Tag1与散列函数,找到散列表上的桶,获取桶所在区的缓冲表共享锁,在该桶中没找到tag1。释放共享锁。
2.从freelist(空缓冲区描述符链表)中获取空缓冲区描述符,并将其钉住,假设获得的是buffer_id2,以独占模式获取桶所在分区的锁。
3.创建一条tag=1,buffer_id=2的数据项,插入桶中。
4.获得排他的IO锁,将数据页面加载到buffer_id=2的缓冲池插槽中,标记vaild为1,释放IO锁。
5.释放桶所在分区的锁。
6.访问buffer_id=2的插槽。
将页面从存储加载到受害者缓冲池槽
以时钟的方式扫描缓冲区描述符,跳过被钉住的描述符,对于未被钉住的描述符,如果其usage_count为0,则将其置换出去,若不为0,则将其减1,然后扫描下一个:
(深色的表示被钉住的)
当需要读写大表时,需要大量置换缓冲池,这会导致之后的缓存命中率降低。所以pg使用额外的环形缓冲区来替代缓冲池加载大表。当有以下情况时,会启用环形缓冲区:
COPY FROM 命令。
CREATE TABLE AS 命令。
CREATE MATERIALIZED VIEW 或 REFRESH MATERIALIZED VIEW 命令。
ALTER TABLE 命令。
除了置换脏页时需要脏页刷盘外,检查点进程会在检查点开始时进行脏页刷盘;
后台写入进程通过少量多次的脏页刷盘来减少检查点的密集写入,默认为200ms一次,默认最多刷100个页。