SQL Server中的MVCC
Tags: MVCCSQL Server
简介
ACID 是事务四大属性,理论上 Serializable 隔离级别是最能保证事务属性的。但在实际场景中,Serializable 隔离级别不利用高并发的访问。关系型数据库为了能在高并发下保证数据的一致性,提供了一种叫 Multi-Version Concurrency Control (MVCC)的机制。
主流的关系型数据库系统都实现了 MVCC,只是实现的方式不一样。
Oracle、SQL Server、PG默认隔离级别都是提交读,而MySQL默认隔离级别是可重复读。
你知道 SQL Server 是如何实现 MVCC 的吗?
SQL Server 数据库引擎使用锁或者行版本控制机制来确保事务的完整性,并在多个用户同时访问数据时保持数据库的一致性。锁和行版本控制可以防止用户读取未提交的数据,还可以防止多个用户尝试同时更改同一数据。如果不进行锁定或行版本控制,对数据执行的查询可能会返回数据库中尚未提交的数据,从而产生意外的结果。
下面,我们将一步步进行测试,理解 SQL Server 如何实现 MVCC 的。
1 2 3 4 5 6 7 | --创建简单的测试数据 CREATE TABLE dbo.Test(name VARCHAR(20) PRIMARY KEY CLUSTERED,info VARCHAR(20)) INSERT INTO dbo.Test VALUES('kk',NULL) --查看当前隔离级别 DBCC USEROPTIONS |
们构建2个事务,按顺序从上至下执行,分别对SQL定义了编号 SQL01~SQL05,以方便我们说明,后面测试皆以此模板为参考。
事务一 | 事务二 |
---|---|
begin tran --SQL01 select * from dbo.Testwhere name = 'kk' | |
begin tran --SQL02 update dbo.Testset info = '更改'where name = 'kk' | |
--SQL03 select from dbo.Test WITH(READPAST)where name = 'kk' --SQL04 select from dbo.Test WITH(NOLOCK)where name = 'kk' --SQL05 select * from dbo.Testwhere name = 'kk' |
测试一:
首先是最常见的隔离级别 READ COMMITTED ,了解事务之间是如何隔离的。把事务隔离级别更改为 READ COMMITTED,并按照示例中的2个事务顺序执行。
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
现象:
- SQL01开始正常执行,结果正常;
- SQL02可正常执行,更改数据;
- SQL03可正常执行,但无查询结果(该行被排除在外);
- SQL04可正常执行,结果为更新后的值;
- SQL05发生堵塞,请求共享锁,等待事务二结束;
结论:在隔离级别 READ COMMITTED下,事务中数据的更改只有在提交的情况下才能被读取,即不可重复读。这种情况在 SQL Server 是比较常见的,很容易引起堵塞或者死锁,不利于高并发。
测试二:
关系型数据库对数据一致性的保障,还有一种隔离脚本叫可重复读(REPEATABLE READ),那我们更改为这个隔离级别会怎样呢?我们更改事务隔离级别,其他步骤与上面的测试一样。
1 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ |
现象:
- SQL01开始正常执行,结果正常;
- SQL02发生堵塞,请求排他锁,等待事务一结束;
- SQL03可正常执行,结果为更新前的值;
- SQL04可正常执行,结果为更新前的值;
- SQL05可正常执行,结果为更新前的值;
结论:在隔离级别 REPEATABLE READ下,同一个事务可以重复读取了,但是其他事务的更改却被堵塞了。也就是说,REPEATABLE READ 中查询的“优先级”高,READ COMMITTED 中更改的“优先级”高。那这样的隔离级别似乎也没多大好处。
我们可以看看这两个事务的锁请求情况。
测试三:
除了四大事务隔离级别 Read uncommitted、Read committed、Repeatable read、Serializable ,SQL Server 提供了一种独特的快照隔离级别 Snapshot,以此来实现行版本控制,也就是我们常说的MVCC。该级别在读操作期间不使用共享锁。必须将 ALLOW_SNAPSHOT_ISOLATION 数据库选项设置为 ON,事务才能在快照隔离下运行。
1 | SET TRANSACTION ISOLATION LEVEL SNAPSHOTALTER DATABASE dbname SET ALLOW_SNAPSHOT_ISOLATION ON |
现象:
- SQL01开始正常执行,结果正常;
- SQL02可正常执行,更改数据;
- SQL03执行错误,此隔离级别不支持READPAST;
- SQL04可正常执行,结果为更新后的值;
- SQL05可正常执行,结果为更新前的值;
结论:在快照隔离级别Snapshot下,同一个事务可以重复读取,读取的是更改之前的版本值,并且也不再堵塞其他事务的更新,实现了MVCC。
我们看看事务一中获取的锁情况,发现只有一个数据库共享锁,也就是所谓的“读锁”。事务一并没有更细粒度的锁,也就不会堵塞其他事务的更改操作了。
其实,事务使用的是行版本控制而不是共享锁,tempdb 数据库必须具有足够的空间用于版本存储区。在 tempdb 已满的情况下,更新操作将停止生成版本,并继续执行,但是因为所需的特定行版本不再存在,读取操作可能会失败。这将影响诸如触发器、MARS 和联机索引的操作。只要活动事务需要访问行版本,就必须存储行版本。后台线程每隔一分钟删除一次不再需要的行版本,从而释放 tempdb 中的版本空间。
因此,在大批量数据操作时,尽量分多批事务进行处理,并及时提交/回滚事务,避免tempdb中的行版本数据过大。
查看tempdb行版本记录大小,此时可以看到版本存储区域分配了数据页。
1 2 3 4 5 6 7 8 | SELECT SUM(version_store_reserved_page_count) AS [version store pages used] ,(SUM(version_store_reserved_page_count)*1.0/128) AS [version store space in MB] FROM tempdb.sys.dm_db_file_space_usage; --其他相关DMV select * from sys.dm_tran_version_store select * from sys.dm_tran_active_snapshot_database_transactions |
Snapshot 是特定的事务隔离级别,事务使用行版本控制提供事务级读取一致性。
测试四:
SQL Server 还有另一种隔离级别 read committed snapshot,是在隔离级别 READ COMMITTED中设置数据库 READ_COMMITTED_SNAPSHOT为ON时出现,READ_COMMITTED 事务使用行版本控制提供语句级读取一致性。
1 2 3 | ALTER DATABASE dbname SET READ_COMMITTED_SNAPSHOT ON; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
现象:
- SQL01开始正常执行,结果正常;
- SQL02可正常执行,更改数据;
- SQL03可正常执行,结果为更新前的值;
- SQL04可正常执行,结果为更新后的值;
- SQL05可正常执行,结果为更新前的值;
结论:与“测试三”一样,在快照隔离级别read committed snapshot下,同一个事务可以重复读取,读取的是更改之前的版本值,并且也不再堵塞其他事务的更新,实现了MVCC。
以上我们已经测试了四种事务隔离级别,其中两种隔离级别实现了MVCC。在行版本控制中,大大提高了数据库的并发访问。
测试一 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED |
---|---|
测试二 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ |
测试三 | SET TRANSACTION ISOLATION LEVEL SNAPSHOTALTER DATABASE dbname SET ALLOW_SNAPSHOT_ISOLATION ON |
测试四 | SET TRANSACTION ISOLATION LEVEL READ COMMITTEDALTER DATABASE dbname SET READ_COMMITTED_SNAPSHOT ON |
对于大多数应用程序,建议使用行版本控制的读提交隔离(read committed snapshot)而不是快照隔离(snapshot),原因如下:
- 读提交隔离比快照隔离消耗更少的tempdb空间。
- 读提交隔离适用于分布式事务,而快照隔离则不然。
- 读提交隔离适用于大多数现有应用程序,无需任何更改,仍然使用默认隔离级别read committed。行版本控制由数据库选项READ_COMMITTED_SNAPSHOT控制。
- 读提交隔离不检查更新冲突,快照隔离容易受到更新冲突的影响。
- 读提交隔离提供语句级读取一致性。事务中的每个语句执行时,都会获取一个新的数据快照并保持每个语句的一致性,直到语句完成执行。快照隔离提供事务级别的读取一致性