SqlServer——事务一进阶之锁的概念(SqlServer技术内幕 T-SQL程序设计 第九章)
一、事务的概念及ACID特性
对于单独一条SQL语句,数据库会隐式的将其作为事务,即该SQL语句要么执行成功,要么失败(相当于不执行),而我们通常说的事务就是将多条SQL语句放在 begin Tran\commit Tran块中,显式的告诉数据库执行多条语句和执行一条语句一样,当在执行begin Tran\commit Tran块中的语句发生错误时,则回滚,即要么所有语句全部执行成功,要么一条也不执行。
事务的ACID(原子性;一致性;隔离性;持久性):
-
原子性:语句块作为一个操作单元,要么所有语句全部执行成功,要么一条也不执行。
-
一致性:执行事务时,通过获取锁来隔离数据资源,以阻塞其他进程对该资源的操作,防止出现不一致;
-
隔离性:事务的执行是互不干扰的。一个事务不可能看到其他事务运行时某一时刻的数据。在Windows中,如果多个进程对同一个文件进行修改是不允许的,Windows通过这种方式来保证不同进程的隔离性;
-
持久性:即在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中。
二、对数据库修改的过程:
SqlServer数据库将文件划分N个8KB的存储空间,也就是说页是Sqlserver 中数据存储的基本单位,页的大小为8KB。我们的程序在执行SQL语句对数据进行修改时,此时SqlServer检查受影响的页是否位于缓存中,当不再缓存中时先从磁盘中将相关的页加载到缓存中修改,此时SqlServer将本次修改记录到数据库的事务日志中,然后再将数据更改到数据库的磁盘文件中。这种机制正是为了维护事务,当需要或事务执行过程中发生错误时,SqlServer根据事务日志的记录回滚。
三、事务中锁
事务的隔离性是通过锁来实现的。可以获取不同粒度级别资源上的锁,表(RowLock)、页(PagLock)、行(TableLock) 三个级别,其中堆(没有聚集索引的表)中的行被锁住时,被锁的资源是行标志,而索引上的行被锁住,被锁的资源是键。通过表提示来指定要锁定的资源类型。
锁可以分为以下几种:
-
共享锁:SQL Server中,共享锁用于所有的只读数据操作。共享锁是非独占的,允许多个并发事务读取其锁定的资源。默认情况下,数据被读取后,SQL Server立即释放共享锁。例如,执行查询“SELECT * FROM AUTHORS”时,首先锁定第一页,读取之后,释放对第一页的锁定,然后锁定第二页。这样,就允许在读操作过程中,修改未被锁定的第一页。但是,事务隔离级别连接选项设置和SELECT语句中的锁定设置都可以改变SQL Server的这种默认设置。例如,“ SELECT * FROM AUTHORS with(HOLDLOCK)”就要求在整个查询过程中,保持对表的锁定,直到事务提交才释放锁定。
-
更新锁:用于可能更新的资源中, 更新锁在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个事务申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这时,这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。而更新锁与排它锁类似只能有一个事务获取,避免了数据修改时由多个相同的锁向排它锁升级时造成的死锁,直到事务提交才释放共享锁。
-
排它锁:排它锁是为修改数据而保留的。它所锁定的资源,其他事务不能读取也不能修改。同一时刻对于相同的数据,只能有一个进程获得,与其他所有锁互斥,即数据有其他锁时,另一个事务不能获得排它锁,或者数据有其他锁时,其他事务不能获得任何锁,直到事务提交才释放排它锁。
-
架构锁:由更新对象架构或依赖与对象架构的进程获取。架构锁的类型为:架构修改 和架构稳定性。执行表的数据定义语言 (DDL) 操作(例如添加列或除去表)时使用架构修改锁。当编译查询时,使用架构稳定性锁。架构稳定性锁不阻塞任何事务锁,包括排它锁。因此在编译查询时,其它事务(包括在表上有排它锁的事务)都能继续运行。但不能在表上执行 DDL 操作。
-
意向锁:意向锁说明SQL Server有(向锁层次结构中较高的资源请求其在资源的低层获得共享锁或排它锁)的意向。例如,表级的共享意向锁说明事务意图将共享锁释放到表中的页或者行。意向锁又可以分为共享意向锁、独占意向锁和共享式独占意向锁。共享意向锁说明事务意图在共享意向锁所锁定的低层资源上放置共享锁来读取数据。独占意向锁说明事务意图在共享意向锁所锁定的低层资源上放置排它锁来修改数据。共享式排它锁说明事务允许其他事务使用共享锁来读取顶层资源,并意图在该资源低层上放置排它锁。
-
大容量更新锁 当将数据大容量复制到表,且指定了 TABLOCK 提示或者使用 sp_tableoption 设置了 table lock on bulk 表选项时,将使用大容量更新 锁。大容量更新锁允许进程将数据并发地大容量复制到同一表,同时防止其它不进行大容量复制数据的进程访问该表。
锁的粒度:
锁粒度是被封锁目标的大小,封锁粒度小则并发性高,但开销大,封锁粒度大则并发性低但开销小。
SQL Server支持的锁粒度可以分为为行、页、键、键范围、索引、表或数据库获取锁。
RID | 行标识符。用于单独锁定表中的一行。 |
键 | 索引中的行锁。用于保护可串行事务中的键范围。 |
页 | 8 千字节 (KB) 的数据页或索引页。 |
扩展盘区 | 相邻的八个数据页或索引页构成的一组。 |
表 | 包括所有数据和索引在内的整个表。 |
DB | 数据库。 |
在SQL语句中设置锁的具体格式如下:
HOLDLOCK、 SERIALIZABLE |
将共享锁保留到事务完成,否则,共享锁在相应的表、行或数据页不再需要时就立即释放。HOLDLOCK 等同于 SERIALIZABLE 。 |
NOLOCK、 READUNCOMMITTED |
不要发出共享锁,并且不要提供排它锁。当此选项生效时,可能会读取未提交的事务或一组在读取中间回滚的页面。有可能发生脏读。仅应用于 SELECT 语句。与READUNCOMMITTED 相同 |
PAGLOCK | 在通常使用单个表锁的地方采用该锁。 |
READCOMMITTED | 用于运行在提交读隔离级别的事务相同的锁语义执行扫描。默认情况下,SQL Server 2000 在此隔离级别上操作。 |
READPAST | 跳过锁定行。此选项导致事务跳过由其它事务锁定的行(这些行平常会显示在结果集内),而不是阻塞该事务,使其等待其它事务释放在这些行上的锁。READPAST 锁提示仅适用于运行在提交读隔离级别的事务,并且只在行级锁之后读取。仅适用于 SELECT 语句。 |
REPEATABLEREAD | 用与运行在可重复读隔离级别的事务相同的锁语义执行扫描。 |
ROWLOCK | 使用行级锁,而不使用粒度更粗的页级锁和表级锁。 |
TABLOCK | 使用表锁代替粒度更细的行级锁或页级锁。在语句结束前,SQLServer 一直持有该锁。但是,如果同时指定 HOLDLOCK,那么在事务结束之前,锁将被一直持有。 |
TABLOCKX | 使用表的排它锁。该锁可以防止其它事务读取或更新表,并在语句或事务结束前一直持有。 |
UPDLOCK | 读取表时使用更新锁,而不使用共享锁,并将锁一直保留到语句或事务的结束。UPDLOCK的优点是允许您读取数据(不阻塞其它事务)并在以后更新数据,同时确保自从上次读取数据后数据没有被更改。 |
XLOCK | 使用排它锁并一直保持到由语句处理的所有数据上的事务结束时。可以使用 PAGLOCK 或TABLOCK 指定该锁,这种情况下排它锁适用于适当级别的粒度 |
-
粒度提示:PAGLOCK、NOLOCK、READCOMMITTEDLOCK、ROWLOCK、TABLOCK 或 TABLOCKX。
-
隔离级别提示:HOLDLOCK、NOLOCK、READCOMMITTED、REPEATABLEREAD 和 SERIALIZABLE。
当你显示设置锁时,可在SQL语句中的 from table_name with(lock) 来设置锁的模式。
注1:WITH ( < table_hint > )指定由查询优化器使用的表扫描、一或多个索引,或由查询优化器利用此数据表以及为此语句使用锁定模式。
注2:WITH (NOLOCK)相当于READ UNCOMMITTED
示例如下:
1、创建如下表并插入数据:
create table student_cj
(
name varchar(20) not null ,
km varchar(10) not null ,
cj int not null
)
insert into student_cj values('小明','数学',100),('小明','语文',100),('小李','语文',100),('小虎','语文',100),('小强','语文',100),('小龙','语文',100),
('小李','数学',100),('小虎','数学',100),('小强','数学',100),('小龙','数学',100)
select * from student_cj
如图
2、在新建查询中,执行以下代码,
(1)begin tran
select name,km ,cj from dbo.student_cj with(XLOCK) where name ='小明'
waitfor delay '00:00:15' --延迟15秒
commit tran
(2)再次新建查询,执行以下代码:
begin tran
select name ,km,cj from dbo.student_cj with(holdlock) --使共享锁保持在整个事务期间,在事务结束时释放锁
commit tran
在(1)中,会出现15秒的“正在执行查询”的等待,在此期间执行(2),也会出现等待,直到2执行完成。
原因是(1)中是一个独占锁,而独占锁与其他锁是排斥的,(2)中的共享锁只能等(1)中的事务执行完成才能获得共享锁。
3、在新建查询中,执行以下代码,
(1)begin tran
select name,km ,cj from dbo.student_cj with(holdlock) where name ='小明'
waitfor delay '00:00:15' --延迟5秒
commit tran
(2)再次新建查询,执行以下代码:
begin tran
select name ,km,cj from dbo.student_cj with(holdlock)
commit tran
在(1)中,会出现15秒的“正在执行查询”的等待,在此期间执行(2),却立即执行完成。
这是因为在(1)中获得了共享锁,相同时刻,相同的数据可以接受多个共享锁,所以在(1)的执行过程中并没有阻塞(2)中的事务执行。
总结:
1、SqlServer会自动选择资源类型进行锁定,也可自动锁定锁模式。因此我们最好不要自己设置。以免出现不必要的死锁。
Sql Server 在 INSERT、 UPDATE 或 DELETE 命令时,SQL Server 会自动设置使用独占锁。
2、WITH (NOLOCK)可以有效提升SQL的查询效能,不过有一点千万要注意的就是,WITH (NOLOCK)的SQL SELECT有可能会造成脏读。当使用NoLock时,它允许阅读那些已经修改但是还没有交易完成的数据。因此如果有需要考虑transaction事务数据的实时完整性时,使用WITH (NOLOCK)就要好好考虑一下。因为SQL Server会执行对应的锁定一致性检查。加了WITH (NOLOCK)即告诉SQL Server,我们的这段SELECT指令无需去考虑目前table的transaction lock状态,因此效能上会有明显的提升,而且数据库系统的Lock现象会有明显的减少(包含Dead Lock)。
相关链接:http://www.cnblogs.com/kele99999/archive/2009/03/09/1406617.html