ORACLE原理机制
时间:2010-09-23 来源:ORA-admin
ORACLE的工作机制-1
肖亚峰(xyf_tck) 我们从一个用户请求开始讲,ORACLE的简要的工作机制是怎样的,首先一个用户进程发出一个连接请求,如果使用的是主机命名或者是本地服务命中的主机名使用的是机器名(非IP地址),那么这个请求都会通过DNS服务器或HOST文件的服务名解析然后传送到ORACLE监听进程,监听进程接收到用户请求后会采取两种方式来处理这个用户请求,下面我们分专用服务器和共享服务器分别采用这两种方式时的情况来讲:
专用服务器模式下:一种方式是监听进程接收到用户进程请求后,产生一个新的专用服务器进程,并且将对用户进程的所有控制信息传给此服务器进程,也就是说新建的服务器进程继承了监听进程的信息,然后这个服务器进程给用户进程发一个RESEND包,通知用户进程可以开始给它发信息了,用户进程给这个新建的服务器进程发一个CONNECT包,服务器进程再以ACCEPT包回应用户进程,至此,用户进程正式与服务器进程确定连接。我们把这种连接叫做HAND-OFF连接,也叫转换连接。另一种方式是监听进程接收到用户进程的请求后产生一个新的专用服务器进程,这个服务器进程选用一个TCP/IP端口来控制与用户进程的交互,然后将此信息回传给监听进程,监听进程再将此信息传给用户进程,用户进程使用这个端口给服务器进程发送一个CONNECT包,服务器进程再给用户进程发送一个ACCEPT包,至此,用户进程可以正式向服务器进程发送信息了。这种方式我们叫做重定向连接。HAND-OFF连接需要系统平台具有进程继承的能力,为了使WINDOWS NT/2000支持HAND-OFF必须在HKEY_LOCAL_MACHINE>SOFTWARE>ORACLE>HOMEX中设置USE_SHARED_SOCKET。
共享服务器模式下:只有重定向连接的方式,工作方式是监听进程接收到用户进程的请求后产生一个新的调度进程,这个调度进程选用一个TCP/IP端口来控制与用户进程的交互,然后将此信息回传给监听进程,监听进程再将此信息传给用户进程,用户进程使用这个端口给调度进程发送一个CONNECT包,调度进程再给用户进程发送一个ACCEPT包,至此,用户进程可以正式向调度进程发送信息了。可以通过设置MAX_DISPIATCHERS这个参数来确定调度进程的最大数目,如果调度进程的个数已经达到了最大,或者已有的调度进程不是满负荷,监听进程将不再创建新的调度进程,而是让其中一个调度进程选用一个TCP/IP端口来与此用户进程交互。调度进程每接收一个用户进程请求都会在监听进程处作一个登记,以便监听进程能够均衡每个调度进程的负荷,所有的用户进程请求将分别在有限的调度进程中排队,所有调度进程再顺序的把各自队列中的部分用户进程请求放入同一个请求队列,等候多个ORACLE的共享服务器进程进行处理(可以通过SHARED_SERVERS参数设置共享服务器进程的个数),也就是说所有的调度进程共享同一个请求队列,共享服务器模式下一个实例只有一个请求队列,共享服务器进程处理完用户进程的请求后将根据用户进程请求取自不同的调度进程将返回结果放入不同的响应队列,也就是说有多少调度进程就有多少响应队列,然后各个调度进程从各自的响应队列中将结果取出再返回给用户进程。
以上我们讲完了用户与ORACLE的连接方式,下面我们要讲ORACLE服务器进程如何处理用户进程的请求,当一个用户进程发出了一条SQL语句:UPDATE TABBLEA SET SALARY=SALARY*2;首先服务器进程将对该语句进行检查语句有效性的语法检查和确保语句能够正常运行的语义检查,首先检查该语句的语法的正确性(语法检查),接着对照数据字典对语句中涉及的表、索引、视图等对象及用户的权限进行检查(语义检查),如果以上任一检查没有通过,就返回一个错误,但不会明确的指出是语法检查出错还是语义检查出错,它只会返回一个ORA-*****的错误码。如果检查通过以后,服务器进程把这条语句的字符转换成ASCII等效数字码(注意SQL中使用*是个例外,如果表的字段改变了,同样是SELECT * FROM TABLEA转换成的ASCII是不同的,其实它在语义检查时就明确的变成了操作具体字段的SQL语句了),接着这个ASCII码被传递给一个HASH函数,并返回一个HASH值,服务器进程将到SHARED POOL的共享PL/SQL区去查找是否存在同样的HASH值,如果存在,服务器进程将使用这条语句已高速缓存在SHARED POOL中的已分析过的版本来执行(软解析),如果不存在,则必须进行以下两个步骤:语句的优化(生成执行计划)和生成执行编码:服务器进程根据ORACLE选用的优化模式以及数据字典中是否存在相应对象的统计数据和是否使用了存储大纲来生成一个执行计划或从存储大纲中选用一个执行计划,最后再生成一个编译代码(硬解析)。(这里要注意的是,语法语义分析在前,计算HASH_VALUE在后,算出HASH_VALUE后只要找到相同的HASH_VALUE就使用这条缓存执行计划,语义分析在前确保了用户的使用权限等问题,不存在算出HASH_VALUE,再找到相同HASH_VALUE缓存执行计划而不能使用的情况。也不是先算HASH_VALUE,然后找缓存执行计划,找到后再语义检查这个步骤也是错的)ORACLE将这条语句的本身实际文本、HASH值、编译代码、与此语句相关联的任何统计数据和该语句的执行计划缓存在SHARED POOL的共享PL/SQL区。V$librarycache中的几个参数解释Pins: (Execution)即SQL实际执行的次数,不包括用户提交的语法语义检查失败的SQL。Reloads: (Parse)未找到相同HASH_VALUE的次数,即必须进行硬解析的次数。Invalidations: (Parse)因对象更改,使得所有引用这个对象的缓存执行计划失效而必须再次硬解析的次数。只要DDL更改了一个对象,所有与此有关的缓存在共享池中执行计划都将立即失效,它的失效不是在下次执行SQL时才发现其失效,而是DDL更改对象后立即就失效。主要表现在DDL发生后v$sql的HASH_VALUE仍保持不变,但PLAN_HASH_VALUE立即变为0,再次运行SQL语句时则会向v$sql插入一条新的缓冲记录HASH_VALUE,PLAN_HASH_VALUE都重新计算。原来的缓冲记录仍然还存在。 服务器进程通过SHARED POOL锁存器来申请可以向哪些共享PL/SQL区中缓存这些内容,也就是说被SHARED POOL锁存器锁定的PL/SQL区中的块不可被覆盖,因为这些块可能正在被其它进程所使用。在SQL分析阶段将用到LIBRARY CACHE,从数据字典中核对表、索引、视图及用户的权限的时候,需要将数据字典从磁盘读入LIBRARY CACHE,因此,在读入之前也要使用LIBRARY CACHE锁存器来申请用于缓存数据字典。
ORACLE的工作机制-2
肖亚峰(xyf_tck) 生成编译代码之后,接着下一步服务器进程要准备开始更新数据,服务器进程将到DB BUFFER中查找是否有相关对象的缓存数据,下面分两个可能进行解释:
如果没有,服务器进程将在表头部请求一些行锁,如果成功加锁,服务器进程将从数据文件中读入这些行所在的第一个数据块(db block)(DB BLOCK是ORACLE的最小操作单元,即使你想要的数据只是DB BLOCK中很多行中的一行或几行,ORACLE也会把这个DB BLOCK中的所有行都读入DB BUFFER中)放入DB BUFFER中空闲的区域或者覆盖已被挤出LRU列表的非脏数据块缓冲区,并且排列在LRU列表的头部,如果这些非脏数据缓冲区写完也不能满足新数据的请求时,会立即触发DBWN进程将脏数据列表中指向的缓冲块写入数据文件,并且清洗掉这些缓冲区,来腾出空间缓冲新读入的数据,也就是在放入DB BUFFER之前也是要先申请DB BUFFER中的锁存器,成功锁定后,再写入DB BUFFER,然后把这个块的头部事务列表及SCN信息及被影响的行数据原值写入回滚段中,以便ORACLE在ROLLBACK时可以利用当前数据块和回滚段重构数据块的"前映像"或递归重构出"前…前映像"来实现读一致性。(回滚段可以存储在专门的回滚表空间中,这个表空间由一个或多个物理文件组成,并专用于回滚表空间,回滚段也可在其它表空间中的数据文件中开辟。)然后在LOG BUFFER中生成日志,服务器程将该语句影响的被读入DB BUFFER块中的这些行的ROWID及将要更新的原值和新值及SCN等信息,以及回滚段的修改信息(即对某某回滚段地址进行了什么修改)逐条的写入REDO LOG BUFFER,在写入REDO LOG BUFFER之前也是先请求REDO LOG BUFFER块的锁存器,成功锁定之后才开始把REDOLOG写入REDOLOG BUFFER。当写入达到REDO LOG BUFFER大小的三分之一或写入量达到1M或超过三秒后或发生检查点时或者COMMIT时或者DBWN之前触发LGWR进程,LGWR将把REDO LOG BUFFER中的数据写入磁盘上的重做日志文件,已被写入重做日志文件的REDO LOG BUFFER中的块上的锁存器被释放,并可被后来写入的信息所覆盖。回滚段其实也有BUFFER(在DB BUFFER中开辟),回滚段BUFFER中的内容是最早向磁盘上回滚段中写的,写完这些才会生成日志BUFFER中的内容,原因是日志中必须要记录回滚段的新旧变化以便在恢复时从日志中的记录的回滚段新旧变化对回滚段再次重写,记住,REDO不光是对数据文件依据日志文件重写,也要依据日志文件对回滚段重写,而且重写回滚段要先于重写数据文件,要理解REDO就是重来一遍,所谓重来一遍就要跟正常的的先后顺利一样重做一遍(正常的操作中的顺序就是先读入DB BUFFER,写回滚段buffer,后写回滚段,后写日志BUFFER,后改写DB BUFFER,后写日志最后写数据文件)区别是REDO时不用再记日志了,这样解释后相信大家应该理解为什么日志中也必须要记录回滚段的信息了,只有这样才可以对正常操作中的一个ROLLBACK动作进行恢复,即在REDO过程中利用即时重写的数据块和回滚段重构出一个当时适用的前镜像来rollback。当一个重做日志文件写满后,LGWR将切换到下一个重做日志文件,重做日志文件也是循环工作方式。如果是归档模式,归档进程还将前一个写满的重做日志进程写入归档日志文件。当DB BUFFER改写之后,服务器进程在脏数据列表中建立一条指向此DB BUFFER缓冲块的指针。接着服务器进程会从数据文件读入第二个数据块(db block)重复以上读入,建立回滚段,写LOG BUFFER,改写DB BUFFER,放入脏列表的动作,当脏数据列表达到一定长度时,DBWN进程将脏数据列表中指向的缓冲块全部写入数据文件,也就是释放加在这些DB BUFER块上的锁存器,并在修改相应块的头部的SCN号(一次UPDATE操作只对应一个SCN)。前面说过DBWN动作之前会先触发LGWR,这用以确保写入数据文件的改变首先会被记录在日志文件中。实际上ORACLE可以从数据文件中一次读入多个块放入DB BUFFER,然后再对这些块建回滚段、再记日志等等,也就是每次操作的对象是DB BLOCK的复数,而不仅限于一次操作一个DB BLOCK,可以通过参数DB_FILE_MULTIBLOCK_READ_COUNT来设置一次读入的块的个数。注意,不管是否提交,用户的所有更改都会被记录在日志文件中,用户级回滚的动作(rollback)没有对应的COMMIT SCN。在密集事务的情况下,LGWR可以把多个COMMIT产生的REDO条目批量写入REDO LOG FILE,但每个COMMIT之间有十分之一秒的间隔,且会产生不同的COMMIT SCN。LGWR正常情况下是一个休眠进程,会被一定的条件触发,唤醒,比如COMMIT就是一个唤醒条件,一旦LGWR被唤醒,LGWR将把唤醒时间点之前LOG BUFFER中产生的所有内容(从上次LGWR唤醒后到本次唤醒前之间写入LOG BUFFER的内容)写入LOG FILE,直到LGWR完成后,LGWR才可以被再次触发,在LGWR触发到完成期间所有对数据库的操作仍然可以不间断的加入LOG BUFFER。在这段时间内,LGWR不再接收其它条件的触发,比如紧跟前一个COMMIT之后的其它COMMIT(复数)都要等待LGWR完成后才可以再次触发LGWR,并在LGWR下次被触发时,将积累的REDO BUFFER条目一次性写入REDO LOG,后继的COMMIT不会单个单个的触发LGWR。
如果要查找的数据已缓存,则根据用户的SQL操作类型决定如何操作,如果是SELECT则查看DB BUFFER块的头部是否有事务,如果有,将利用回滚段进行重构出一致性块再读取,如果没有则比较SELECT的SCN与DB BUFFER块头部的SCN如果比自己大,仍然同上,如果比自己小则认这是一个非脏缓存,可以直接从这个DB BUFFER块中读取。如果是UPDATE则即使在DB BUFFER中找到一个没有事务,而且SCN比自己小的非脏缓存数据块,服务器进程仍然要到表的头部对这条记录申请加锁,加锁成功则进行后续动作,如果不成功,则要等待前面的进程解锁后才能进行动作。
ORACLE的工作机制-3
肖亚峰(xyf_tck) 只有当SQL语句影响的所有行所在的最后一个块被读入DB BUFFER并且重做信息被写入REDO LOG BUFFER(仅是指重做日志缓冲,而非重做日志文件)之后,用户才可以发出COMMIT,COMMIT触发LGRW,但并不强制立即DBWN来释放所有相应的DB BUFFER块上的锁,也就是说有可能出现已COMMIT,但在随后的一段时间内DBWN还在写这条语句涉及的数据块的情形,表头部的行锁,并不是在COMMIT一发出就马上释放,实际上要等到相应的DBWN进程结束才会释放。一个用户请求锁定另一个用户已COMMIT的资源不成功的机会是存在的。COMMIT发出后会将回滚段中的"前映像"标识为已提交.DML语句会产生一个SCN号,DBWN触发时写入到数据块的头部,COMMIT时也会产生一个SCN号,也会被写入数据块的头部。在数据块的头部只存储一个最新的SCN号,COMMIT之后这个事务插槽可以被另外一个事务使用。
如果用户ROOLBACK,则服务器进程会根据数据文件块和DB BUFFER中块的头部的事务列表和SCN以及回滚段地址重构出相应的修改前的副本,并且用这些原值来还原当前数据文件中已修改但未提交的改变。如果有多个"前映像",服务器进程会在一个"前映像"的头部找到"前前映像"的回滚段地址,一直重构出同一事务下的最早的一个"前映像"为止。一旦发出了COMMIT,用户就不能ROLLBACK,这使得COMMIT后DBWN进程还没有全部完成的后续动作得到了保障。
下面我们要提到检查点的作用,ckpt的触发,有以下几种情况:
1.当发生日志组切换的时候
2.当满足log_checkpoint_timeout、log_checkpoint_interval、fast_start_io_target、fast_start_mttr_target参数设置的时候
3.当运行alter system switchlogfile的时候
4.当运行alter systemckeckpoint的时候
5.当运行alter tablespacetbs_name begin backup[end backup]的时候
6.当运行alter tablespace[datafile] offline的时候
7.系统正常关闭时
只有在4.7两种情况下发生完全检查点。发生完全检查点时,首先系统记录检查点对应的Checkpoint SCN,并记录下该时刻修改的DB BUFFER对应的日志文件的最新的重做字节地址(Redo Byte Address (RBA)),然后DBWN进程将这个重做字节地址(RBA)之前已发生的DB BUFFER中的脏缓冲写入数据文件(之所以要以重做字节地址(RBA)为标志是因为在检查点发生到检查点完成之间的时间内,系统还在一直不断的产生修改,这些修改所产生的DB BUFFER脏缓冲,以及日志条目将不会影响这次检查点最后确认的一致性结果,也就是最后确认这个Checkpoint SCN之前的系统是一致的)。最后把Checkpoint SCN和RBA更新至控制文件,Checkpoint SCN更新至每个数据文件头部,表明当前数据库是一致的。日志切换并不导致一个完全检点的发生,比如有三个日志文件组,当发生日志切换时发生检查点,而发生日志切换一般是因为当前的LGWR正在写重做日志,也就是LGWR当刚写满2号日志就立即触发检查点,于是系统开始核对3号日志中记录的REDO项目所对应的数据是否已经从DB BUFFER中写入数据文件(不管事务是否已提交),如果没有写入,检查点就触发DBWN进程将这些缓冲块写入数据文件,显然LGWR因此而发生等待,除此以外,检查点还让DBWN进程将在2号日志中对应修改的DB BUFFER块写入数据文件,然后继续LGWR进程,直到LGWR进程将LGWR触发之前存在于REDO LOG BUFFER中的所有缓冲(包含未提交的重做信息)写入重做日志文件,检查点再更新数据文件,控制文件头部SCN。其实LGWR等待的并不是CKPT的完成,而是等待CKPT触发的DBWN进程的完成。可以想像断电时可能既有未COMMIT的事务,也可能同时存在已COMMIT但DBWN未完成的情况,如果断电时有一个已COMMIT但DBWN动作没有完成的情况存在,因为已经COMMIT,COMMIT会触发LGWR进程,所以不管DBWN动作是否已完成,该语句将要影响的行及其产生的结果一定已经记录在重做日志文件中了,则实例重启后,SMON进程从控制文件中记录的上一次重做字节地址(RBA)开始,按照重做日志文件中的条目对数据文件和回滚段重新做一遍即前滚,注意这些条目的操作在断电之前有的已经被DBWN写入了数据文件,有的还没有来得及写,不管有没有写进数据文件,前滚时都会再重新写一次(9I之前是这样的),9I之后,由于也在日志中记录了DBWN改写的块信息,系统会过滤掉已写入的条目而只重做那些未写入的条目。对于一个未提交事务,分几种情况来描述:1)LGWR与DBWN一致的情况即一个语句执行完成后很长时间也没有COMMIT,这种情况一般不存在DBWN来不及完成的情况。只是没有COMMIT而已。那么SMON将在前滚完成后,利用回滚段重构出具有最小SCN的前映像,并把它的值写回原位。2)事务执行中断电,即可能存在LGWR与DBWN不同步的情况(因为DBWN之前会触发LGWR,所以DBWN对数据文件的修改一定会被先记录在重做日志文件中。因此只可能存在已写入重做日志而未来得及写入数据文件的情况存在。而不可能存在已写入数据文件却没有写入日志文件的情况。),这种情况下SMON也会先前滚一点(即把数据文件与相应的日志文件先同步再回滚,之所以说前滚一点,是指仅LGWR与DBWN之间进度的差距,而不是把这条语句进行到底再回滚,因为日志文件中记录的是执行语句操作的一个个块的修改信息,而不只是记录一条执行语句的字面内容),然后利用回滚段重构出具有最小SCN的前映像,并把它的值写回原位。由此可见,实例失败后用于恢复的时间由两个检查点之间的间隔大小来决定,我们可以通个四个参数设置检查点执行的频率,LOG_CHECKPOINT_IMTERVAL决定了两个检查点之间写入重做日志文件的系统物理块的大小,LOG_CHECKPOINT_TIMEOUT决定了两个检查点之间的时间长度,FAST_START_IO_TARGET决定了用于恢复时需要处理的块的大小,FAST_START_MTTR_TARGET直接决定了用于恢复的时间的长短。检查点的作用就是不断的确认LGWR与DBWN之间的同步情况,以便实例失败后从上一个检查点开始恢复,问题是两个检查点之间LGWR与DBWN大部分的操作是同步的,只是一小部分没有同步,这种传统的检查点使实例恢复做了比较多的无用功,因此,ORACLE引入了增量检查点,增量检查点会在上一次传统检查点发生后到下一次传统检查点发生之前,不断的更新记录在控制文件中重做字节地址(RBA)(CKPT进程每三秒更新一次,见下面DBWN讲述),这样实例失败后将直接从控制文件中记录的最后更新的重做字节地址(RBA)开始进行前滚和回滚,这就省略掉了恢复时大部份的重做日志的重做(即使在9I以后的版本里也省略掉了大部分的过滤重做日志条目的时间)。(对以上描述做一个简单的比喻:比如一个贸易公司下设经营部、货运部、监督部,经营部负责贸易合同的签订与记录,货运部负责按合同号的顺序把货物送达,监督部负责定期检查确认经营部签订的合同与货运部货物送达情况之间的同步情况,监督部每月检查一次,每次检查时,先确认当时正在装车的货物的合同号,并要求货运部把在这个合同号之前的所有还存在临时仓库中的未送货物全部送达。等货运部完成监督部下达的任务后,监督部在检查本上记录下本次开始检查时那票正在装车的货物的合同号,本次检查完成。如果这个公司发生了一次人事大换血,公司重新开业后,监督部就会从检查本上记录的合同号开始,检查在这之后所有发生的合同及货物送达情况,要求货运部把所有客户确认的但还未送达的货物送达。把所有客户未确认的货物收回。监督部发现这次重新开业后的工作量实在是太大了,几乎核对了整整一个月的几万单合同(好在不是半年检查一次^_^),为了防止今后出现这种情况,监督部增加了一项工作内容,每三天派人去记录一下货运部正在装车的那票货的合同号,今后如果发生类似情况,监督部就从最后一次记录的合同号开始核对,这样工作量就小多了。)
ORACLE的工作机制-4
肖亚峰(xyf_tck) 在这里我们要说一下回滚段存储的数据,假如是delete操作,则回滚段将会记录整个行的数据,假如是update,则回滚段只记录行被修改了的字段的变化前的数据(前映像),也就是没有被修改的字段是不会被记录的,假如是insert,则回滚段只记录插入记录的rowid。这样假如事务提交,那回滚段中简单标记该事务已经提交;假如是回退,则如果操作是delete,回退的时候把回滚段中数据重新写回数据块,操作如果是update,则把变化前数据修改回去,操作如果是insert,则根据记录的rowid把该记录删除。注意,检查点除了触发LGWR和DBWN向数据块头部写SCN和COMMIT SCN,检查点还向控制文件和数据文件头部写SCN,而用户的DML和COMMIT仅是向数据块头部写SCN和COMMIT SCN而不更新控制文件和数据文件的SCN,SMON的前滚是以文件头部的SCN为起始点的也就是从前一个检查点开始,SMON的回滚是回滚所有回滚段中未标识为已提交的数据块,用户的回滚是回滚与此事务有关的回滚段中未标识为已提交的数据块。
下面我们要讲DBWN如何来写数据文件,在写数据文件前首先要找到可写的空闲数据块,
ORACLE中空闲数据块可以通过FREELIST或BITMAP来维护,它们位于一个段的头部用来标识当前段中哪些数据块可以进行INSERT。在本地管理表空间中ORACLE自动管理分配给段的区的大小,只在本地管理的表空间中才能选用段自动管理,采用自动段空间管理的本地管理表空间中的段中的空闲数据块的信息就存放在段中某些区的头部,使用位图来管理(最普通的情况是一个段的第一个区的第一个块为FIRST LEVEL BITMAP BLOCK,第二个块为SECOND LEVEL BITMAP BLOCK,第三个块为PAGETABLE SEGMENT HEADER,再下面的块为记录数据的数据块,FIRST LEVEL BITMAP BLOCK的父数据块地址指向SECOND LEVEL BITMAP BLOCK,SECOND LEVEL BITMAP BLOCK的父数据块地址指向PAGETABLE SEGMENT HEADER,FIRST LEVEL BITMAP BLOCK记录了它所管理的所有块(包括头部三个块,不仅仅指数据块)的状态,标识的状态有Metadata、75-100% free、50-75% free、25-50% free、0-25% free、full、unformatted,在SECOND LEVEL BITMAP BLOCK中有一个列表,记录了它管理的FIRST LEVEL BITMAP BLOCK,PAGETABLE SEGMENT HEADER中记录的内容比较多,除了记录了它管理的SECOND LEVEL BITMAP BLOCK,还记录了各个区的首块地址以及各个区的DB BLOCK的个数,段的各个区所对应的FIRST LEVEL BITMAP BLOCK的块地址以及区里面记录数据的数据块的起始地址。如果一个区拥有很多块,这时会在一个区里出现两个或多个FIRST LEVEL BITMAP BLOCK,这些FIRST LEVEL BITMAP BLOCK分别管理一个区中的一些块,当区的数据块比较少时,一个区的FIRST LEVEL BITMAP BLOCK可以跨区管理多个区的数据块,BITMAP BOLCK最多为三级)。采用手动管理的本地管理表空间中的段和数据字典管理的表空间中的段中的空闲数据块的管理都使用位于段头部的空闲列表来管理,例如SYSTEM表空间是本地管理表空间,但是它是采用了手动段空间管理,所以也是用FREELIST来管理段中的空闲数据块的。空闲列表是一个逻辑上的链表,在段的HEADER BLOCK中记录了一个指向第一个空闲块的BLOCK ADDRESS,第一个DB BLOCK中同时也记录了指向下一个空闲块的BLOCK ADDRESS。以此形成一个单向链表。如果段上有两个FREE LIST则会在段头部的HEADER BLOCK存有两个指针分别指向两个空闲块并建立独立的两个单向链表。空闲列表的工作方式:首先当建立一个段时,初始分配的第一个区的第一个块会成为段的头块,初始分配的第一个区的其它块将全部加入空闲列表,再次扩展一个区时,这个区中的块立即全部加入空闲列表,扩展一次加入一次。与位图管理不同的是用空闲列表时区的头部将不记录区里面空闲块的信息。当其中空闲空间小于PCTFREE设置的值之后,这个块从空闲列表删除,即上一个指向它的块中记录的下一个空闲块地址更改为其它空闲块的地址,使得这个块类似于被短路,当这个块中的内容降至PCTUSED设置的值之下后,这个数据块被再次加入空闲列表,而且是加入到空闲列表前端,即头块直接指向它,它再指向原头块指向的空闲块,位于空闲列表中的数据块都是可以向其中INSERT的块,但是INSERT都是从空闲列表指向的第一个块开始插入,当一个块移出了空闲列表,但只要其中还有保留空间就可以进行UPDATE,当对其中一行UPDATE一个大数据时,如果当前块不能完全放下整个行,只会把整个行迁移到一个新的数据块,并在原块位置留下一个指向新块的指针,这叫行迁移。如果一个数据块可以INSERT,当插入一个当前块装不下的行时,这个行会溢出到两个或两个几上的块中,这叫行链接。如果用户的动作是INSERT则服务器进程会先锁定FREELIST,然后找到第一个空闲块的地址,再释放FREELIST,当多个服务器进程同时想要锁定FREELIST时即发生FREELIST的争用,也就是说多个进程只在同时INSERT时才会发生FREELIST争用,可以在非采用自动段空间管理的表空间中创建表时指定FREELIST的个数,默认为1,如果是在采用自动段空间管理的表空间中创建表,即使指定了FREELIST也会被忽略,因为此时将使用BITMAP而不是FREELIST来管理段中的空闲空间。采用自动段空间管理还会忽略的参数有PCTUSED和FREELIST GROUPS。如果用户动作是UPDATE或DELETE等其它操作,服务器进程将不会使用到FREELIST和BITMAP,因为不要去寻找一个空闲块,而使用锁的队列。对数据块中数据操作必须使用transaction entries,即事务入口。在建立段时我们可以通过MINTRANS和MAXTRANS参数指定它的最大值和最小值,MAXTRANS规定了在段中每一个块上最大并发事务数量,可以输入1到255之间的值。我们可以把它比喻为是一些长在块头部的事务插座,每个插座后面是一个可以伸缩的操作手,当事务进程插到一个插座上时相当于找到一个可以操作数据块中数据行的操作手,通过这个操作手,事务进程可以对块中数据进行INSERT、UPDATE、DELETE等操作。在没有超过MAXTRANS设定的最大值时,如果transaction entries不够用,则会在块上自动分配一个,但不会影响其它块中的transaction entries数量。只不过INSERT操作必须要先找到空闲块然后才能INSERT。
那么DBWN是根据什么顺序来写DB BUFFER中的脏数据的呢?ORACLE从8I开始加入新的数据结构--检查点队列(Buffer Checkpoint Queue)。检查点队列是一个链接队列。这个队列的按照Buffer块第一次被修改的顺序排列,分别指向被修改的Buffer块。在DB_Buffer中的数据被第一次被修改时,会记录所生成的REDO LOG条目的位置RBA作为该Buffer的Low RBA,记录在该Buffer的头部(Buffer Header),如果该数据继续被修改,则把该块修改的最新的REDO LOG的RBA作为High RBA记录在该Buffer的头部。如果DB_Buffer中的块没有被修改的数据,则该块的头部不会有Low RBA和High RBA的信息。检查点队列按照被修改块的Low RBA的递增值链接修改块,没有被修改的块因为没有Low RBA,而不会加入到检查点队列中。在没有检查点发生时DBWR就按照检查点队列的Low RBA的升序,将被修改的块写入到数据文件中。当块被写入到数据文件后,该块会从检查点队列中断开。DBWR继续写下一个块。CKPT进程每三秒记录检查点队列中对应的最小Low RBA到控制文件中,也就是更新控制文件中的CheckPointRBA,当实例崩溃时,恢复将从CheckPointRBA所指向的日志位置开始。这就是"增量检查点"的行为和定义。CKPT进程也会记录检查点位置到数据文件的头部,但是只是日志切换时才写。而不是每三秒。当检查点发生时,DBWN不会一直不停的写DB BUFFER中脏数据,它将写到检查点队列的开始块的Low RBA的值大于该检查点的Checkpoint RBA的值时停止写入,然后完成这次检查点,CKPT进程将记录该检查点的有关信息到控制文件中去。
ORACLE的工作机制-5
肖亚峰(xyf_tck) 下面来讲一下ORACLE锁的机制,分锁存器和锁两种。锁存器是用来保护对内存结构的访问,比如对DB BUFFER中块的锁存器申请,只有在DBWN完成后,这些DB BUFFER块被解锁。然后用于其它的申请。锁存器不可以在进程间共享,锁存器的申请要么成功要么失败,没有锁存器申请队列。主要的锁存器有SHARED POOL锁存器,LIBRARY CACHE锁存器,CACHE BUFFERS LRU CHAIN锁存器,CACHE BUFFERS CHAINS锁存器,REDO ALLOCATION锁存器,REDO COPY锁存器。ORACLE的锁是用来保护数据访问的,锁的限制比锁存器要更宽松,比如,多个用户在修改同一表的不同行时,可以共享一个表上的一个锁,锁的申请可以按照被申请的顺序来排队等候,然后依次应用,这种排队机制叫做队列(ENPUEUE),如果两个服务器进程试图对同一表的同一行进行加锁,则都进入锁的申请队列,先进的加锁成功,后面的进程要等待,直到前一个进程解锁才可以加锁,这叫做锁的争用,而且一旦加锁成功,这个锁将一直保持到用户发出COMMIT或ROOLBACK命令为止。如果两个用户锁定各自的一行并请求对方锁定的行的时候将发生无限期等待即死锁,死锁的发生都是由于锁的争用而不是锁存器的争用引起的,ORACLE在遇到死锁时,自动释放其中一个用户的锁并回滚此用户的改变。正常情况下发生锁的争用时,数据的最终保存结果由SCN来决定哪个进程的更改被最终保存。两个用户的服务器进程在申请同一表的多个行的锁的时候是可以交错进入锁的申请队列的。只有其中发生争用才会进行等待。创建表时指定的MAXTRANS参数决定了表中的一个数据块同时最多可以被几个事务锁定。
下面来讲一下ORACLE的读一致性机制,ORACLE的读一致性保证了事务之间的高度隔离性。
下面是几个关于回滚段读一致性和死锁的事例:
有表:Test (id number(10)) 有记录1000000条
SQL> create table test (idnumber(10))tablespaceusers;
表已创建。
SQL> begin
2 foriin 1..1000000 loop
3 insertinto test values(i);
4 endloop;
5 commit;
6 end;
7 /
PL/SQL过程已成功完成
一,大SELECT,小UPDATE
A会话----Select * from test;----设scn=101----执行时间09:10:11
B会话-----Update test set id=9999999 where id=1000000----设scn=102-----执行时间09:10:12
我们会发现B会话会在A会话前完成,A会话中显示的ID=100000是从回滚段中读取的,因为A会话在读到ID=1000000所在的BLOCK时发现BLOCK上有事务信息,因此要从回滚段中读,如果UPDATE在SELECT读到此BLOCK之前已经COMMIT,则SELECT读到此BLOCK时发现其BLOCK上没有事务信息,但是会发现其BLICK的SCN比SELECT自己的SCN大,因此也会利用回滚段进行重构。根据当前块上所有的itl找到相应的undo地址,重构出之前的block image,之前的那个block又含有自己的itl信息.如果这个before image中对应的scn不满足查询,又会根据undo生成beforebeforeimage,这样不断往复,直到构造出符合查询的scn的block返回结果,或者系统实在无法根据undo构造出符合查询的block,,报ora-01555的错误为止....。需要强调的是读一致性是通过对当前整个块利用回滚段(当前块上的所有ITL记录的所有回滚段地址)进行递归重构到过去某一时间点或某一SCN的块的一致性快照。而不是只针对块中一部分ITL记录在回滚段中递归查找来完成的,一定要理解递归重构与递归查找是完全不同的两个概念。Oracle回滚段确保了事务的高度的隔离性。即只要回滚段足够大,那么一个SELECT不管执行多长,它读取的所有数据都将是在这条SELECT语句开始执行瞬间这个时间点的值,而不会被其它用户在SELECT读取期间对数据是否做过修改而影响。
二、大UPDATE,小SELECT
A会话----Update test set id=1;----设scn=101----执行时间09:10:11
B会话-----select * from test where id=1000000----设scn=102-----执行时间09:10:12
我们会发现B会话会在A会话前完成,B会话中显示的ID=1000000是从BLOCK中直接读取的,因为B会话在读到ID=1000000所在的BLOCK时,A会话还没有来得及对其锁定,因此B会话既不会发现BLOCK上有事务信息,也不会发现BLOCK上的SCN比SELECT的大,因此会从BLOCK中直接读取,如果SELECT在UPDATE锁定此BLOCK后才发出,B会话读到此BLOCK时发现其BLOCK上有事务信息,因此会从回滚段中读取。
三、大UPDATE,小UPDATE
A会话----Update test set id=1;----设scn=101----执行时间09:10:11
B会话1-----Update test set id=999999 where id=1000000----设scn=102-----执行时间09:10:12
B会话2----- select * from test where id=2----设scn=103-----执行时间09:10:14
B会话3----- update test set id=3 where id=2----设scn=104-----执行时间09:10:15
我们会发现B会话1会完成,A会话将一直等待,因为B会话1会先于A会话锁定ID=1000000所在的BLOCK,并改写头部的事务信息,A会话在试图锁定此BLOCK时,发现其上有事务信息,将会一直等待B会话1事务结束后再行锁定,B会话2查询到的ID=2是从回滚段中读取的而不是从BLOCK中直接读出来的。因为A会话已将ID=2的BLOCK锁定,并写入了回滚段,从B会话3可以证明这一点,B会话3发出后,B会话3会收到死锁的信息,死锁的原因是A会话在等待B会话对ID=1000000所在的BLOCK解锁,现在B会话又在等待A会话对ID=2所在的BLOCK解锁,因此形成死锁,因此证明ID=2所在的BLOCK已被A会话锁定,然后A会话也会收到死锁的信息
肖亚峰(xyf_tck) 我们从一个用户请求开始讲,ORACLE的简要的工作机制是怎样的,首先一个用户进程发出一个连接请求,如果使用的是主机命名或者是本地服务命中的主机名使用的是机器名(非IP地址),那么这个请求都会通过DNS服务器或HOST文件的服务名解析然后传送到ORACLE监听进程,监听进程接收到用户请求后会采取两种方式来处理这个用户请求,下面我们分专用服务器和共享服务器分别采用这两种方式时的情况来讲:
专用服务器模式下:一种方式是监听进程接收到用户进程请求后,产生一个新的专用服务器进程,并且将对用户进程的所有控制信息传给此服务器进程,也就是说新建的服务器进程继承了监听进程的信息,然后这个服务器进程给用户进程发一个RESEND包,通知用户进程可以开始给它发信息了,用户进程给这个新建的服务器进程发一个CONNECT包,服务器进程再以ACCEPT包回应用户进程,至此,用户进程正式与服务器进程确定连接。我们把这种连接叫做HAND-OFF连接,也叫转换连接。另一种方式是监听进程接收到用户进程的请求后产生一个新的专用服务器进程,这个服务器进程选用一个TCP/IP端口来控制与用户进程的交互,然后将此信息回传给监听进程,监听进程再将此信息传给用户进程,用户进程使用这个端口给服务器进程发送一个CONNECT包,服务器进程再给用户进程发送一个ACCEPT包,至此,用户进程可以正式向服务器进程发送信息了。这种方式我们叫做重定向连接。HAND-OFF连接需要系统平台具有进程继承的能力,为了使WINDOWS NT/2000支持HAND-OFF必须在HKEY_LOCAL_MACHINE>SOFTWARE>ORACLE>HOMEX中设置USE_SHARED_SOCKET。
共享服务器模式下:只有重定向连接的方式,工作方式是监听进程接收到用户进程的请求后产生一个新的调度进程,这个调度进程选用一个TCP/IP端口来控制与用户进程的交互,然后将此信息回传给监听进程,监听进程再将此信息传给用户进程,用户进程使用这个端口给调度进程发送一个CONNECT包,调度进程再给用户进程发送一个ACCEPT包,至此,用户进程可以正式向调度进程发送信息了。可以通过设置MAX_DISPIATCHERS这个参数来确定调度进程的最大数目,如果调度进程的个数已经达到了最大,或者已有的调度进程不是满负荷,监听进程将不再创建新的调度进程,而是让其中一个调度进程选用一个TCP/IP端口来与此用户进程交互。调度进程每接收一个用户进程请求都会在监听进程处作一个登记,以便监听进程能够均衡每个调度进程的负荷,所有的用户进程请求将分别在有限的调度进程中排队,所有调度进程再顺序的把各自队列中的部分用户进程请求放入同一个请求队列,等候多个ORACLE的共享服务器进程进行处理(可以通过SHARED_SERVERS参数设置共享服务器进程的个数),也就是说所有的调度进程共享同一个请求队列,共享服务器模式下一个实例只有一个请求队列,共享服务器进程处理完用户进程的请求后将根据用户进程请求取自不同的调度进程将返回结果放入不同的响应队列,也就是说有多少调度进程就有多少响应队列,然后各个调度进程从各自的响应队列中将结果取出再返回给用户进程。
以上我们讲完了用户与ORACLE的连接方式,下面我们要讲ORACLE服务器进程如何处理用户进程的请求,当一个用户进程发出了一条SQL语句:UPDATE TABBLEA SET SALARY=SALARY*2;首先服务器进程将对该语句进行检查语句有效性的语法检查和确保语句能够正常运行的语义检查,首先检查该语句的语法的正确性(语法检查),接着对照数据字典对语句中涉及的表、索引、视图等对象及用户的权限进行检查(语义检查),如果以上任一检查没有通过,就返回一个错误,但不会明确的指出是语法检查出错还是语义检查出错,它只会返回一个ORA-*****的错误码。如果检查通过以后,服务器进程把这条语句的字符转换成ASCII等效数字码(注意SQL中使用*是个例外,如果表的字段改变了,同样是SELECT * FROM TABLEA转换成的ASCII是不同的,其实它在语义检查时就明确的变成了操作具体字段的SQL语句了),接着这个ASCII码被传递给一个HASH函数,并返回一个HASH值,服务器进程将到SHARED POOL的共享PL/SQL区去查找是否存在同样的HASH值,如果存在,服务器进程将使用这条语句已高速缓存在SHARED POOL中的已分析过的版本来执行(软解析),如果不存在,则必须进行以下两个步骤:语句的优化(生成执行计划)和生成执行编码:服务器进程根据ORACLE选用的优化模式以及数据字典中是否存在相应对象的统计数据和是否使用了存储大纲来生成一个执行计划或从存储大纲中选用一个执行计划,最后再生成一个编译代码(硬解析)。(这里要注意的是,语法语义分析在前,计算HASH_VALUE在后,算出HASH_VALUE后只要找到相同的HASH_VALUE就使用这条缓存执行计划,语义分析在前确保了用户的使用权限等问题,不存在算出HASH_VALUE,再找到相同HASH_VALUE缓存执行计划而不能使用的情况。也不是先算HASH_VALUE,然后找缓存执行计划,找到后再语义检查这个步骤也是错的)ORACLE将这条语句的本身实际文本、HASH值、编译代码、与此语句相关联的任何统计数据和该语句的执行计划缓存在SHARED POOL的共享PL/SQL区。V$librarycache中的几个参数解释Pins: (Execution)即SQL实际执行的次数,不包括用户提交的语法语义检查失败的SQL。Reloads: (Parse)未找到相同HASH_VALUE的次数,即必须进行硬解析的次数。Invalidations: (Parse)因对象更改,使得所有引用这个对象的缓存执行计划失效而必须再次硬解析的次数。只要DDL更改了一个对象,所有与此有关的缓存在共享池中执行计划都将立即失效,它的失效不是在下次执行SQL时才发现其失效,而是DDL更改对象后立即就失效。主要表现在DDL发生后v$sql的HASH_VALUE仍保持不变,但PLAN_HASH_VALUE立即变为0,再次运行SQL语句时则会向v$sql插入一条新的缓冲记录HASH_VALUE,PLAN_HASH_VALUE都重新计算。原来的缓冲记录仍然还存在。 服务器进程通过SHARED POOL锁存器来申请可以向哪些共享PL/SQL区中缓存这些内容,也就是说被SHARED POOL锁存器锁定的PL/SQL区中的块不可被覆盖,因为这些块可能正在被其它进程所使用。在SQL分析阶段将用到LIBRARY CACHE,从数据字典中核对表、索引、视图及用户的权限的时候,需要将数据字典从磁盘读入LIBRARY CACHE,因此,在读入之前也要使用LIBRARY CACHE锁存器来申请用于缓存数据字典。
ORACLE的工作机制-2
肖亚峰(xyf_tck) 生成编译代码之后,接着下一步服务器进程要准备开始更新数据,服务器进程将到DB BUFFER中查找是否有相关对象的缓存数据,下面分两个可能进行解释:
如果没有,服务器进程将在表头部请求一些行锁,如果成功加锁,服务器进程将从数据文件中读入这些行所在的第一个数据块(db block)(DB BLOCK是ORACLE的最小操作单元,即使你想要的数据只是DB BLOCK中很多行中的一行或几行,ORACLE也会把这个DB BLOCK中的所有行都读入DB BUFFER中)放入DB BUFFER中空闲的区域或者覆盖已被挤出LRU列表的非脏数据块缓冲区,并且排列在LRU列表的头部,如果这些非脏数据缓冲区写完也不能满足新数据的请求时,会立即触发DBWN进程将脏数据列表中指向的缓冲块写入数据文件,并且清洗掉这些缓冲区,来腾出空间缓冲新读入的数据,也就是在放入DB BUFFER之前也是要先申请DB BUFFER中的锁存器,成功锁定后,再写入DB BUFFER,然后把这个块的头部事务列表及SCN信息及被影响的行数据原值写入回滚段中,以便ORACLE在ROLLBACK时可以利用当前数据块和回滚段重构数据块的"前映像"或递归重构出"前…前映像"来实现读一致性。(回滚段可以存储在专门的回滚表空间中,这个表空间由一个或多个物理文件组成,并专用于回滚表空间,回滚段也可在其它表空间中的数据文件中开辟。)然后在LOG BUFFER中生成日志,服务器程将该语句影响的被读入DB BUFFER块中的这些行的ROWID及将要更新的原值和新值及SCN等信息,以及回滚段的修改信息(即对某某回滚段地址进行了什么修改)逐条的写入REDO LOG BUFFER,在写入REDO LOG BUFFER之前也是先请求REDO LOG BUFFER块的锁存器,成功锁定之后才开始把REDOLOG写入REDOLOG BUFFER。当写入达到REDO LOG BUFFER大小的三分之一或写入量达到1M或超过三秒后或发生检查点时或者COMMIT时或者DBWN之前触发LGWR进程,LGWR将把REDO LOG BUFFER中的数据写入磁盘上的重做日志文件,已被写入重做日志文件的REDO LOG BUFFER中的块上的锁存器被释放,并可被后来写入的信息所覆盖。回滚段其实也有BUFFER(在DB BUFFER中开辟),回滚段BUFFER中的内容是最早向磁盘上回滚段中写的,写完这些才会生成日志BUFFER中的内容,原因是日志中必须要记录回滚段的新旧变化以便在恢复时从日志中的记录的回滚段新旧变化对回滚段再次重写,记住,REDO不光是对数据文件依据日志文件重写,也要依据日志文件对回滚段重写,而且重写回滚段要先于重写数据文件,要理解REDO就是重来一遍,所谓重来一遍就要跟正常的的先后顺利一样重做一遍(正常的操作中的顺序就是先读入DB BUFFER,写回滚段buffer,后写回滚段,后写日志BUFFER,后改写DB BUFFER,后写日志最后写数据文件)区别是REDO时不用再记日志了,这样解释后相信大家应该理解为什么日志中也必须要记录回滚段的信息了,只有这样才可以对正常操作中的一个ROLLBACK动作进行恢复,即在REDO过程中利用即时重写的数据块和回滚段重构出一个当时适用的前镜像来rollback。当一个重做日志文件写满后,LGWR将切换到下一个重做日志文件,重做日志文件也是循环工作方式。如果是归档模式,归档进程还将前一个写满的重做日志进程写入归档日志文件。当DB BUFFER改写之后,服务器进程在脏数据列表中建立一条指向此DB BUFFER缓冲块的指针。接着服务器进程会从数据文件读入第二个数据块(db block)重复以上读入,建立回滚段,写LOG BUFFER,改写DB BUFFER,放入脏列表的动作,当脏数据列表达到一定长度时,DBWN进程将脏数据列表中指向的缓冲块全部写入数据文件,也就是释放加在这些DB BUFER块上的锁存器,并在修改相应块的头部的SCN号(一次UPDATE操作只对应一个SCN)。前面说过DBWN动作之前会先触发LGWR,这用以确保写入数据文件的改变首先会被记录在日志文件中。实际上ORACLE可以从数据文件中一次读入多个块放入DB BUFFER,然后再对这些块建回滚段、再记日志等等,也就是每次操作的对象是DB BLOCK的复数,而不仅限于一次操作一个DB BLOCK,可以通过参数DB_FILE_MULTIBLOCK_READ_COUNT来设置一次读入的块的个数。注意,不管是否提交,用户的所有更改都会被记录在日志文件中,用户级回滚的动作(rollback)没有对应的COMMIT SCN。在密集事务的情况下,LGWR可以把多个COMMIT产生的REDO条目批量写入REDO LOG FILE,但每个COMMIT之间有十分之一秒的间隔,且会产生不同的COMMIT SCN。LGWR正常情况下是一个休眠进程,会被一定的条件触发,唤醒,比如COMMIT就是一个唤醒条件,一旦LGWR被唤醒,LGWR将把唤醒时间点之前LOG BUFFER中产生的所有内容(从上次LGWR唤醒后到本次唤醒前之间写入LOG BUFFER的内容)写入LOG FILE,直到LGWR完成后,LGWR才可以被再次触发,在LGWR触发到完成期间所有对数据库的操作仍然可以不间断的加入LOG BUFFER。在这段时间内,LGWR不再接收其它条件的触发,比如紧跟前一个COMMIT之后的其它COMMIT(复数)都要等待LGWR完成后才可以再次触发LGWR,并在LGWR下次被触发时,将积累的REDO BUFFER条目一次性写入REDO LOG,后继的COMMIT不会单个单个的触发LGWR。
如果要查找的数据已缓存,则根据用户的SQL操作类型决定如何操作,如果是SELECT则查看DB BUFFER块的头部是否有事务,如果有,将利用回滚段进行重构出一致性块再读取,如果没有则比较SELECT的SCN与DB BUFFER块头部的SCN如果比自己大,仍然同上,如果比自己小则认这是一个非脏缓存,可以直接从这个DB BUFFER块中读取。如果是UPDATE则即使在DB BUFFER中找到一个没有事务,而且SCN比自己小的非脏缓存数据块,服务器进程仍然要到表的头部对这条记录申请加锁,加锁成功则进行后续动作,如果不成功,则要等待前面的进程解锁后才能进行动作。
ORACLE的工作机制-3
肖亚峰(xyf_tck) 只有当SQL语句影响的所有行所在的最后一个块被读入DB BUFFER并且重做信息被写入REDO LOG BUFFER(仅是指重做日志缓冲,而非重做日志文件)之后,用户才可以发出COMMIT,COMMIT触发LGRW,但并不强制立即DBWN来释放所有相应的DB BUFFER块上的锁,也就是说有可能出现已COMMIT,但在随后的一段时间内DBWN还在写这条语句涉及的数据块的情形,表头部的行锁,并不是在COMMIT一发出就马上释放,实际上要等到相应的DBWN进程结束才会释放。一个用户请求锁定另一个用户已COMMIT的资源不成功的机会是存在的。COMMIT发出后会将回滚段中的"前映像"标识为已提交.DML语句会产生一个SCN号,DBWN触发时写入到数据块的头部,COMMIT时也会产生一个SCN号,也会被写入数据块的头部。在数据块的头部只存储一个最新的SCN号,COMMIT之后这个事务插槽可以被另外一个事务使用。
如果用户ROOLBACK,则服务器进程会根据数据文件块和DB BUFFER中块的头部的事务列表和SCN以及回滚段地址重构出相应的修改前的副本,并且用这些原值来还原当前数据文件中已修改但未提交的改变。如果有多个"前映像",服务器进程会在一个"前映像"的头部找到"前前映像"的回滚段地址,一直重构出同一事务下的最早的一个"前映像"为止。一旦发出了COMMIT,用户就不能ROLLBACK,这使得COMMIT后DBWN进程还没有全部完成的后续动作得到了保障。
下面我们要提到检查点的作用,ckpt的触发,有以下几种情况:
1.当发生日志组切换的时候
2.当满足log_checkpoint_timeout、log_checkpoint_interval、fast_start_io_target、fast_start_mttr_target参数设置的时候
3.当运行alter system switchlogfile的时候
4.当运行alter systemckeckpoint的时候
5.当运行alter tablespacetbs_name begin backup[end backup]的时候
6.当运行alter tablespace[datafile] offline的时候
7.系统正常关闭时
只有在4.7两种情况下发生完全检查点。发生完全检查点时,首先系统记录检查点对应的Checkpoint SCN,并记录下该时刻修改的DB BUFFER对应的日志文件的最新的重做字节地址(Redo Byte Address (RBA)),然后DBWN进程将这个重做字节地址(RBA)之前已发生的DB BUFFER中的脏缓冲写入数据文件(之所以要以重做字节地址(RBA)为标志是因为在检查点发生到检查点完成之间的时间内,系统还在一直不断的产生修改,这些修改所产生的DB BUFFER脏缓冲,以及日志条目将不会影响这次检查点最后确认的一致性结果,也就是最后确认这个Checkpoint SCN之前的系统是一致的)。最后把Checkpoint SCN和RBA更新至控制文件,Checkpoint SCN更新至每个数据文件头部,表明当前数据库是一致的。日志切换并不导致一个完全检点的发生,比如有三个日志文件组,当发生日志切换时发生检查点,而发生日志切换一般是因为当前的LGWR正在写重做日志,也就是LGWR当刚写满2号日志就立即触发检查点,于是系统开始核对3号日志中记录的REDO项目所对应的数据是否已经从DB BUFFER中写入数据文件(不管事务是否已提交),如果没有写入,检查点就触发DBWN进程将这些缓冲块写入数据文件,显然LGWR因此而发生等待,除此以外,检查点还让DBWN进程将在2号日志中对应修改的DB BUFFER块写入数据文件,然后继续LGWR进程,直到LGWR进程将LGWR触发之前存在于REDO LOG BUFFER中的所有缓冲(包含未提交的重做信息)写入重做日志文件,检查点再更新数据文件,控制文件头部SCN。其实LGWR等待的并不是CKPT的完成,而是等待CKPT触发的DBWN进程的完成。可以想像断电时可能既有未COMMIT的事务,也可能同时存在已COMMIT但DBWN未完成的情况,如果断电时有一个已COMMIT但DBWN动作没有完成的情况存在,因为已经COMMIT,COMMIT会触发LGWR进程,所以不管DBWN动作是否已完成,该语句将要影响的行及其产生的结果一定已经记录在重做日志文件中了,则实例重启后,SMON进程从控制文件中记录的上一次重做字节地址(RBA)开始,按照重做日志文件中的条目对数据文件和回滚段重新做一遍即前滚,注意这些条目的操作在断电之前有的已经被DBWN写入了数据文件,有的还没有来得及写,不管有没有写进数据文件,前滚时都会再重新写一次(9I之前是这样的),9I之后,由于也在日志中记录了DBWN改写的块信息,系统会过滤掉已写入的条目而只重做那些未写入的条目。对于一个未提交事务,分几种情况来描述:1)LGWR与DBWN一致的情况即一个语句执行完成后很长时间也没有COMMIT,这种情况一般不存在DBWN来不及完成的情况。只是没有COMMIT而已。那么SMON将在前滚完成后,利用回滚段重构出具有最小SCN的前映像,并把它的值写回原位。2)事务执行中断电,即可能存在LGWR与DBWN不同步的情况(因为DBWN之前会触发LGWR,所以DBWN对数据文件的修改一定会被先记录在重做日志文件中。因此只可能存在已写入重做日志而未来得及写入数据文件的情况存在。而不可能存在已写入数据文件却没有写入日志文件的情况。),这种情况下SMON也会先前滚一点(即把数据文件与相应的日志文件先同步再回滚,之所以说前滚一点,是指仅LGWR与DBWN之间进度的差距,而不是把这条语句进行到底再回滚,因为日志文件中记录的是执行语句操作的一个个块的修改信息,而不只是记录一条执行语句的字面内容),然后利用回滚段重构出具有最小SCN的前映像,并把它的值写回原位。由此可见,实例失败后用于恢复的时间由两个检查点之间的间隔大小来决定,我们可以通个四个参数设置检查点执行的频率,LOG_CHECKPOINT_IMTERVAL决定了两个检查点之间写入重做日志文件的系统物理块的大小,LOG_CHECKPOINT_TIMEOUT决定了两个检查点之间的时间长度,FAST_START_IO_TARGET决定了用于恢复时需要处理的块的大小,FAST_START_MTTR_TARGET直接决定了用于恢复的时间的长短。检查点的作用就是不断的确认LGWR与DBWN之间的同步情况,以便实例失败后从上一个检查点开始恢复,问题是两个检查点之间LGWR与DBWN大部分的操作是同步的,只是一小部分没有同步,这种传统的检查点使实例恢复做了比较多的无用功,因此,ORACLE引入了增量检查点,增量检查点会在上一次传统检查点发生后到下一次传统检查点发生之前,不断的更新记录在控制文件中重做字节地址(RBA)(CKPT进程每三秒更新一次,见下面DBWN讲述),这样实例失败后将直接从控制文件中记录的最后更新的重做字节地址(RBA)开始进行前滚和回滚,这就省略掉了恢复时大部份的重做日志的重做(即使在9I以后的版本里也省略掉了大部分的过滤重做日志条目的时间)。(对以上描述做一个简单的比喻:比如一个贸易公司下设经营部、货运部、监督部,经营部负责贸易合同的签订与记录,货运部负责按合同号的顺序把货物送达,监督部负责定期检查确认经营部签订的合同与货运部货物送达情况之间的同步情况,监督部每月检查一次,每次检查时,先确认当时正在装车的货物的合同号,并要求货运部把在这个合同号之前的所有还存在临时仓库中的未送货物全部送达。等货运部完成监督部下达的任务后,监督部在检查本上记录下本次开始检查时那票正在装车的货物的合同号,本次检查完成。如果这个公司发生了一次人事大换血,公司重新开业后,监督部就会从检查本上记录的合同号开始,检查在这之后所有发生的合同及货物送达情况,要求货运部把所有客户确认的但还未送达的货物送达。把所有客户未确认的货物收回。监督部发现这次重新开业后的工作量实在是太大了,几乎核对了整整一个月的几万单合同(好在不是半年检查一次^_^),为了防止今后出现这种情况,监督部增加了一项工作内容,每三天派人去记录一下货运部正在装车的那票货的合同号,今后如果发生类似情况,监督部就从最后一次记录的合同号开始核对,这样工作量就小多了。)
ORACLE的工作机制-4
肖亚峰(xyf_tck) 在这里我们要说一下回滚段存储的数据,假如是delete操作,则回滚段将会记录整个行的数据,假如是update,则回滚段只记录行被修改了的字段的变化前的数据(前映像),也就是没有被修改的字段是不会被记录的,假如是insert,则回滚段只记录插入记录的rowid。这样假如事务提交,那回滚段中简单标记该事务已经提交;假如是回退,则如果操作是delete,回退的时候把回滚段中数据重新写回数据块,操作如果是update,则把变化前数据修改回去,操作如果是insert,则根据记录的rowid把该记录删除。注意,检查点除了触发LGWR和DBWN向数据块头部写SCN和COMMIT SCN,检查点还向控制文件和数据文件头部写SCN,而用户的DML和COMMIT仅是向数据块头部写SCN和COMMIT SCN而不更新控制文件和数据文件的SCN,SMON的前滚是以文件头部的SCN为起始点的也就是从前一个检查点开始,SMON的回滚是回滚所有回滚段中未标识为已提交的数据块,用户的回滚是回滚与此事务有关的回滚段中未标识为已提交的数据块。
下面我们要讲DBWN如何来写数据文件,在写数据文件前首先要找到可写的空闲数据块,
ORACLE中空闲数据块可以通过FREELIST或BITMAP来维护,它们位于一个段的头部用来标识当前段中哪些数据块可以进行INSERT。在本地管理表空间中ORACLE自动管理分配给段的区的大小,只在本地管理的表空间中才能选用段自动管理,采用自动段空间管理的本地管理表空间中的段中的空闲数据块的信息就存放在段中某些区的头部,使用位图来管理(最普通的情况是一个段的第一个区的第一个块为FIRST LEVEL BITMAP BLOCK,第二个块为SECOND LEVEL BITMAP BLOCK,第三个块为PAGETABLE SEGMENT HEADER,再下面的块为记录数据的数据块,FIRST LEVEL BITMAP BLOCK的父数据块地址指向SECOND LEVEL BITMAP BLOCK,SECOND LEVEL BITMAP BLOCK的父数据块地址指向PAGETABLE SEGMENT HEADER,FIRST LEVEL BITMAP BLOCK记录了它所管理的所有块(包括头部三个块,不仅仅指数据块)的状态,标识的状态有Metadata、75-100% free、50-75% free、25-50% free、0-25% free、full、unformatted,在SECOND LEVEL BITMAP BLOCK中有一个列表,记录了它管理的FIRST LEVEL BITMAP BLOCK,PAGETABLE SEGMENT HEADER中记录的内容比较多,除了记录了它管理的SECOND LEVEL BITMAP BLOCK,还记录了各个区的首块地址以及各个区的DB BLOCK的个数,段的各个区所对应的FIRST LEVEL BITMAP BLOCK的块地址以及区里面记录数据的数据块的起始地址。如果一个区拥有很多块,这时会在一个区里出现两个或多个FIRST LEVEL BITMAP BLOCK,这些FIRST LEVEL BITMAP BLOCK分别管理一个区中的一些块,当区的数据块比较少时,一个区的FIRST LEVEL BITMAP BLOCK可以跨区管理多个区的数据块,BITMAP BOLCK最多为三级)。采用手动管理的本地管理表空间中的段和数据字典管理的表空间中的段中的空闲数据块的管理都使用位于段头部的空闲列表来管理,例如SYSTEM表空间是本地管理表空间,但是它是采用了手动段空间管理,所以也是用FREELIST来管理段中的空闲数据块的。空闲列表是一个逻辑上的链表,在段的HEADER BLOCK中记录了一个指向第一个空闲块的BLOCK ADDRESS,第一个DB BLOCK中同时也记录了指向下一个空闲块的BLOCK ADDRESS。以此形成一个单向链表。如果段上有两个FREE LIST则会在段头部的HEADER BLOCK存有两个指针分别指向两个空闲块并建立独立的两个单向链表。空闲列表的工作方式:首先当建立一个段时,初始分配的第一个区的第一个块会成为段的头块,初始分配的第一个区的其它块将全部加入空闲列表,再次扩展一个区时,这个区中的块立即全部加入空闲列表,扩展一次加入一次。与位图管理不同的是用空闲列表时区的头部将不记录区里面空闲块的信息。当其中空闲空间小于PCTFREE设置的值之后,这个块从空闲列表删除,即上一个指向它的块中记录的下一个空闲块地址更改为其它空闲块的地址,使得这个块类似于被短路,当这个块中的内容降至PCTUSED设置的值之下后,这个数据块被再次加入空闲列表,而且是加入到空闲列表前端,即头块直接指向它,它再指向原头块指向的空闲块,位于空闲列表中的数据块都是可以向其中INSERT的块,但是INSERT都是从空闲列表指向的第一个块开始插入,当一个块移出了空闲列表,但只要其中还有保留空间就可以进行UPDATE,当对其中一行UPDATE一个大数据时,如果当前块不能完全放下整个行,只会把整个行迁移到一个新的数据块,并在原块位置留下一个指向新块的指针,这叫行迁移。如果一个数据块可以INSERT,当插入一个当前块装不下的行时,这个行会溢出到两个或两个几上的块中,这叫行链接。如果用户的动作是INSERT则服务器进程会先锁定FREELIST,然后找到第一个空闲块的地址,再释放FREELIST,当多个服务器进程同时想要锁定FREELIST时即发生FREELIST的争用,也就是说多个进程只在同时INSERT时才会发生FREELIST争用,可以在非采用自动段空间管理的表空间中创建表时指定FREELIST的个数,默认为1,如果是在采用自动段空间管理的表空间中创建表,即使指定了FREELIST也会被忽略,因为此时将使用BITMAP而不是FREELIST来管理段中的空闲空间。采用自动段空间管理还会忽略的参数有PCTUSED和FREELIST GROUPS。如果用户动作是UPDATE或DELETE等其它操作,服务器进程将不会使用到FREELIST和BITMAP,因为不要去寻找一个空闲块,而使用锁的队列。对数据块中数据操作必须使用transaction entries,即事务入口。在建立段时我们可以通过MINTRANS和MAXTRANS参数指定它的最大值和最小值,MAXTRANS规定了在段中每一个块上最大并发事务数量,可以输入1到255之间的值。我们可以把它比喻为是一些长在块头部的事务插座,每个插座后面是一个可以伸缩的操作手,当事务进程插到一个插座上时相当于找到一个可以操作数据块中数据行的操作手,通过这个操作手,事务进程可以对块中数据进行INSERT、UPDATE、DELETE等操作。在没有超过MAXTRANS设定的最大值时,如果transaction entries不够用,则会在块上自动分配一个,但不会影响其它块中的transaction entries数量。只不过INSERT操作必须要先找到空闲块然后才能INSERT。
那么DBWN是根据什么顺序来写DB BUFFER中的脏数据的呢?ORACLE从8I开始加入新的数据结构--检查点队列(Buffer Checkpoint Queue)。检查点队列是一个链接队列。这个队列的按照Buffer块第一次被修改的顺序排列,分别指向被修改的Buffer块。在DB_Buffer中的数据被第一次被修改时,会记录所生成的REDO LOG条目的位置RBA作为该Buffer的Low RBA,记录在该Buffer的头部(Buffer Header),如果该数据继续被修改,则把该块修改的最新的REDO LOG的RBA作为High RBA记录在该Buffer的头部。如果DB_Buffer中的块没有被修改的数据,则该块的头部不会有Low RBA和High RBA的信息。检查点队列按照被修改块的Low RBA的递增值链接修改块,没有被修改的块因为没有Low RBA,而不会加入到检查点队列中。在没有检查点发生时DBWR就按照检查点队列的Low RBA的升序,将被修改的块写入到数据文件中。当块被写入到数据文件后,该块会从检查点队列中断开。DBWR继续写下一个块。CKPT进程每三秒记录检查点队列中对应的最小Low RBA到控制文件中,也就是更新控制文件中的CheckPointRBA,当实例崩溃时,恢复将从CheckPointRBA所指向的日志位置开始。这就是"增量检查点"的行为和定义。CKPT进程也会记录检查点位置到数据文件的头部,但是只是日志切换时才写。而不是每三秒。当检查点发生时,DBWN不会一直不停的写DB BUFFER中脏数据,它将写到检查点队列的开始块的Low RBA的值大于该检查点的Checkpoint RBA的值时停止写入,然后完成这次检查点,CKPT进程将记录该检查点的有关信息到控制文件中去。
ORACLE的工作机制-5
肖亚峰(xyf_tck) 下面来讲一下ORACLE锁的机制,分锁存器和锁两种。锁存器是用来保护对内存结构的访问,比如对DB BUFFER中块的锁存器申请,只有在DBWN完成后,这些DB BUFFER块被解锁。然后用于其它的申请。锁存器不可以在进程间共享,锁存器的申请要么成功要么失败,没有锁存器申请队列。主要的锁存器有SHARED POOL锁存器,LIBRARY CACHE锁存器,CACHE BUFFERS LRU CHAIN锁存器,CACHE BUFFERS CHAINS锁存器,REDO ALLOCATION锁存器,REDO COPY锁存器。ORACLE的锁是用来保护数据访问的,锁的限制比锁存器要更宽松,比如,多个用户在修改同一表的不同行时,可以共享一个表上的一个锁,锁的申请可以按照被申请的顺序来排队等候,然后依次应用,这种排队机制叫做队列(ENPUEUE),如果两个服务器进程试图对同一表的同一行进行加锁,则都进入锁的申请队列,先进的加锁成功,后面的进程要等待,直到前一个进程解锁才可以加锁,这叫做锁的争用,而且一旦加锁成功,这个锁将一直保持到用户发出COMMIT或ROOLBACK命令为止。如果两个用户锁定各自的一行并请求对方锁定的行的时候将发生无限期等待即死锁,死锁的发生都是由于锁的争用而不是锁存器的争用引起的,ORACLE在遇到死锁时,自动释放其中一个用户的锁并回滚此用户的改变。正常情况下发生锁的争用时,数据的最终保存结果由SCN来决定哪个进程的更改被最终保存。两个用户的服务器进程在申请同一表的多个行的锁的时候是可以交错进入锁的申请队列的。只有其中发生争用才会进行等待。创建表时指定的MAXTRANS参数决定了表中的一个数据块同时最多可以被几个事务锁定。
下面来讲一下ORACLE的读一致性机制,ORACLE的读一致性保证了事务之间的高度隔离性。
下面是几个关于回滚段读一致性和死锁的事例:
有表:Test (id number(10)) 有记录1000000条
SQL> create table test (idnumber(10))tablespaceusers;
表已创建。
SQL> begin
2 foriin 1..1000000 loop
3 insertinto test values(i);
4 endloop;
5 commit;
6 end;
7 /
PL/SQL过程已成功完成
一,大SELECT,小UPDATE
A会话----Select * from test;----设scn=101----执行时间09:10:11
B会话-----Update test set id=9999999 where id=1000000----设scn=102-----执行时间09:10:12
我们会发现B会话会在A会话前完成,A会话中显示的ID=100000是从回滚段中读取的,因为A会话在读到ID=1000000所在的BLOCK时发现BLOCK上有事务信息,因此要从回滚段中读,如果UPDATE在SELECT读到此BLOCK之前已经COMMIT,则SELECT读到此BLOCK时发现其BLOCK上没有事务信息,但是会发现其BLICK的SCN比SELECT自己的SCN大,因此也会利用回滚段进行重构。根据当前块上所有的itl找到相应的undo地址,重构出之前的block image,之前的那个block又含有自己的itl信息.如果这个before image中对应的scn不满足查询,又会根据undo生成beforebeforeimage,这样不断往复,直到构造出符合查询的scn的block返回结果,或者系统实在无法根据undo构造出符合查询的block,,报ora-01555的错误为止....。需要强调的是读一致性是通过对当前整个块利用回滚段(当前块上的所有ITL记录的所有回滚段地址)进行递归重构到过去某一时间点或某一SCN的块的一致性快照。而不是只针对块中一部分ITL记录在回滚段中递归查找来完成的,一定要理解递归重构与递归查找是完全不同的两个概念。Oracle回滚段确保了事务的高度的隔离性。即只要回滚段足够大,那么一个SELECT不管执行多长,它读取的所有数据都将是在这条SELECT语句开始执行瞬间这个时间点的值,而不会被其它用户在SELECT读取期间对数据是否做过修改而影响。
二、大UPDATE,小SELECT
A会话----Update test set id=1;----设scn=101----执行时间09:10:11
B会话-----select * from test where id=1000000----设scn=102-----执行时间09:10:12
我们会发现B会话会在A会话前完成,B会话中显示的ID=1000000是从BLOCK中直接读取的,因为B会话在读到ID=1000000所在的BLOCK时,A会话还没有来得及对其锁定,因此B会话既不会发现BLOCK上有事务信息,也不会发现BLOCK上的SCN比SELECT的大,因此会从BLOCK中直接读取,如果SELECT在UPDATE锁定此BLOCK后才发出,B会话读到此BLOCK时发现其BLOCK上有事务信息,因此会从回滚段中读取。
三、大UPDATE,小UPDATE
A会话----Update test set id=1;----设scn=101----执行时间09:10:11
B会话1-----Update test set id=999999 where id=1000000----设scn=102-----执行时间09:10:12
B会话2----- select * from test where id=2----设scn=103-----执行时间09:10:14
B会话3----- update test set id=3 where id=2----设scn=104-----执行时间09:10:15
我们会发现B会话1会完成,A会话将一直等待,因为B会话1会先于A会话锁定ID=1000000所在的BLOCK,并改写头部的事务信息,A会话在试图锁定此BLOCK时,发现其上有事务信息,将会一直等待B会话1事务结束后再行锁定,B会话2查询到的ID=2是从回滚段中读取的而不是从BLOCK中直接读出来的。因为A会话已将ID=2的BLOCK锁定,并写入了回滚段,从B会话3可以证明这一点,B会话3发出后,B会话3会收到死锁的信息,死锁的原因是A会话在等待B会话对ID=1000000所在的BLOCK解锁,现在B会话又在等待A会话对ID=2所在的BLOCK解锁,因此形成死锁,因此证明ID=2所在的BLOCK已被A会话锁定,然后A会话也会收到死锁的信息
相关阅读 更多 +