检查点(CHECKPOINT)
原文标题:How do checkpoints work and what gets logged
原文地址:http://www.sqlskills.com/BLOGS/PAUL/post/How-do-checkpoints-work.aspx
我一直想写本文,因为我注意到最近网上有些关于检查点的误导性言论。我想在这里快速解释一下在涉及到日志记录时检查点时如何工作的。
不管一个检查点操作是如何被触发的(比如是通过手工命令CHECKPOINT、数据库的完全或差异性备份、自动触发),它都会引起下面的一组操作:
1)数据库中所有的脏的数据页(自从磁盘读出后或者自从上次检查点后)不管改变他们的事务是什么状态,都被写到磁盘中。
2)在将数据页写到磁盘之前,所有的包括当前的用来描述页改变的日志记录都要写到磁盘中(日志记录也可能被缓存的)。这样才能保证恢复(recovery)工作能进行,并且这叫做预写式日志(WAL)。日志记录被按顺序地写入到日志文件中的,不同事务的日志记录是散布在日志文件中的。日志记录是不能选择性的写到磁盘中的,所以在将一条日志记录写入到磁盘前,需要将该记录前的所有记录都写入磁盘。
3)也会有专门描述检查点的日志记录产生的。
4)上面产生的检查点的日志记录的LSN被写入到数据文件的BOOT页的dbi_checkptLSN字段中。
5)如果是简单恢复模式,日志文件中的VLF会被检查是否能被标为“不活动”(这被称为“清理”或者“截断”。当然这两个词都是有点用词不当的,因为这种操作物理上对日志文件既不清理也不截断)。
上面我用的一些术语如果你还不理解(比如像日志、恢复、事务日志架构),可以阅读我在2009年二月份的TechNet杂志上的文章:Understanding Logging and Recovery In SQL Server .
事务日志其实是不需要跟踪检查点的,它只是想记录在检查点发生时有哪些事务时还是活动的。最后一个检查点记录的LSN被写在数据库的BOOT页中。这也是恢复操作开始的地方,所以如果不能读取BOOT页,那么数据库就不能加载、打开或者处理,这部分是因为BOOT页知道数据库是不是“干净”地被关闭的;部分是因为只有该页记录了最后一个检查点的LSN。你可能会说,它也记录在日志文件中啊,但是假如日志文件坏了呢?
我见到的一个令人混淆的说法是检查点记录会被后面的检查点覆盖。这绝不可能,一旦记录,任何一个日志记录永远不可能被更新或者覆盖。只有VLF被重用了,当日志回卷的时候才可能覆盖原先的记录。在使用fn_dblog命令时,这将导致进一步的混淆:日志中的检查点到底何时是可取的?(译注:既然日志没有被覆盖,为什么使用fn_dblog时却看不到以前的检查点记录呢?)
下面我将举例说明检查点在不同情况下,事务日志中到底是如何记录的。
考虑下面的例子:
CREATE DATABASE CheckpointTest;
GO
USE CheckpointTest;
GO
CREATE TABLE t1 (c1 INT);
GO
INSERT INTO t1 VALUES (1);
GO
CHECKPOINT;
GO
SELECT [Current LSN], [Operation] FROM fn_dblog (NULL, NULL);
GO
Current LSN Operation
----------------------- -------------------------------
00000047:00000051:009b LOP_BEGIN_CKPT
00000047:00000091:0001 LOP_END_CKPT
我们看到检查点的日志记录。这种情况下,检查点是非常简单的,只有两条:检查点开始和结束。
如果再运行一个CHECKPOINT命令,我们会看到什么呢?
CHECKPOINT;
GO
SELECT [Current LSN], [Operation] FROM fn_dblog (NULL, NULL);
GO
Current LSN Operation
----------------------- -------------------------------
00000047:00000092:0001 LOP_BEGIN_CKPT
00000047:00000093:0001 LOP_END_CKPT
还是一个检查点的两条日志记录,但是LSN却不同了,所以并不是覆盖了以前的检查点。
这是由于我们执行了一个新检查点后日志向前移动,以前的检查点不再需要了(比如,对数据库镜像、活动事务、日志备份、事务复制都不需要了)而被认为是不活动的了。其实他们还是在日志文件中的,但已不在需要的部分中,所以fn_dblog也不显示了。
如果我们现在在另外一个连接中创建一个事务:
USE CheckpointTest;
GO
BEGIN TRAN;
GO
INSERT INTO t1 VALUES (1);
GO
现在再做一个检查点,看看日志什么样?
CHECKPOINT;
GO
SELECT [Current LSN], [Operation] FROM fn_dblog (NULL, NULL);
GO
Current LSN Operation
----------------------- -------------------------------
00000047:00000094:0001 LOP_BEGIN_XACT
00000047:00000094:0002 LOP_INSERT_ROWS
00000047:00000094:0003 LOP_COUNT_DELTA
00000047:00000094:0004 LOP_COUNT_DELTA
00000047:00000094:0005 LOP_COUNT_DELTA
00000047:00000094:0006 LOP_BEGIN_CKPT
00000047:00000096:0001 LOP_XACT_CKPT
00000047:00000096:0002 LOP_END_CKPT
我们看到日志记录有:事务的开始、插入数据、更新行数以及检查点。你可能也注意到了为检查点新产生了一条日志记录——LOP_XACT_CKPT。该记录列出了检查点开始时系统中所有活动的(未提交的)事务。在灾难恢复时,利用这条记录可以知道(从最后一个检查点)往回多远才开始REDO和UNDO操作(当然技术上只有UNDO需要往回)。再看看这条日志记录:
SELECT [Current LSN], [Operation], [Num Transactions], [Log Record]
FROM fn_dblog (NULL, NULL) WHERE [Operation] = 'LOP_XACT_CKPT';
GO
Current LSN Operation Num Transactions
----------------------- ------------------------------- ----------------
00000047:00000096:0001 LOP_XACT_CKPT 1
Log Record
-----------------------------------------------------------------------------------------------------------
0x000018 <snip> 7805000000000000470000009400000001000 401470000009400000002000 00001 <snip> 6621000000000000
日志记录中包含检查点时每个活动的(未提交的)事务。不需要详细知道日志记录里的数据,你便可以看出两件事:
1)最老的活动事务的LOP_BEGIN_XACT的LSN.(上面第一个黑体数字,它和上文日志中的LOP_BEGIN_XACT记录一致)。
2)事务中第一条修改数据库的日志记录的LSN.(上面第二个黑体数字,它和上文日志中的LOP_INSERT_ROWS记录一致).
注意:这些LSN的字节顺序是反的。
如果我们再来执行另一个CHECKPOINT,会有什么情况呢?
CHECKPOINT;
GO
SELECT [Current LSN], [Operation] FROM fn_dblog (NULL, NULL);
GO
Current LSN Operation
----------------------- -------------------------------
00000047:00000094:0001 LOP_BEGIN_XACT
00000047:00000094:0002 LOP_INSERT_ROWS
00000047:00000094:0003 LOP_COUNT_DELTA
00000047:00000094:0004 LOP_COUNT_DELTA
00000047:00000094:0005 LOP_COUNT_DELTA
00000047:00000094:0006 LOP_BEGIN_CKPT
00000047:00000096:0001 LOP_XACT_CKPT
00000047:00000096:0002 LOP_END_CKPT
00000047:00000097:0001 LOP_BEGIN_CKPT
00000047:00000098:0001 LOP_XACT_CKPT
00000047:00000098:0002 LOP_END_CKPT
这时我们看到当前和以前的检查点了。这是因为当有一个事务是活动时,不管有多少的检查点,所有的检查点日志都是记录到最老的活动事务。
我们如果在第三个连接上开始一个新的事务会怎么呢?
USE CheckpointTest;
GO
BEGIN TRAN;
GO
INSERT INTO t1 VALUES (2);
GO
然后回到原来的连接,再做一个CHECKPOINT,再次列出日志:
CHECKPOINT;
GO
SELECT [Current LSN], [Operation] FROM fn_dblog (NULL, NULL);
GO
Current LSN Operation
----------------------- -------------------------------
00000047:00000094:0001 LOP_BEGIN_XACT
00000047:00000094:0002 LOP_INSERT_ROWS
00000047:00000094:0003 LOP_COUNT_DELTA
00000047:00000094:0004 LOP_COUNT_DELTA
00000047:00000094:0005 LOP_COUNT_DELTA
00000047:00000094:0006 LOP_BEGIN_CKPT
00000047:00000096:0001 LOP_XACT_CKPT
00000047:00000096:0002 LOP_END_CKPT
00000047:00000097:0001 LOP_BEGIN_CKPT
00000047:00000098:0001 LOP_XACT_CKPT
00000047:00000098:0002 LOP_END_CKPT
00000047:00000099:0001 LOP_BEGIN_XACT
00000047:00000099:0002 LOP_INSERT_ROWS
00000047:00000099:0003 LOP_COUNT_DELTA
00000047:00000099:0004 LOP_COUNT_DELTA
00000047:00000099:0005 LOP_COUNT_DELTA
00000047:00000099:0006 LOP_BEGIN_CKPT
00000047:0000009b:0001 LOP_XACT_CKPT
00000047:0000009b:0002 LOP_END_CKPT
你现在看到三组检查点和两个活动事务的日志。只有一组检查点日志记录是恰当的,前面两个已过时了。但是不管怎样,你看的很清楚:日志记录并没有被覆盖或删除。
下面来看看所有的LOP_XACT_CKPT记录,我们会发现(输出格式有点变化):
SELECT [Current LSN], [Operation], [Num Transactions], [Log Record]
FROM fn_dblog (NULL, NULL) WHERE [Operation] = 'LOP_XACT_CKPT';
GO
Current LSN Operation Num Transactions
----------------------- ------------------------------- ----------------
00000047:00000096:0001 LOP_XACT_CKPT 1
00000047:00000098:0001 LOP_XACT_CKPT 1
00000047:0000009b:0001 LOP_XACT_CKPT 2
Log Record
-------------------------------------------------------------------------------------------------------------
0x000018 <snip> 7805000000000000470000009400000001000 40147000000940000000200000001 <snip> 21000000000000
0x000018 <snip> 7805000000000000470000009400000001000 40147000000940000000200000001 <snip> 21000000000000
0x000018 <snip> 7805000000000000470000009400000001000 40147000000940000000200000001 <snip> 21000000000000 ...
... 7905000000000000470000009900000001000 4014700000099000000020000000100000002000000DC000000
前面两个检查点只是列出了一个活动事务,而后面的一个列出了两个事务——这正是我们希望看到的。前面的两个列出相同的最老活动事务(见黑体数字)。后面的检查点列出相同的最老活动事务(这个没有变化),但是比前两个多列出了一个事务(47000000990000000200000001000,就是上文列出的第二个LOP_BEGIN_XACT日志记录的LSN),所以它的事务数目是2。
作为结尾,我们用DBCC PAGE或DBCC DBINFO来看看数据库的起始页。
DBCC TRACEON (3604);
GO
DBCC DBINFO ('CheckpointTest');
GO
DBINFO STRUCTURE:
DBINFO @0x6711EF64
dbi_dbid = 18 dbi_status = 65536 dbi_nextid = 2089058478
dbi_dbname = CheckpointTest dbi_maxDbTimestamp = 2000 dbi_version = 611
dbi_createVersion = 611 dbi_ESVersion = 0
dbi_nextseqnum = 1900-01-01 00:00:00.000 dbi_crdate = 2009-09-28 07:06:35.873
dbi_filegeneration = 0
dbi_checkptLSN
m_fSeqNo = 71 m_blockOffset = 153 m_slotId = 6
dbi_RebuildLogs = 0 dbi_dbccFlags = 2
dbi_dbccLastKnownGood = 1900-01-01 00:00:00.000
<snip>
dbi_checkptLSN用十进制列出,转换成十六进制为(47:99:6),这正好和最后的LOP_BEGIN_CKPT记录的LSN一样。
希望上面已经解释清楚了。