以前我已经写了一个关于SQL Server日志的简单系列文章。本篇文章会进一步挖掘日志背后的一些概念,原理以及做用。若是您没有看过我以前的文章,请参阅:html
浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架数据库
浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色安全
浅谈SQL Server中的事务日志(三)----在简单恢复模式下日志的角色架构
浅谈SQL Server中的事务日志(四)----在完整恢复模式下日志的角色并发
浅谈SQL Server中的事务日志(五)----日志在高可用和灾难恢复中的做用数据库设计
在关系数据库系统中,咱们须要数据库可靠,所谓的可靠就是当碰见以下两种状况之一时保证数据库的一致性:性能
实际上,上述第二种状况就是并发性所须要解决的问题,传统关系数据库中,咱们用锁来解决这个问题,而对于内存数据库或带有乐观并发控制的数据库系统,经过多版本并发控制(MVCC)来解决这个问题。由于本篇文章的主旨是讨论日志而不是并发,所以对于上述第二种状况不会详细解释。测试
咱们上面还屡次提到了一致性(Consistence),在开始了解日志如何维持一致性以前,咱们首先要明白什么是一致性。一致性在数据库系统中所指的内容比较广,一致性不只仅须要数据库中的数据知足各类约束,好比说惟一约束,主键约束等,还须要知足数据库设计者心中的隐式约束,简单的业务约束好比说性别这列只容许男或女,这类隐式约束一般使用触发器或约束来实现,或是在数据库所服务的应用程序中进行约束。优化
下面咱们把一致性的范围缩减到事务一致性,事务一致性的概念学术上的解释为:线程
若是事务执行期间没有出现系统错误或其余事务错误,而且数据库在事务开始期间是数据一致的,那么在该事务结束时,咱们认为数据库仍然保证了一致性。
所以,引伸出来事务必须知足原子性,也就是事务不容许部分执行。事务的部分执行等同于将数据库置于不一致的境地之下。此外多事务并发执行也可能致使数据库不一致,除非数据库系统对并发进行控制。
关于上面的显式约束,由数据库系统来实现,好比说违反了一致性约束的语句会致使数据库系统报错并拒绝执行。但一些隐式的事务约束,好比说写语句的开发人员对系统设计者所设计的规则并不了解,致使了违反业务规则的数据修改,这种状况在数据库端很难探查。可是这种问题一般能够规则到权限控制的领域,咱们认为授予某个用户修改特定数据的权限,就认为这个用户应该了解数据库中隐式和显式的规则。
除去这些业务上的数据不一致以外,咱们须要在系统崩溃等状况下保证数据的一致性,而可能致使这类数据不一致的状况包括但不限于下面这些状况:
SQLServer中的日志
SQL Server中靠日志来维护一致性(固然,日志的做用很是多,但一致性是日志的基本功能,其余功能能够看做是额外的功能)。一般咱们建立数据库的时候,会附带一个扩展名为ldf的日志文件。日志文件其实本质上就是日志记录的集合。在SQL Server中,咱们能够经过DBCC LOGINFO来看这个日志的信息,如图1所示。
图1.DBCC LOGINFO
该命令能够从VLF的角度从一个比较高的层级看日志。其中值得注意的列是VLF大小,状态(2表示使用,0表示从未使用过),偏移量。对于这些信息对咱们规划VLF数量的时候颇有帮助,由于VLF过多可能引发严重的性能问题,尤为是在复制等Scale-Out或HA环境下。
而后,事务对数据库中每次修改都会分解成多个多个原子层级的条目被记录到持久存储中,这些条目就是所谓的日志记录(Log Record),咱们能够经过fn_dblog来查看这些条目。如图2所示。
图2.Fn_dblog
每一个日志记录都会被背赋予一个惟一的顺序编号,这个编号大小为10字节,由三部分组成,分别为:
所以,因为VLF是不断递增的(同一个VLF被复用会致使编号改变),所以LSN序号也是不断递增的。所以,经过上面的LSN结构不难发现,若是比VLF更小的粒度并非直接对应LOG RECORD,而是LOG Block。Log Block是日志写入持久化存储的最小单位,Log Block的大小从512字节到60K不等,这取决于事务的大小,那些在内存还未被写入持久化存储的Log Block也就是所谓的In-Flight日志。如下两个因素决定Log Block的大小:
所以当一个事务很大时(好比说大面积update),每60K就会成为一个Log Block写入持久化存储。而对于不少小事务,提交或回滚就会称为一个Block写入持久化存储,所以根据事务的大小,LOG Block的大小也会不一样。值得疑惑的是,由于磁盘上分配单元的大小是2的N次方,所以最接近LOG BLOCK的大小应该是64K,而SQL Server为何不把Log Block设定为64K呢。这样能够更优化IO。
VLF和Log Block和Log Record的关系如图3所示。
图3.三者之间的关系
从比较高的层级了解了日志以后,咱们再仔细了解日志中应该存储的关键信息,每条Log Record中都包含下面一部分关键信息:
固然,这些仅仅是日志的一小部份内容。经过Log Record所记录的内容,就可以精确的记录对数据库所作的修改。
在了解为了Undo,日志所起的做用以前,咱们首先能够了解一下为何须要事务存在回滚:
所以,Log Record会为这些列保存一些字节来执行数据库回滚,最简单的例子莫过于执行插入后Rollback事务,则日志会产生一条所谓的Compensation Log Record来反操做前面已经插入的事务,如图4所示。
图4.Compensation Log示例
图4执行的是一个简单的Insert语句,而后回滚。咱们看到,SQL Server生成了一个Compensation Log Record来执行反向操做,也就是Delete操做。值得注意的是,为了防止这些回滚操做,SQL Server会保留一些空间用于执行回滚,咱们看到LOP_INSERT_ROWS保留的74字节空间被下面的Compensation Log Record所消耗。Compensation Log record还有一个指向以前LSN的列,用于回滚,直至找到LOP_BEGIN_XACT的事务开始标记。另外,Compenstion Log Record只可以用于Redo,而不能用于Undo。
那假设咱们某一个事务中删除了多条数据怎么办?好比说,某一个事务中一个Delete语句删除了10行,则须要在Log Record对应10个LOP_DELETE_ROWS(引伸一下,由此咱们能够看出某一个语句可能致使N个Log Record,这么多Log Record在复制,镜像时都须要在另外一端Redo,所以须要额外的开销),若是咱们此时RollBack了该事务,则Redo的顺序是什么呢,如图5所示。
图5.回滚事务
图5中,删除3条数据后,进行回滚,首先从删除3开始,生成对应的反向Compensation Log Record,并指向删除2,再对应删除2生成反向Compensation Log Record并指向删除1,以此类推,最终回滚事务指回开始事务。
与Undo不一样,在计算机存储体系中,辅助存储一般是带有磁头的磁盘。这类存储系统的IOPS很是低,所以若是对于事务对数据库执行的修改操做,咱们积累到必定量再写入磁盘,无疑会提升IO的利用率。可是在数据在主存尚未持久化的辅助存储的期间,若是遭遇系统故障,则这部分数据的丢失则可能致使数据库的不一致状态。
所以,使用日志使得该问题获得解决。与日志Undo方面的不一样之处在于:Undo用于解决事务未完成和事务回滚的状况,而Redo则是为了保证已经提交的事务所作的修改持久化到辅助存储。
Redo则引伸出了WAL,即事务日志会在COMMIT或COMMIT以前写入持久化存储中,而后事务对数据自己的修改才能生效。所以就可以保证在系统故障时能够经过读取日志来Redo日志的持久化操做。所以对于最终用户能够显示事务已经提交而暂时不用将所修改的数据写入持久化存储。因为数据在日志未写入持久化存储以前没法持久化,则须要更大的主存做为BUFFER空间。
由于日志既要用于Undo,又要用于Redo,所以为了可以成功生成Compensation Log Record,须要日志既记录被修改前的数据,又记录被修改后的数据,好比咱们在图6中作一个简单的更新。
图6.记录更新以前和以后的数据
值得注意的是,若是修改的值是汇集索引键,则因为修改该数据会致使存储的物理位置改变,因此SQL Server并不会像这样作即席更新,而是删除数据再插入数据,从而致使成本的增长,所以尽可能不要修改汇集索引键。
当SQL Server非正常缘由关闭时,也就是在没有走CheckPoint(会在下面提到)时关闭了数据库,此时数据库中数据自己可能存在不一致的问题。所以在数据库再次启动的时候,会去扫描日志,找出那些未提交却写入持久化存储的数据,或已提交却未写入持久化存储的数据,来进行Undo和Redo来保证事务的一致性。Undo/Redo Recovery遵循如下规则:
图7中,咱们进行一个简单测试,在启动过程当中,首先禁用了CheckPoint以防止自动CheckPoint,而后咱们修改数据,不提交,并持久化到磁盘。另外一个线程修改数据并提交,但未持久化到磁盘。为了简单起见,我把两个线程写到一个窗口中。
图7.须要Undo和Redo的两个事务
此时咱们强制杀死SQL Server进程,致使数据自己不一致,此时在SQL Server的重启过程当中,会自动的Redo和Undo上面的日志,如图8所示。
图8.实现Redo和Undo
那么,什么是CheckPoint?
图8给出的简单例子足以说明Recovery机制。但例子过于简单,假如一个很是繁忙的数据库可能存在大量日志,一个日志若是所有须要在Recovery过程当中被扫描的话,那么Recovery过程所致使的宕机时间将会成为噩梦。所以,咱们引入一个叫CheckPoint的机制,就像其名称那样,CheckPoint就是一个存档点,意味着咱们能够从该点继续开始。
在Undo/Redo机制的数据库系统中,CheckPoint的机制以下:
1.将CheckPoint标记写入日志(标记中包含当前数据库中活动的事务信息),并将Log Block写入持久化存储
2.将Buffer Pool中全部的脏页写入磁盘,全部的脏页包含了未提交事务所修改的数据
3.将结束CKPT标记写入日志,并将Log Block写入持久化存储
咱们在日志中能够看到的CheckPoint标记如图9所示。
图9.CheckPoint标记
其中,这些Log Record会包含CheckPoint的开始时间,结束时间以及MinLSN,用于复制的LSN等。由图9中咱们还能够看到一个LOP_XACT_CKPT操做的Log Record,该操做符的上下文若是为NULL的话,则意味着当前:
由CheckPoint的机制能够看出,因为内存中的数据每每比持久化存储中的数据更新,而CheckPoint保证了这部分数据可以被持久化到磁盘,所以CheckPoint以前的数据必定不会再须要被Redo。而对于未提交的事物所修改的数据写入持久化存储,则能够经过Undo来回滚事务(未提交的事物会致使CheckPoint没法截断日志,所以这部分日志能够在Recovery的时候被读取到,即便这部分日志在CheckPoint以前)。
此时,咱们就能够100%的保证,CheckPoint以前的日志就能够被安全删除(简单恢复模式)或归档了(完整恢复模式),在Recovery时,仅仅须要从CheckPoint开始扫描日志,从而减小宕机时间。
本篇文章深刻挖掘了数据库中日志为保护数据一致性的的做用、实现原理。日志在这些功能以外,也是为了用于实现高可用性,所以了解这些原理,能够更好的帮助咱们在搭建高可用性拓扑以及设计备份计划时避免一些误区。