SQL Server 中的数据版本控制:使用快照(也称为行版本)
启用 SQL Server 内部行版本控制,
引言
本文简要介绍了如何通过读取正在修改的数据来更新操作块。此行为可以使用行版本控制进行更改。使用行版本控制的优点是并发性更高,但也有一些副作用,本文对此进行了描述。
在许多情况下,我注意到应用程序可能会使用 **Read uncommitted**(读取未提交)隔离级别来克服与锁定相关的问题(通常是操作所花费的时间)。行版本控制大大减少了使用 Read uncommitted 隔离级别的需求。当可以使用 **Read committed**(读取已提交)隔离级别而不会花费过多时间在操作上时,结果的正确性会显著提高。
这种行为类似于 Oracle 数据库中的情况,尽管实现方式略有不同。
如果您想了解更多关于 **Read uncommitted** 及其副作用的信息,请参阅 What should be considered when NOLOCK hint is used(使用 NOLOCK 提示时应考虑什么)。
正常行为
首先,我们需要一个用于测试的简单数据库,以及一个包含一行数据的表。最简单的形式,可以使用以下命令在现有的 SQL Server 实例中创建:
-- Create the database
CREATE DATABASE RVTest;
-- Change the context for the connection
USE RVTest;
-- Create a table for testing
CREATE TABLE Test (
Column1 varchar(50)
);
-- Insert a row to the table
INSERT INTO Test (Column1) VALUES ('For testing');
现在,当应用程序开始修改数据时,如果操作正确,修改将被包含在一个事务中。为了模拟这种情况,我们修改测试行并保持事务挂起。
BEGIN TRANSACTION;
UPDATE Test
SET Column1 = 'Modified value';
如果从数据库端通过查询 sys.dm_tran_locks
来观察这种情况,我们可以看到正在修改的行上获取了一个 **排他锁**。此外,在更高层级还需要一些意向锁,但主要关注的是行锁。输出将如下所示:
type description resource_associated_entity_id request_mode request_status request_session_id ---- ----------- ----------------------------- ------------ -------------- ------------------ PAGE 1:176 72057594043105200 IX GRANT 53 RID 1:176:0 72057594043105200 X GRANT 53
到目前为止,没有问题。现在,当另一个连接尝试读取相同数据时,它实际上会尝试获取该行的 **共享锁**。由于该行已授予排他锁,因此无法授予共享锁。在这种情况下,请求共享锁的连接将被放入队列中等待锁。
SELECT * FROM Test;
当我们再次查询 sys.dm_tran_locks
来查看情况时,等待状态如下所示:
type description resource_associated_entity_id request_mode request_status request_session_id
---- ----------- ----------------------------- ------------ -------------- ------------------
PAGE 1:176 72057594043105200 IS GRANT 54
PAGE 1:176 72057594043105200 IX GRANT 53
RID 1:176:0 72057594043105200 X GRANT 53
RID 1:176:0 72057594043105200 S WAIT 54
这种情况将一直持续到第一个会话的事务提交或回滚。当事务结束时,排他锁将被移除,之后就可以再次授予共享锁,第二个会话的操作就可以继续进行。在一个繁忙的系统中,队列中的情况可能就像圣诞节前一天礼品店一样拥挤。
使用行版本控制
行版本控制是一个数据库设置,可以使用 ALTER DATABASE
命令进行修改。要启用行版本控制,请将 READ_COMMITTED_SNAPSHOT
设置为 on(开启)。
ALTER DATABASE RVTest SET READ_COMMITTED_SNAPSHOT ON;
执行命令时,数据库可以处于多用户模式,但数据库中不能有其他并发连接。如果有,命令将不会返回,直到所有其他连接都关闭。
现在,当行为改变后,让我们看看之前的情况。首先,一个会话再次修改测试表:
BEGIN TRANSACTION;
UPDATE Test
SET Column1 = 'Another modified value';
此时,没有任何改变。该会话拥有与之前相同的锁,但当另一个会话在相同数据上执行 SELECT
时,我们可以看到区别:
SELECT * FROM Test;
现在,查询会立即返回,并产生以下结果:
Column1
-------
For testing
最重要的是,我们看到 **锁得到遵守**。我们不会像在 **Read uncommitted** 隔离级别下那样看到未提交的数据。相反,我们看到的结果是该行的最后一次提交状态。
那么,实际发生了什么?当修改发生时(UPDATE
语句),SQL Server 在数据被更改之前获取了数据的副本。这个副本被放在 tempdb
中。当查询该行时,数据库引擎会注意到该行存在未提交的修改,并根据事务序列号,从 tempdb
中读取原始数据。
权衡
因为没有免费的午餐,这种行为也存在一些权衡。其中最显著的是:
Tempdb
空间使用。由于版本化数据存储在tempdb
中,因此该数据库必须有足够的空间来存储实例中所有开启了行版本控制的数据库中所有已修改的行。这可能会急剧增加tempdb
的使用量。tempdb
中使用的空间不会在不再需要时立即释放。相反,有一个单独的线程会定期(通常每分钟一次)从tempdb
中清理不再需要的数据。- I/O 量增加。首先,数据必须放入
tempdb
,并且在查询时,必须从tempdb
中检索。 - CPU 占用量增加,因为需要对该功能进行管理操作。
- 数据行占用更多空间。每行必须包含有关事务序列号的信息以及指向版本化行的指针。
- 对于 LOB 字段,由于增加了头部信息,每个数据片段的空间会减少 40 字节。因此,与早期版本的 SQL Server 相比,数据库可能会显著增长。
结论
行版本控制易于设置,并且在同一数据被同时修改和读取的环境中可以实现更高的并发性。它会消耗更多资源,因此现有的服务器配置可能不足。正确使用时,它将对应用程序的整体吞吐量产生非常积极的影响。
如果应用程序的设计是基于读取在活动事务修改数据时是不可能的这一早期事实,那么不应使用行版本控制,或者必须在需要的地方重新设计应用程序。
参考文献
历史
- 2008年10月11日:创建
- 2008年10月15日:修正了
sp_lock
输出中的布局。 - 2012年11月3日:添加了脚本,并对照 SQL Server 2012 检查了文章。
- 2018年2月7日:将已弃用的
sp_lock
替换为sys.dm_tran_locks