oracle内核实现

31 阅读11分钟

核心思想

  • 页面级多版本管理实现
  • 每个pdb(可插拔db)下会创建一个表空间给undo使用

模块介绍

SGA区

image.png

database buffer cache

image.png

library cache

image.png

buffer cache详细设计

oracle针对磁盘上存的数据,在SGA内存中实现了buffer cache功能来缓存数据,每个buffer对应磁盘上一个block(段->区->block->行),因此Buffer cache中每个buffer大小固定。从buffer中读取数据称为逻辑读,从磁盘block上读取数据,称为物理读。每个buffer都被链接到lru,lru上的节点可以是脏块,也可以是空闲块。oracle为Buffer cache设计了4个lru链表:

  1. 主lru:分热链和冷链,实际上都在一个链表上;主链中查找可用buffer(非脏块且cnt<3的都是可用buffer)时,会从冷链尾部开始找,会跳过脏块和cnt>=3的块,遇到cnt>=3的无论是不是脏块,都会移到热链头部并清空cnt计数,同时会热链尾部的一个buffer挤到冷链头。可用脏块会移动到冷链头部;
  2. 辅助lru:上面链接的buffer都是可以直接被用来写block数据的,缩短从主lru上找可用buffer的时间;SMON后台进程会3s醒来检查主lru和辅助lru存的节点数是不是3:1,如果辅助lru上少于25%,则会从主lru冷链尾开始找可用buffer移动到辅助lru上;数据库刚启动,或者flush过,所有的buffer都会在辅助lru上;
  3. 主lruw:专门链接脏块buffer的,server进程从主lru冷链尾部查找可用buffer的过程中,会将遇到的cnt<3的脏块移动到lruw中;
  4. 辅助lruw:上面链接的脏buffer都会在dbwr进程醒来后,立即刷到磁盘上;dbwr进程醒来后,会将lruw上的脏块移到辅助lruw上,并从辅助lruw上读取脏块写盘,看起来是为了缩短持有主lruw latch的时间,减少锁冲突。

buffer上维护有cnt计算,3s内被访问到就会加1且3s内只会加1次(避免很快的被移到热链中,大表扫描场景,会使用辅助lru上的buffer且cnt保持为0,不会访问主lru)。
为了加快数据buffer的查找效率,实现了hash链表来存储buffer的地址,key是文件号+块号hash生成,rowid中存储着这部分信息。server先通过别的手段定位到想访问数据的rowid,解析出文件号+块号,hash查找hash表,如果没找到,那么就会从lru上找一个可用的buffer并将磁盘block加载到该buffer中,存到hash表上。
多个bucket共用一个latch来保证并发场景正确性。

buffer cache刷脏机制

脏buffer除了记录在lruw上外,还会存到checkpoint队列上,checkpoint上记录的buffer都是由“不脏”转为“脏”块的buffer,对于已经是脏块的buffer,不会多次追加到队列尾,checkpoint队列上记录的脏块顺序和redo log中buffer变为脏块时生成redo record的顺序一致,首次变脏生成的redo record地址称为LRBA(这个信息是不是得在写到日志文件中才能知道)。检查点队列头部记录着最先需要刷到磁盘的buffer的LRBA;
DBWR进程3s醒来后刷脏流程:

  1. 检查lruw链表是否有buffer节点,主lruw上的节点会移到辅助lruw上,然后立即开始刷到磁盘上:根据buffer header上记录的文件号和块号,找到要刷的磁盘block,并将脏块从checkpoint队列上移除;
  2. 检查checkpoint队列长度,如果超过阈值,确定要刷的个数,继续刷盘,并更新队列头部的LRBA;
  3. 刷脏块时,oracle会将脏块移到所属对象的队列上,尝试检查是否有相邻的块并进行合并,一起写到磁盘上,减少io次数。

CheckPoint Process-CKPT

image.png

oracle的checkpoint进程/线程每3秒检查内存使用是否超过PGA_AGGREGATE_LIMIT设置的值,如果达到了,则触发db writer进程读取buffer cache然后将修改的 数据写到数据文件中,在完成检查点请求后,CKPT会更新数据文件头和控制文件,以记录最近的检查点即LRBA。 实例恢复时,实际上就是恢复checkpoint队列上的脏buffer,根据记录的LRBA定位到redo log的某个位置,开始依次回放数据。

LogWriter Process-LGWR

image.png

oracle的db可以以多进程架构来运行也可以多线程,参数可配,默认多进程.
Log writer也可以是独立进程或者线程。oracle执行dml时,数据修改先在redo log buffer中写redo日志,然后在buffer cache中更新数据;log writer负责异步将redo log buffer中的日志顺序写到在线redo log file中.
oracle记录redo log有两种模式:IMU(In Memory Undo)模式和非IMU模式。
非IMU模式下:每执行一条dml,都会按照wal原则修改SGA内存中的数据:

  1. 先生成undo操作的redo record(record header + 包含undo操作的后映像数据,即当前值,redo change等信息),拷贝到redo log buffer(循环写的缓冲区)中;
  2. 再生成表数据的redo record记录(包含redo操作的后映像数据),也拷贝到log buffer中;
  3. 然后更改内存中undo buffer中的数据(这里还有一个版本链维护的工作吧);
  4. 再修改表对应行buffer中的数据,至此dml语句执行完成;
  5. 提交时,会再生成一条redo record,拷贝log buffer,然后将log buffer中的数据顺序写到log file中。

IMU模式下:
执行dml时,会推迟redo日志的生成到提交阶段,并将三条redo record合并成一条,减少写redo相关锁冲突开销和redo条数(信息不会减少).

  1. 共享内存中会存在一个In memory undo区域,dml执行时,不会先生成redo log,而是先读取表块的前映像数据,存到IMU区域并在数据所在地址附近生成修改undo块的redo change信息,不会实际去修改undo块的buffer;
  2. 在共享内存的私有redo区生成修改数据块对应的redo change信息;
  3. 将数据块对应buffer修改为新值,buffer变为脏块,至此dml执行完成;
  4. 提交时:
    • 将imu中的前映像数据写到undo块的buffer中(这一步可能是smon后台进程提前完成了,否则就是server进程来做);
    • 将修改表块使用的新值和生成的redo change信息拷贝到log buffer中;
    • 将提交的redo日志写到log buffer中;
    • 将undo块对应的前映像值和生成的redo change信息拷贝到log buffer中;
    • 将log buffer中的日志一次性写到redo文件中;

IMU模式下,多版本得结合undo段的版本链+imu区域来实现。

MVCC实现

Oracle data block结构:

  1. block header:
    • Block address:文件号+block号,唯一标识此块;
    • 表目录:表明存储的数据属于哪个表;
    • 行目录:行指针数组,指向每行数据起始地址;
    • ITL(interested transaction list):slots数组,每个slot记录正在或者曾经修改此块的事务:
      • XID:事务id;
      • UBA:uba=dba.seq#.record#如(0x0080104d.00a1.6e),指向事务在当前block上修改所使用的undo块中最新的一条undo record;
      • SCN:提交时填写的system change number,事务未提交时为空。通过该值来判断是否可以读取事务的修改。
      • FLAG:事务标志位,如事务是否提交等;
  2. 行数据空间:
    • 存储行数据:行头会记录修改当前行的事务在ITL中的槽位,以及表示行是否锁定;
  3. 空闲空间: 可用于新行插入,或者现有行的更新;

回滚段:

  • 段头:记录一个事务表,每个事务开始时,都会在系统分配的回滚段段头中申请一个槽位,生成一条事务record:
    • Index(槽位号)+state(事务状态)+scn+dba(执行最新前映像 undo块)等信息;
  • Undo block:块头+多个undo record组成:
    • 块头:记录xid,seq,irb(指向未提交事务最新修改生成的undo record),block包含的所有record的offset
    • Undo record:
      • 头部信息:
        • Rci:记录事务使用的上一个record offset,把一个事务中的所有修改生成的undo record连接起来,用于回滚整个事务;
        • Uba:单个dml中所有修改生成的undo record通过该值连接起来,实现语句级回滚;
      • 行前映像数据;

image.png

每个事务修改data block生成的所有undo record都可以通过ITL中的uba+record header的uba+rci串联起来,也就是可以找到所有前映像数据.
事务提交时,会在ITL中写入scn值,已提交事务在ITL中占用的槽位可以被重用,如果被重用了,重用前会将当前槽位上记录的信息写到新事务分配的undo block中,因此可以通过读undo block中的信息找到上一个修改data block的事务信息,从而找到对应的前映像数据.
查询时,找到所需data block中的行后,根据行头记录的信息判断是否有事务正在处理,如果有则找到对应ITL中的事务信息,根据查询语句持有的scn和事务scn比较:

  • 如果是未提交事务且读语句在同一个事务中,那么可以直接读取对应修改;
  • 如果不在同一个事务中,那么根据当前事务的undo block记录的前事务信息找到对应的scn来比较,依次下去,直到找到第一个版本号小于查询语句scn,然后通过事务的uba可找到所有record记录的前映像数据,结合data block构造一个CR块(一致性读块)供查询使用。

语句级回滚使用ITL的uba找到最新的undo record->通过record header记录的uba找到所有当前语句产生的修改,回滚即可.
事务级回滚使用ITL的uba找到最新的undo record->通过record header记录的rci找到事务的所有修改产生的record,回滚即可.

每个事务有个undo data segment,回滚段中的区段形成一个环,事务采取循环写的策略将信息写入区的block中,每个事务有个当前区,不同的活跃事务可以写入同一个当前区,或者不同,oracle会先优先保证每个事务独享一个回滚段,空间不足时,才会出现多个事务共享同一个回滚段的情况。在单个区内,一个data block仅包含一个事务的数据;当前区写满后,需要空间的第一个事务会检查下一个区段的可用性,如果不包含活跃事务的数据,那么会成为当前区.
回滚段重用规则:

  1. 维护一个seq值,某个事务在分配的回滚段中写数据时,单个区的所有block的seq值相同,跨区时,会递增seq;
  2. 需要跨区写时,会检查要写的区中是否有活跃事务(未提交),待写区记录的seq是否小于当前seq,满足条件才会复用,否则尝试从表空间中扩展一个区,如果有,则链接到当前区和下一个包含活跃事务区之间;如果没有,那么尝试从其他回滚段中偷取可重用的区(以区为单位).

事务中的dml修改数据时,定位到data block buffer后,会去block header中记录的ITL中查找可用槽位并占据(会写redo日志),写入事务信息,然后将要访问的buffer中的对应数据行头记录该ITL(表示行被锁定),将前映像数据拷贝到回滚段的undo record中,行修改为新数据.

事务提交时:

  1. 产生一个scn,事务表中记录着已提交事务,已提交事务被设置一个唯一scn,这个scn同时被记录到事务表中;
  2. log writer将redo log buffer中剩余的日志条目和事务的scn写到在线redo log中;
  3. 释放持有的行锁和表锁;等锁的事务被唤醒继续执行;
  4. 删除所有savepoint;
  5. 如果已提交事务的修改过的data block还在SGA中,且没有其他session正在修改这些数据块,oracle会进一步从block中删除锁相关的事务信息(ITL entry);理想情况下,commit阶段就会清理掉,这样后续执行的select语句就不用做这个动作。如果特定行在ITL中没有表项,表示没有被锁,否则就表示可能被锁,会话需要检查undo segment的header部分来确定相关的事务是否已提交,如果已提交,会话会清理ITL entry并生成redo log,如果先前的commit已经清理了,就不需要检查header和清理了;
  6. 标记事务完成;