并发事务下各数据库外部表现实测之一(SQL Server篇)

简介: 当同时有多个事务访问相同数据时,DBMS会采取锁或MVCC的机制确保数据的完整性。反映到应用程序上的表现可能就是等待或报错。不同DBMS因为采取的机制或策略不同,其外部表现也有很大差异。
当同时有多个事务访问相同数据时,DBMS会采取锁或MVCC的机制确保数据的完整性。反映到应用程序上的表现可能就是等待或报错。不同DBMS因为采取的机制或策略不同,其外部表现也有很大差异。于是笔者决定通过测试一探究竟。先以SQL Server作为对象进行测试。

一、 背景知识
对并发事务的数据完整性保护的强弱可通过隔离级别控制,如果过强会影响并发性能,过弱可能不满足应用的需要。SQL 标准用三个存在并发的事务时应该避免的现像定义了四个级别的事务隔离。 
脏读 
  一个事务读取了另一个未提交事务写入的数据。
不可重复读 
  一个事务重新读取前面读取过的数据,发现该数据已经被另一个已提交事务修改。
幻读 
  一个事务重新执行一个查询,返回一套符合查询条件的行,发现这些行因为其它最近提交的事务而发生了改变。

SQL 事务隔离级别
隔离级别
脏读
不可重复读
幻读
读未提交
可能
可能
可能
读已提交
不可能
可能
可能
可重复读
不可能
不可能
可能
可串行化
不可能
不可能
不可能

二、测试准备
1. 测试环境
OS:Windows 7
DBMS:SQL Server 2008 Express


2. 测试观点
考虑以下因素组合的的情况下2个并发事务的相互影响。
1)事务隔离级别
  读未提交,读已提交,可重复读,可串行化
2)DML语句
  select,insert,update,delete
3)访问对象
 表,同一行,不同行

3. 数据定义
使用下面具有代性的表定义,并插入两条记录
create table tb1(id int primary key,name varchar(30));
insert into tb1 values(1,'a');
insert into tb1 values(2,'a');

三、测试方法
同时开2个终端并开始事务块,先在终端1上执行一个SQL语句,不提交,然后在终端2上执行第二个SQL语句,观察第二个SQL语句是否受第一个SQL语句影响。
先执行的SQL语句根据作用范围,分为单行和整表的查询/更新/删除(单行处理一般会走索引,而整表处理则顺序扫描);后执行的SQL语句根据前一SQL语句作用范围,分为同一行,不同行和整表的操作。
在不同的事务隔离级别下,分别做以上测试。

先执行的SQL语句如下:
 名称  SQL语句
 单行查询  select * from tb1 where id = 1
 整表查询  select * from tb1
 插入  insert into tb1 values(5,'b')
 单行更新  update tb1 set name = 'b' where id = 1
 整表更新  update tb1 set name = 'b'
 单行删除  delete from tb1 where id = 1
 整表删除  delete from tb1

后执行的SQL语句,根据先执行的SQL语句有所不同。先执行的SQL语句是单行查询、单行更新或单行删除时,后执行的SQL语句如下:
 名称  SQL语句
 同一行查询  select * from tb1 where id = 1
 非同行查询  select * from tb1 where id = 2
 整表查询  select * from tb1
 同一行插入  insert into tb1 values(1,'c')
 非同行插入  insert into tb1 values(6,'c')
 同一行更新  update tb1 set name = 'c' where id = 1
 非同行更新  update tb1 set name = 'c' where id = 2
 整表更新  update tb1 set name = 'c'
 同一行删除  delete from tb1 where id = 1
 非同行删除  delete from tb1 where id = 2
 整表删除  delete from tb1

先执行的SQL语句是整表查询、整表更新或整表删除时,非同行的查询、更新和删除的对象为不存在的行,其他和先行SQL是单行操作时相同。
名称 SQL语句
同一行查询 select * from tb1 where id = 1
非同行查询 select * from tb1 where id = 100
整表查询 select * from tb1
同一行插入 insert into tb1 values(1,'c')
非同行插入 insert into tb1 values(6,'c')
同一行更新 update tb1 set name = 'c' where id = 1
非同行更新 update tb1 set name = 'c' where id = 100
整表更新 update tb1 set name = 'c'
同一行删除 delete from tb1 where id = 1
非同行删除 delete from tb1 where id = 100
整表删除 delete from tb1
注:黄色代表和先行SQL是单行操作时不同的地方

先执行的SQL语句是插入时,同一行代表和插入语句的id相同,非同行代表完全新的行。
名称 SQL语句
同一行查询 select * from tb1 where id = 5
非同行查询 select * from tb1 where id = 2
整表查询 select * from tb1
同一行插入 insert into tb1 values(5,'c')
非同行插入 insert into tb1 values(6,'c')
同一行更新 update tb1 set name = 'c' where id = 5
非同行更新 update tb1 set name = 'c' where id = 2
整表更新 update tb1 set name = 'c'
同一行删除 delete from tb1 where id = 5
非同行删除 delete from tb1 where id = 2
整表删除 delete from tb1
注:黄色代表和先行SQL是单行操作时不同的地方

四、测试结果
4种隔离级别下的测试结果如下

读未提交:

先执行SQL\后执行SQL
同一行查询 非同行查询 整表查询 同一行插入 非同行插入 同一行更新 非同行更新 整表更新 同一行删除 非同行删除 整表删除
单行查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
整表查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
插入

OK(*)

OK OK(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行更新 OK(*) OK OK(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表更新 OK(*) OK OK(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行删除 OK(*) OK OK(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表删除 OK(*) OK OK(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
OK(*):基于更新后数据,即发生了脏读
等待(*):如果先行的SQL提交,则基于更新后的数据,否则基于原来的数据

读已提交:

先执行SQL\后执行SQL
同一行查询 非同行查询 整表查询 同一行插入 非同行插入 同一行更新 非同行更新 整表更新 同一行删除 非同行删除 整表删除
单行查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
整表查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
插入

等待(*)

OK 等待(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行更新

等待(*)

OK

等待(*)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表更新

等待(*)

OK

等待(*)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行删除

等待(*)

OK

等待(*)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表删除

等待(*)

OK

等待(*)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
等待(*):如果先行的SQL提交,则基于更新后的数据,否则基于原来的数据
注:黄色代表和读未提交不同的地方

可重复读:

先执行SQL\后执行SQL
同一行查询 非同行查询 整表查询 同一行插入 非同行插入 同一行更新 非同行更新 整表更新 同一行删除 非同行删除 整表删除
单行查询 OK OK OK 等待 OK 等待 OK 等待 等待 OK 等待
整表查询 OK OK OK 等待 OK 等待 OK 等待 等待 OK 等待
插入

等待(*)

OK 等待(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行更新

等待(*)

OK

等待(*)

等待(*) OK 等待(*)

OK

等待(*) 等待(*) OK 等待(*)
整表更新

等待(*)

OK

等待(*)

等待(*) OK 等待(*)

OK

等待(*) 等待(*) OK 等待(*)
单行删除

等待(*)

OK

等待(*)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表删除

等待(*)

OK

等待(*)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
等待(*):如果先行的SQL提交,则基于更新后的数据,否则基于原来的数据
注:黄色代表和读已提交不同的地方

可串行化:

先执行SQL\后执行SQL
同一行查询 非同行查询 整表查询  同一行插入  非同行插入 同一行更新 非同行更新 整表更新 同一行删除 非同行删除 整表删除
单行查询 OK OK OK 等待 OK 等待 OK 等待 等待 OK 等待
整表查询 OK OK OK 等待 等待 等待 等待 等待 等待 等待 等待
插入

等待(*)

OK 等待(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行更新 等待(*) OK 等待(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表更新 等待(*) OK 等待(*) 等待(*) (*) 等待(*) (*) 等待(*) 等待(*) (*) 等待(*)
单行删除 等待(*) OK 等待(*) 等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表删除 等待(*) OK 等待(*) 等待(*) (*) 等待(*) (*) 等待(*) 等待(*) (*) 等待(*)
等待(*):如果先行的SQL提交,则基于更新后的数据,否则基于原来的数据
注:黄色代表和可重复读不同的地方

不难看出上述4种隔离级别是通过锁实现的,SQL Server中还支持基于行版本控制(MVCC)的隔离级别。启用这个功能需要打开下面2个选项

  1. ALTER DATABASE dbname SET ALLOW_SNAPSHOT_ISOLATION ON
  2. ALTER DATABASE dbname SET READ_COMMITTED_SNAPSHOT ON
注:详见 http://msdn.microsoft.com/zh-cn/library/ms175095(v=SQL.100).aspx

打开READ_COMMITTED_SNAPSHOT开关后,读已提交的实现方式及外部表现就不一样了。
读已提交(READ_COMMITTED_SNAPSHOT=ON):

先执行SQL\后执行SQL
同一行查询 非同行查询 整表查询 同一行插入 非同行插入 同一行更新 非同行更新 整表更新 同一行删除 非同行删除 整表删除
单行查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
整表查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
插入

OK(**)

OK

OK(**)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行更新

OK(**)

OK

OK(**)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表更新

OK(**)

OK

OK(**)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
单行删除

OK(**)

OK

OK(**)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
整表删除

OK(**)

OK

OK(**)

等待(*) OK 等待(*) OK 等待(*) 等待(*) OK 等待(*)
OK(**):基于更新前的数据,即看到是查询时的快照
等待(*):
如果先行的SQL提交,则基于更新后的数据,否则基于原来的数据
注:黄色代表和READ_COMMITTED_SNAPSHOT=OFF时的读已提交不同的地方

打开ALLOW_SNAPSHOT_ISOLATION开关后,可以使用SQL Server扩展的一种隔离级别SNAPSHOT,也称作SI(SNAPSHOT ISOLATION)。
SNAPSHOT满足SQL规范的可串行化隔离级别定义,即脏读、不可重复读和幻读都不会出现,但SNAPSHOT并不是真正的可串行化,关于这一点准备以后详细进行说明。

  1. SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

SNAPSHOT:

先执行SQL\后执行SQL
同一行查询 非同行查询 整表查询 同一行插入 非同行插入 同一行更新 非同行更新 整表更新 同一行删除 非同行删除 整表删除
单行查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
整表查询 OK OK OK 主键冲突 OK OK OK OK OK OK OK
插入

OK(***)

OK

OK(***)

等待(*) OK 等待(**) OK 等待(**) 等待(**) OK 等待(**)
单行更新

OK(***) 

 
OK

OK(***)

等待(*) OK 等待(**) OK 等待(**) 等待(**) OK 等待(**)
整表更新

OK(***)

OK

OK(***)

等待(*) OK 等待(**) OK 等待(**) 等待(**) OK 等待(**)
单行删除

OK(***)

OK

OK(***)

等待(*) OK 等待(**) OK 等待(**) 等待(**) OK 等待(**)
整表删除

OK(***)

OK

OK(***)

等待(*) OK 等待(**) OK 等待(**) 等待(**) OK 等待(**)
OK(***):基于更新前的数据,查询看到的是事务开始时的快照
等待(*):
如果先行的SQL提交,则基于更新后的数据,否则基于原来的数据
等待(**):如果先行的SQL提交,则报更新冲突的错误
注:黄色代表和READ_COMMITTED_SNAPSHOT=ON时的读已提交不同的地方


五、小结
SQL Server完整实现了SQL标准定义的4个隔离级别。并且还提供了基于MVCC的读未提交和SNAPSHOT隔离级别,可避免读和写之间的锁定提高并发性能。






相关文章
|
11月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS费用价格:MySQL、SQL Server、PostgreSQL和MariaDB引擎收费标准
阿里云RDS数据库支持MySQL、SQL Server、PostgreSQL、MariaDB,多种引擎优惠上线!MySQL倚天版88元/年,SQL Server 2核4G仅299元/年,PostgreSQL 227元/年起。高可用、可弹性伸缩,安全稳定。详情见官网活动页。
1622 152
|
11月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS支持MySQL、SQL Server、PostgreSQL和MariaDB引擎
阿里云数据库RDS支持MySQL、SQL Server、PostgreSQL和MariaDB引擎,提供高性价比、稳定安全的云数据库服务,适用于多种行业与业务场景。
1117 156
|
11月前
|
SQL 人工智能 Linux
SQL Server 2025 RC1 发布 - 从本地到云端的 AI 就绪企业数据库
SQL Server 2025 RC1 发布 - 从本地到云端的 AI 就绪企业数据库
778 5
SQL Server 2025 RC1 发布 - 从本地到云端的 AI 就绪企业数据库
|
10月前
|
SQL 存储 监控
SQL日志优化策略:提升数据库日志记录效率
通过以上方法结合起来运行调整方案, 可以显著地提升SQL环境下面向各种搜索引擎服务平台所需要满足标准条件下之数据库登记作业流程综合表现; 同时还能确保系统稳健运行并满越用户体验预期目标.
444 6
|
11月前
|
关系型数据库 分布式数据库 数据库
阿里云数据库收费价格:MySQL、PostgreSQL、SQL Server和MariaDB引擎费用整理
阿里云数据库提供多种类型,包括关系型与NoSQL,主流如PolarDB、RDS MySQL/PostgreSQL、Redis等。价格低至21元/月起,支持按需付费与优惠套餐,适用于各类应用场景。
|
11月前
|
SQL Oracle 关系型数据库
Oracle数据库创建表空间和索引的SQL语法示例
以上SQL语法提供了一种标准方式去组织Oracle数据库内部结构,并且通过合理使用可以显著改善查询速度及整体性能。需要注意,在实际应用过程当中应该根据具体业务需求、系统资源状况以及预期目标去合理规划并调整参数设置以达到最佳效果。
670 8
|
12月前
|
SQL 人工智能 Java
用 LangChain4j+Ollama 打造 Text-to-SQL AI Agent,数据库想问就问
本文介绍了如何利用AI技术简化SQL查询操作,让不懂技术的用户也能轻松从数据库中获取信息。通过本地部署PostgreSQL数据库和Ollama模型,结合Java代码,实现将自然语言问题自动转换为SQL查询,并将结果以易懂的方式呈现。整个流程简单直观,适合初学者动手实践,同时也展示了AI在数据查询中的潜力与局限。
1446 8
|
12月前
|
SQL 人工智能 Linux
SQL Server 2025 RC0 发布 - 从本地到云端的 AI 就绪企业数据库
SQL Server 2025 RC0 发布 - 从本地到云端的 AI 就绪企业数据库
457 5
|
11月前
|
缓存 关系型数据库 BI
使用MYSQL Report分析数据库性能(下)
使用MYSQL Report分析数据库性能
613 158
|
11月前
|
关系型数据库 MySQL 数据库
自建数据库如何迁移至RDS MySQL实例
数据库迁移是一项复杂且耗时的工程,需考虑数据安全、完整性及业务中断影响。使用阿里云数据传输服务DTS,可快速、平滑完成迁移任务,将应用停机时间降至分钟级。您还可通过全量备份自建数据库并恢复至RDS MySQL实例,实现间接迁移上云。