活到老学到老

现学现卖

博客园 首页 新随笔 联系 订阅 管理

 

脏读:事务t1修改一个行,然后t2在t1提交之前读取改行;如果这时t1回滚或失败,那么t2就读取一个从来没有提交过的行,也就是从来没有在系统中存在过的行。读取新插入但未提交的行业属于脏读。

不可重复读:t1读取了一行,然后t2修改或删除改行并提交;如果t1再次读取改行,会发现内容已经改变或者找不到该行。于是可以断定有其它事务在搞它。

幻读:t1读取了满足条件a的一个行集,然后t2插入了满足a的一个或多个行并提交;如果t1再次试图读取符合条件a的行集,那么将会读到先前不存在的行。

任何事务无论其隔离级别如何,都具有原子性。原子性和隔离级别是两码事。
假定只有一种资源-数据表上的“行“可以被锁定。锁只有共享锁和排他锁两种。
锁的兼容性:
  SX
S 10
X 00

看一个例子,来自http://www.mssqlcity.com/Articles/Adm/SQL70Locks.htm

有4个并发的会话,试图锁定同一个数据行,假定并发被解决为下列顺序:

Process1 : SELECT
Process2 : SELECT
Process3 : UPDATE
Process4 : SELECT

Process1 在行上设置共享锁,因为行上这时还没有锁。

Process2 设置共享锁,因为和已有的共享锁兼容。
Process3 想要设置排他锁,但是因为和已有的所不兼容而被阻塞。在这种情况下SQL Server会发出一些意向锁来使Process3的意向对其它会话可见,例如加个更新锁。

Process4 按理说会加个共享锁访问数据(共享锁和更新锁相容),但是由于某些Process3加的其它意向锁的存在,Process4无法加共享锁,因此也被阻塞。这样的设计是为了防止在读繁忙的时候,写操作被无限期延后。例如,如果只有共享锁和排他锁的存在,那么在读重叠的情况下,就没法向数据库写数据了。

当 Process1 和 Process2 结束后, Process3 可以把更新锁升级为排他锁并修改数据, Process3 结束后 Process4 就可以加共享锁访问数据了。

 

更新锁  更新锁在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个事务申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这时,这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。http://it.china-b.com/olpl/473130_3.html

 

事务的隔离级别:
Read Uncommitted
数据读取时不需要锁定
Read Committed
数据读取时需要共享锁定
Repeatable Read
数据读取时需要排他锁
Serializable
事务必须等同于串行执行

注意无论任何时候更新行都必须要有排他锁。
插入行不需要锁。
在Read Committed隔离级别下,数据读取完毕后立即释放共享锁,而在Repeatable Read隔离级别下,事务保持共享

锁直到整个事务结束。

在SQL Server中调整事务隔离级别是针对会话的,set tran isolation level后,会话中后来开始的事务都在此隔

离级别上执行。一个事务只能具有一个隔离级别。同一会话中的所有事务必须串行执行。必须通过begin tran语句来覆盖默认事务范围。

 

测试:

 /*环境*/

CREATE TABLE [dbo].[TranTest](
 [Id] [int] IDENTITY(1,1) NOT NULL,
 [count1] [int] NULL,
 [count2] [int] NULL
)

insert into TranTest values(1,2)
insert into TranTest values(3,4)
insert into TranTest values(5,6)
insert into TranTest values(7,8)

 

 

约定总是先执行session1,并且在session1结束前执行session2
1. 排他锁总是在事务结束时释放

 

--session 1

set tran isolation level any
begin tran
 update TranTest set count1=count1 + 10 where id=1
 waitfor delay '00:00:10'
commit tran


--session 2

 select * from TranTest

现象:session2被阻塞直到session1中的会话结束。

解释:session1首先执行,更新id=1的行时持有了该行的排他锁,该锁直到session1中的事务结束时释放,这阻止

了session2获得共享锁。


2. 读未提交无需任何锁

--session 1

set tran isolation level any
begin tran
 update TranTest set count1=count1 + 10 where id=1
 waitfor delay '00:00:10'
commit tran

--session 2

set tran isolation level read uncommitted
begin tran
 select * from TranTest
commit tran

现象:session2未被阻塞,而是读取到了session1尚未提交的数据。
解释:读未提交无需锁定,因而绕开了锁定机制。


3. 死锁

--session 1

set tran isolation level read committed
begin tran
 update TranTest set count1=1000 where id=1
 waitfor delay '00:00:10'
 update TranTest set count1=1000 where id=2
commit tran

--session 2

set tran isolation level read committed
begin tran
 update TranTest set count1=2000 where id=2
 waitfor delay '00:00:10'
 update TranTest set count1=2000 where id=1
commit tran

现象:发生死锁,有一个事务被强制终止。
解释:session1持有了id=1的行的排他锁,等待id=2的行的排他锁;session2持有了id=2的行的排他锁,等待id=1

的行排他锁。从逻辑上说这样是会造成死锁的,但实际上在sql server中这种情况也很有可能不会导致死锁。这

是sql server死锁检测程序的功劳。当它预测到死锁可能发生的时候,可能会悬挂一个事务,等待另一个执行完成


 

 如果是运行在repeatable read隔离级别下,共享锁会在读取行之后保持。如果读取的行是连接而来的,则锁定参与连接的各个表中对应的行。查询中的子查询中参与的行也会一并被锁定。

 

--------------------------------------------------

最后关于新实现的快照隔离级别和基于行版本控制的read committed。没有实际使用过,不过根据MSDN上的描述,原理应该是这样的:

在Read Committed隔离级别下读取数据不加共享锁。如果被读取的行没有被其他事务修改,那么这一行就是已经提交的行;如果被读取的数据被加了排他锁,则根据行上的链接找到该行的前一版本。这个行上的链接是这么回事,如果一个事务想在一个行上加排他锁,则它必须把这一行的数据copy到temp db一份,然后在再上做一个到这个前一版本的链接。之后加排他锁的事务才可以尽情的修改原来的行。当提交的时候在把新的行版本插入到版本链中。

snapshot隔离级别是可重复读的。

当读取一个行的时候并不用加共享锁,而是读取行在本事务开始之前的版本。

 

posted on 2008-11-04 10:36  John Rambo  阅读(1261)  评论(1编辑  收藏  举报