SQL2005中的事务与锁定(一到九整合版)

出处:http://blog.csdn.net/happyflystone

终于定下心来写这个事务与锁定的文章,下笔后才发现真的很难写,交易中事务与锁定这个话题过于复杂,甚至有些还摸不着(就是通过DMV或DMF或其它工具你可能很难看到它的踪影),让人有点让人望而止步,但是既然说了要写,似乎不继续下去不是我的风格。在接下来的几篇文章(其实我也不知道要几篇)里我就事务与锁定这个话题写写,由SQL2005的并发模型引入事务,在事务的概念里展开锁定,本着先概念后实例的原则,和大家一起来学习,有不当之处希望大家指正。

一、并发及并发控制模型

对于这个我在<< SQL2005数据库引擎结构>>一文有所提及,你可以通过如下链接进行访问:SQL2005数据库引擎结构(三)并有一起的意思,显然就是多个的意思啦,光书面来理解并发就是多个东西同时发生,在数据库并发就是多个进程同时取、存数据库里数据的能力。着眼我们开发的系统,当然是激动态的并互不打架的并发用户进程越多并发能力就越强大啦,你想想看好多的网上购物系统,如果没有并发处理的能力,那么在上面登记的用户信息、商品有库存信息及用户帐户信息很难保证正确性和一致性,比如一个物品本身库存只有100个,结果如果100人同时在线进行预定,库存就有可能搞一个100-1的效果出来。

很显然对上述的例子我们希望一个进程在修改库存数据时必须阻止其它进程读或修改数据,或是正在读的用户进程限制其它活动的用户进程进行读或修改的操作,这样一来势必造成系统的并发性能下降,但是如果不采用这种办法又无法保证数据正确性和一致性。那怎么解决这个问题呢,办法只有通过不同的并发模式来管理这些并发事件。我们下面来理解并发控制的模式、并发下可能发生的非一致数据行为,即并发副作用,并由模式及数据行为引入事务及相关的5个隔离等级等概念,进而来理解不同隔离等级下并发实现的机理,显然我们自己也就可以回答上面这个问题了。

并发控制模式:一般并发控制模式有两种:积极并发(又称乐观并发)和消极并发(又称悲观并发)。积极并发是SQL2005才引入的新模式,在2005以前的版本其实只有唯一的并发模式即:消极并发。那什么是消极并发呢?消极并发就是SQLSERVER默认行为是进程以获取锁的方式来阻止其它进程对正在使用的数据进行修改的能力。对于数据库来说对数据修改的操作进程肯定很多,这些进程肯定都会去影响其它进程读取数据的能力,反之,对数据进行读时加上锁也一定会影响其它进程修改数据的能力,简而言之,就是读取与修改数据之间是冲突的、互相阻塞的。乐观并发是SQL2005利用一个行版本控制器的新技术来处理上述的冲突。行版本控制器在当前进程读取数据时生成旧版本的数据,使得其它请求读的进程能看到当前进程一开始读取时的数据状态,并且不受当前进程或其它进程对数据进行修改的影响。简而言之读与修改之间是不冲突的,但是修改与修改之间还是冲突的。

对于这两种并发模式两个进程同时请求数据修改必然会冲突的,除此以外的差别在于一个是在冲突发生前进行控制,另一个在冲突发生了进行协调处理。这好比生活一样,两种方式就是两种不同的人生,一种消极怠工一种积极向上。

二、并发下可能发生的并发副作用:丢失更新、脏读、不可重复读、幻影。

为了把这些可能发生的并发副作用说清楚,我们先“布置”一个场景:这是一个卖工艺石头的小商店,平时在前场完成交易,客户凭单据到后场领取石头,AMM和BMM是营业员,她们平时掌握库存数是通过大厅里的一块LED显示牌得之,并且在各自完成一笔交易后修改LED显示,以保证数据的实时性。在这个场景下我们来观察可能发生的行为:

1、 丢失更新:

丢失更新估计是所有数据库用户都不想发生的情况,什么是丢失更新呢?丢失更新是当2个或两个以上的用户进程同时读取同样的数据后又企图修改原来的数据时就会发生。好在上述场景下,大厅LED显示牌显示当前库存1000,这时同时有两个客户上门了,AMM和BMM满面春风接待,比如AMM卖出1个,BMM呢卖出了10个,AMM处理完业务后赶紧把LED显示数修改为1000 - 1 = 999个,几乎同一时间BMM处理完自己的业务后习惯性的把LED显示数修改为1000 - 10 = 990 个,这时老板从后场过来,看着LED有点不爽,大吼一声:现在还有多少存货呀?,AMM说我卖了1个,BMM说我是10个,不过两个人都傻眼了,LED显示怎么是990呢?原来BMM在更改时把AMM做的更改搞丢了,这就是丢失更新。显然对老板和营业员来说都是必须回避不能发生的事。

2、 脏读

很显然,在上面的例子里因为AMM和BMM事先因为不知道对方已经修改了柜台存货,所以才造成了存货数目显示错误,出了问题我们要想办法解决问题,英明的老板说了,你们随便哪个在谈一笔生意时先把客户意向购卖石头数扣掉,如果最后客户不要你再改回头,两个MM对老板的英明决定表示等赞同,可是问题还是发生了,怎么回事呢,还是假设柜台存货1000个石头,AMM有一笔生意正在谈着,顾客意向要600块石头,AMM赶紧把LED显示修改为400。这时BMM也很兴奋因为她已经谈成一笔700块石头的生意,所以呢BMM抬头一看,好嘛,还有400块可卖,完了BMM的生意做不成了,只好向客户表达歉意。BMM只能让老板进货,可是老板一看LED显示还有1000块怎么你的700块生意做不成了呢?哦,因为最后AMM的600块生意没做成。嘿嘿,也就是BMM错误的读取了AMM修改的数据,完成了一次“脏读”操作。脏读也就是一个用户进程读取了另一个用户进程修改过但没有正式提交的数据,这时导致了数据不一样的情形发生了。因为A用户进程是无法确认另一个B用户进程在自己提交数据前是否修改过数据,这是数据库系统默认情况下必须回避的。

3、 不可重复读

不可重复读又称不一致分析,不过,个人以为似乎不一致分析更让人好理解一点,但是大部分地方称不可重复读。不可重复读是指一个用户进程两次读取数据得到不同样的数据。比如那个英明的老板吧,他知道要盘点,掌握库存的变化,忙得満头大汗,终于计出库存数来,比如说1000吧,或是当他跑到大厅一看LED显示牌却只有了900,显然这一次的检查库存的过程中两次得到库存数不一样,原因就是AMM在老板从后场走到前场的过程中做了一担生意,卖出100块。嘿嘿,老板气又不是不气又不是,这AMM真可爱,做生意挺两下呀!显然在一个用户进程两次读取数据间隔内另一个用户进程修改了数据,这就是不可重复读。

4、 幻影

幻影,嘿嘿,我们不是经常无视BS自己的人吗?你无视他并不代表他不BS你吧,这个BS你的人就成了幻影,嘿嘿开个玩笑。这种情况多数在查询带谓词时结果集内部分数据变化的时候发生,如果谓词限定下在一个交易里两次同一查询的结果集不同,那些不同的行或行集就是幻影。比方说英明的老板到大厅走走,顺便请大家吃饭,数数人数,BMM,。。。。一路数过去,发现有10人,呵呵,正好一桌,,通知好她们后老板回办会室拿人民币,回到前场看见AMM,再一数11人,晕,刚才怎么看到AMM ?AMM也知道了老板请客没数到他,很是生气,这时AMM就成了幻影。

以上是四种并发副作用只是一个交易事务里或事务间可能发生的异常的非一致的数据行为(记好并发副作用和不一致的数据行为术语,这在以后会经常提及),其实还是有好多的行为是我们所期望的,那么我们期望的行为是什么呢,下面我们在事务里来介绍。我们可以通过隔离级别来设定一个合适级别以决定上述上种数据行为哪些是允许的。那什么是交易事务,什么又是隔离等级呢?

三、事务

事务是数据库一笔交易的基本单元,存于两种并发模型中。又分为显式事务和隐式事务。显式事务是显式的开始一个事务并显式的滚回或提交事务,除了显式的事务还有隐式的了,隐式事务是数据库自己根据情况完成的事务处理,如单独的select、update、delete、select语句。

作为一个事务,它能保证数据库的数据一致性及重做。提到事务不得不提及事务的ACID属性:原子性、一致性、隔离性及持久性。不管是显式还是隐式的,都必须维持这四个属性。

原子性:一个事务是一个整体,要不全部提交,要不全部中止。意思就是要不全部成功提交到数据,要不全部回滚恢复事务开始前的状态。比方我们做一个入库操作,在这个事务里,审核入库单和修改库存作为一个整体,要不单据变成审核过同时库存增加相应的值,要不就是单据未审核同时库存不变。

一致性:一致性要求事务保证数据在逻辑上正确,处理的结果不是一个不确定的状态,什么是不确定状态呢,比如说我们完成一个库存减少的操作,如果没有一个出货单据那么这个库存的当前修改就是一个不确定状态,因为你无法知道减少的东东到哪儿去了。

隔离性:这个隔离和锁定有关,以后在说锁的过程中会提到这些,你先记住这个就行。

持久性:持久很显然是要求正确提交的修改必须保证长久存在,不会因为关机或掉电把这笔交易丢掉。进行中的事务发生故障那事务就完全撤销,像没有发生一样,如果事务提交的确认已经反馈给应用程序发生故障,那么这些日志利用先写技术,在启动恢复阶段自动完成相应的动作保证事务的持久性。(这个在前面的引擎组件有过介绍哦。)

四、隔离等级

首先来说说隔离,隔离是一个事务必须与其他事务所进行的资源或数据更改相隔开,显然隔离等级就是相隔的程度了吧。在说隔离级别不得不提及锁的概念,但是在本单不提及锁,在以后听章节里再作说明,大家只要有个印象就行。在这儿我们必须明白两件事:

1,隔离级别不会影响进程获得数据修改的排它锁,并且这个锁会保存到事务结束。相对于读进程来说,隔离级别就是对读操作的一个保护级别,保护读操作受其它事务影响的程序。

2,较低的隔离级别可以增强许多用户同时访问数据的能力,但也增加了用户可能遇到的并发副作用(例如脏读或丢失更新)的数量。相反,较高的隔离级别减少了用户可能遇到的并发副作用,却需要太多的系统资源及一个事务阻塞其他事务的可能性。

应平衡应用程序的完整性要求与相应隔离级别的系统开销,在此基础上选择相应的隔离级别。最高隔离级别(可序列化)保证事务在每次重复读取操作时都能准确检索到相同的数据,但需要通过执行某种级别的锁定来完成此操作,而锁定可能会影响其他用户进程。最低隔离级别(未提交读)可以检索其他事务已经修改但未提交的数据。在未提交读中,所有并发副作用都可能发生,但因为没有读锁定或修改阻塞读取,所以开销最少。

不同的隔离级别决定我们有哪些数据副作用可以发生,而并发模型决定不同隔离等级下如何来限制这些数据行为或如何协调这数据行为。好,那我们来关注一下不同隔离等级下如何限制这些行为的发生。

未提交读(uncommitted Read):字面理解一下,修改了的未提交数据可以读取。准确点:一个用户进程可以读取另一个用户进程修改却未提交的数据。SQL SERVER对这个等级下的读操作不需要获得任何锁就可以读取数据,因为不需要锁所以不会和其它任何进程互相阻塞,自然而然能读取其它进程修改了的却未提交数据。显然这不是我们理想的一种模式,但是它却有了高并发性,因为读操作没有锁不会影响其它进程的读或写操作。在这种级别下,除了丢失更新(上一讲中的数据可能发生的行为)外,其它行为都有可能发生,冒着数据不一致的风险来避免修改的进程阻塞读取的进程,事务的一致性肯定是得不到保障,显然这是消极并发模式下的回避阻塞频繁的一种解决方案。未提交读那肯定是不适合于股票、金融系统的,但是在一些趋势分析的系统里,要求的只是一种走向,准确性可以不是那么严格时,这个级别因并发性能超强成为首选。

已提交读(Read Committed):它和未提交读相反,已提交读级别保证一个进程不可能读到另一个进程修改但未提交的数据。这个级别是引擎默认的级别,也是2005乐观并发模式下支持的级别,也就是说已提交读可是乐观的也可以是悲观的,那究竟当前库是属于哪个并发模型下的已提交读呢,这取决于一个READ_COMMITED_SNAPSHOT数据库配置项,并且缺省是悲观并发控制的。这个配置项决定已提交读级别下事务使用锁定还是行版本控制,并很显然行版本控制是乐观并发模式,锁定是悲观并发模式。我们来点角本看看:

--设置已提交读隔离使用行版本控制

ALTER DATABASE testcsdn SET READ_COMMITTED_SNAPSHOT ON

GO

--查看当前已提交读隔离并发模型

select name,database_id,is_read_committed_snapshot_on from sys.databases

/*

name database_id is_read_committed_snapshot_on

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

master 1 0

tempdb 2 0

model 3 0

msdb 4 0

ReportServer$SQL2005 5 0

ReportServer$SQL2005TempDB 6 0

TestCsdn 7 1 --current

(7 行受影响)

*/

--设置已提交读隔离使用锁定

ALTER DATABASE testcsdn SET READ_COMMITTED_SNAPSHOT OFF

GO

--查看已提交读隔离并发模型

select name,database_id,is_read_committed_snapshot_on from sys.databases

/*

name database_id is_read_committed_snapshot_on

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

master 1 0

tempdb 2 0

model 3 0

msdb 4 0

ReportServer$SQL2005 5 0

ReportServer$SQL2005TempDB 6 0

TestCsdn 7 0 --curret

(7 行受影响)

*/

已提交读在逻辑上保证了不会读到不实际存在的数据。悲观并发下的已提交读,当进程要修改数据时会在数据行上申请排它锁,其它进程(无论是读还是写)必须等到排它锁释放才可以使用这些数据。如果进程仅是读取数据时会使用共享锁,其它进程虽然可以读取数据但是无法更新数据,必须等到共离锁释放(共享锁在数据处理完即释放,比如行共享锁在当前数据行数据处理完就自动释放,不会在整个事务内保留发。)。乐观并发的已提交读,也确保不会读到未提交的数据,不是通过锁定的方式来实现,而是通过行版本控制器生成行的提前交的数据版本,被修改的数据虽然仍然锁定,但是其它进程可以可以读取更新前版本数据。

可重复读(Repeatable Read):这也是一个悲观并发的级别。可重复读比已提交读要求更严格,在已提交读的基础上增加了一个限制:获取的共享锁保留到事务结束。在这个限制下,进程在一个事务里两交次读取的数据一致,也就是不会读取到其它进程修改了数据。在这儿我们提到共享锁会保留到事务结束,那得申明一下无论哪种级别及并发模型,排它锁是一定要保留到事务结束的。在可重复读级别共享锁同样也会保留到事务结束。那么这种对数据安全的保证是通过增加共享保留的开销为代价的,也就是只要开始一个事务,其它用户进程是不可能修改数据的,显而易见的系统的并发性和性能必然下降。这似乎是我们想像中的一种级别,虽然这个级别暂时无法回避幻影读,而且我们也默许并发及性能下降,那只有对程序员对事务的控制有严格的要求:事务要短并尽量不要人为因素的干扰,减少潜在的锁竞争。

快照(SnapShot):乐观并发级别。这是2005新增加的一个隔离级别。快照级别与使用乐观并发的已提交读差不多,差别在于行版控制器里的数据版本有多早,这个在以后讲锁时再说。这个级别保证了一个事务读取的数据是事务开始时就在数据库逻辑上确认并符合一致性的数据。读操作不会要求共享锁定,如果要求的数据已经排它,就会通过行版本控制器读取最近的符合一致性的数据。

可串行化:是目前最严谨、最健壮的一个级别,属于悲观并发。它防止幻影的发生,回避了以前所有意外行为的发生。可串行化意味着系统按进程进入队列的顺序依次、序列化的执行的结果与事务同时运行得到一致的结果。这个最健壮的级别显然共享锁也是随事务开始随事务结束,并通过锁定部分不存在的数据(即索引键范围锁定)来回避幻影的发生。

在前面的两篇里我从纯理论上把事务相关的知识作了一个梳理,有人看了一定觉得无味了吧,好这一篇我们加入一点T-SQL语句把前面所说有东东关联起来,我们人为产生锁定来理解不同的意外数据行为在不同隔离等级下的表现,顺便再重温一下意外数据行及隔离等级,让大家对交易事务有一个直观的认识。

在进行实例前不得不先介绍一点锁的知识,注意这儿只是简单的说一下,不作深入讨论。我们根据用户访问资源的行为先归纳出几种锁,这几种锁在下面的实例里会出现,它们为:共享锁定、排它锁定、更新锁定及用意向这个限定词限定的三种锁(意向共享、意向排它、意向更新),当当然还有其它的模式,我们在下一篇再说。意向锁的存在是解决死锁的发生,保证进程在申请锁定前确定当前数据是否存在不兼容性的锁定。

先对上面提到的锁作一个简单的描述,更详细的下面再说。

共享锁定发生在查询记录时,直观就是我们select啦,但是并不是只有select才有共享锁定。一个查询记录的语句必须在没有与共享锁定互斥锁定存在或等待互斥锁定结束后,才能设置共享锁定并提取数据(互斥不互斥就是锁的兼容性,这在以后再说明)。

排它锁定发生在对数据增加、删除、修改时,事务开始以后语句申请并设置排它锁定(前提是没有其它互斥锁定存在),以明确告知其它进程对当前数据不可以查询或修改,等待事务结束后其它进程才可以查询或修改。

更新锁定是一个介于共享与排它之间的中继锁定,比如我们带where条件的update语句,在查询要更新的记录时是设置共享锁定,当要更新数据时这时锁定必须由共享锁定升级成更新锁定继而升级为排它锁定,当排它锁定设置成功才可以进行数据修改操作。显然也是要要求在锁升级的过程中没有互斥锁定的存在。简单的理解更新锁定是一个中继闸一样,把升级成排它锁定进程“序列化”,以解决死锁。最后重点说明一下,数据更新阶段是要对数据排它锁定不是更新锁定,不要被字面意思训导哦。

最后说一下在上述锁定模式下的互斥,共享锁定只与排它锁定互斥,更新锁定只与共享锁定不互斥。

在进行具体实例前我们一定要有一个工具来对我们实例过程进行监控,好,下面我写了一个过程,在需要时直接调用就行,过程如下:

Create Proc sp_us_lockinfo

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

-- Author : HappyFlyStone

-- Date : 2009-10-03 15:30:00

-- BLOG : http://blog.csdn.net/happyflystone

-- 申明 :请保留作者信息,转载注明出处

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

AS

BEGIN

SELECT

DB_NAME(t1.resource_database_id) AS [数据库名],

t1.resource_type AS [资源类型],

-- t1.request_type AS [请求类型],

t1.request_status AS [请求状态],

-- t1.resource_description AS [资源说明],

CASE t1.request_owner_type WHEN 'TRANSACTION' THEN '事务所有'

WHEN 'CURSOR' THEN '游标所有'

WHEN 'SESSION' THEN '用户会话所有'

WHEN 'SHARED_TRANSACTION_WORKSPACE' THEN '事务工作区的共享所有'

WHEN 'EXCLUSIVE_TRANSACTION_WORKSPACE' THEN '事务工作区的独占所有'

ELSE ''

END AS [拥有请求的实体类型],

CASE WHEN T1.resource_type = 'OBJECT'

THEN OBJECT_NAME(T1.resource_ASsociated_entity_id)

ELSE T1.resource_type+':'+ISNULL(LTRIM(T1.resource_ASsociated_entity_id),'')

END AS [锁定的对象],

t4.[name] AS [索引],

t1.request_mode AS [锁定类型],

t1.request_session_id AS [当前spid],

t2.blocking_session_id AS [锁定spid],

-- t3.snapshot_isolation_state AS [快照隔离状态],

t3.snapshot_isolation_state_desc AS [快照隔离状态描述],

t3.is_read_committed_snapshot_on AS [已提交读快照隔离]

FROM

sys.dm_tran_locks AS t1

left join

sys.dm_os_waiting_tasks AS t2

ON

t1.lock_owner_address = t2.resource_address

left join

sys.databases AS t3

ON t1.resource_database_id = t3.database_id

left join

(

SELECT rsc_text,rsc_indid,rsc_objid,b.[name]

FROM

sys.syslockinfo a

JOIN

sys.indexes b

ON a.rsc_indid = b.index_id and b.object_id = a.rsc_objid) t4

ON t1.resource_description = t4.rsc_text

END

GO

/*

调用示例:exec sp_us_lockinfo

*/

exec sp_us_lockinfo

/*

SQL2005中的事务与锁定(一到九整合版)_第1张图片

*/

drop proc sp_us_lockinfo

最后介绍一个隔离等级设置命令:

SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED

| READ COMMITTED

| REPEATABLE READ

| SNAPSHOT

| SERIALIZABLE}[;]

好,下面开始实例“快乐”之旅了。

五、隔离等级实例

测试数据准备:

CREATE DATABASE testcsdn;

GO

CREATE TABLE TA(TCID INT PRIMARY KEY,TCNAME VARCHAR(20))

INSERT TA SELECT 1,'AA'

INSERT TA SELECT 2,'AA'

INSERT TA SELECT 3,'AA'

INSERT TA SELECT 4,'BB'

INSERT TA SELECT 5,'CC'

INSERT TA SELECT 6,'DD'

INSERT TA SELECT 7,'DD'

GO

约定:以下提及的查询N,都是打开一个新连接执行查询

1、 未提交读(uncommitted Read)

概念回顾:未提交读是最低等级的隔离,允许其它进程读取本进程未提交的数据行,也就是读取数据时不设置共享锁定直接读取,忽略已经存在的互斥锁定。很显然未提交读这种隔离级别不会造成丢失更新,但是其它意外行为还是可以发生的。它和select 加锁定提示NOLOCK效果相当。

测试实例:

查询一:

SELECT * FROM TA WHERE TCID = 1

BEGIN TRAN

UPDATE TA

SET TCNAME = 'TA'

WHERE TCID = 1

--COMMIT TRAN --Don't commit

SELECT * FROM TA WHERE TCID = 1

SELECT @@SPID

/*

tcid Tcname

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

1 AA

(1 行受影响)

(1 行受影响)

tcid Tcname

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

1 TA

(1 行受影响)

SPID

------

54

(1 行受影响)

*/

查询二:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

SELECT * FROM TA WHERE TCID = 1

/*

tcid Tcname

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

1 TA

(1 行受影响)

*/

--显然未提交读模式我们读到SPID=54未提交的数据。

查询三:

SELECT * FROM TA WHERE TCID = 1

--查询一直进行中…… 无结果

--因为缺省下已提交读级别,所以修改数据设置了排它锁定必须等到SPID=54的事务结束

查询四:

--查看当前的锁定信息

exec sp_us_lockinfo

/*

SQL2005中的事务与锁定(一到九整合版)_第2张图片

*/


这个时候如果我们回头到查询一里执行commit tran ,你会发现查询三会得到结果,并且是查询一修改后的结果,如果你改用rollback ,那么结果就是原来的值不变,这个你们自己再测试。

1、 已提交读(Read Committed)

概念回顾:已提交读是SQL SERVER的缺省隔离级别,悲观模型下是用锁定,乐观模型下使用行版本控制器。这个设置可以通过SET READ_CIMMITTED_SNAPSHOT来修改。在悲观模型下对于读取来说设置共享锁定仅阻止排它锁定,并在数据读取结束自动释放,其它进程方可进行修改操作。也就是说读不会阻止其它进程设置共享及更新锁定,仅阻止排它锁定。在悲观模型下对于修改数据来说设置排锁定阻止所有锁定请示,必须等到排它锁定释放。这个级别的隔离解决了脏读的意外行为。

A、 READ_COMMITTED_SNAPSHOT为OFF的情况(缺省)

I、读数据测试

查询一:

BEGIN TRAN

--用锁定提示模拟共享锁定,并强制共享锁定持续到事务结束

SELECT * FROM TA with(holdlock) WHERE TCID = 1

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

tcid Tcname

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

1 CA

(1 行受影响)

------

54

(1 行受影响)

*/

查询二:悲观模型下已提交读级别

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

UPDATE TA

SET TCNAME = 'TA'

WHERE TCID = 1

--查询一直没有结果,显然我们验证了共享锁定阻止了排它锁定。

查询三:

exec sp_us_lockinfo

--结果大家自己运行看结果。

II、修改数据测试

查询一:

SELECT * FROM TA WHERE TCID = 1

BEGIN TRAN

UPDATE TA

SET TCNAME = 'READ COMMITTED LOCK'

WHERE TCID = 1

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

tcid Tcname

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

1 TA

(1 行受影响)

------

54

(1 行受影响)

*/

查询二:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

SELECT * FROM TA WHERE TCID = 1

/*

--查询一直进行中……被锁定无结果

--修改数据设置了排它锁定必须等到SPID=54的事务结束

*/

查询三:

exec sp_us_lockinfo

/*

SQL2005中的事务与锁定(一到九整合版)_第3张图片

*/

A、 READ_COMMITTED_SNAPSHOT为ON的情况

先修改当前当前库的READ_COMMITTED_SNAPSHOT为ON

ALTER DATABASE TESTCSDN

SET READ_COMMITTED_SNAPSHOT ON

GO

查询一:

SELECT * FROM TA WHERE TCID = 1

BEGIN TRAN

UPDATE TA

SET TCNAME = 'READ COMMITTED SNAP'

WHERE TCID = 1

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

TCID TCNAME

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

1 AA

(1 行受影响)

(1 行受影响)

------

56

(1 行受影响)

*/

查询二:因为启用行版本控制器来锁定数据,保证其它进程读取到虽然被排它锁定但在事务开始前已经提交的保证一致性的数据。

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

SELECT * FROM TA WHERE TCID = 1

/*

TCID TCNAME

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

1 AA

(1 行受影响)

*/

查询三:

exec sp_us_lockinfo

/*

SQL2005中的事务与锁定(一到九整合版)_第4张图片

*/

3、可重复读(Repeatable Read)

概念回顾:可重复读等级比已提交读多了一个约定:所有的共享锁定持续到事务结束,不是在读取完数据就释放。数据被设置了共享锁定后其它进程只能进行查询与增加不能更改,显然这个级别的隔离对程序有了更高的要求,因为可能因长时间的共享锁定影响系统的并发性能,增加死锁发生的机率。很显然是解决了不可重复读的意外行为。

数据测试:

查询一:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

BEGIN TRAN

SELECT * FROM TA WHERE TCID = 1 --可重复查询,并且读不到未提交的数据

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

tcid Tcname

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

1 READ COMMITTED LOCK

(1 行受影响)

------

52

(1 行受影响)

*/

查询二:

INSERT TA SELECT 9,'FF'

/*

(1 行受影响)

*/

SELECT * FROM TA-- WITH(UPDLOCK)

WHERE TCID = 1

/*

tcid Tcname

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

1 READ COMMITTED LOCK

(1 行受影响)

*/

UPDATE TA

SET TCNAME = 'READ COMMITTED REP'

WHERE TCID = 1

/*

--查询一直进行中……被锁定无结果

--修改数据设置了排它锁定必须等到SPID=52的事务结束

*/

查询三:

SQL2005中的事务与锁定(一到九整合版)_第5张图片

很显然查询三中的S,Is(共享及意向共享)锁定一直没消失,因为查询一的事务没有结束,在查询二里可以发现插入与读取(包括在查询一里再次select)是不影响的,并且读取的是未修改前的数据。

4、快照(SnapShot)

概念回顾:这是SQL SERVER2005的新功能,启用快照后所有的读操作不再受其它锁定影响,读取的数据是通过行版本管制器读取事务开始前逻辑确定并符合一致性的数据行版本。 这个级别隔离与已提交读的行版管理器的差别仅是行版本管理器里历史版本数据多久。

测试数据:

查询一:

ALTER DATABASE TESTCSDN

SET ALLOW_SNAPSHOT_ISOLATION ON

GO

SELECT * FROM TA WHERE TCID = 1 --OLD数据

BEGIN TRAN

UPDATE TA

SET TCNAME = 'SNAPSHOT'

WHERE TCID = 1

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

tcid Tcname

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

1 READ COMMITTED REP

(1 行受影响)

(1 行受影响)

------

52

(1 行受影响)

*/

查询二:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT

SELECT * FROM TA WHERE TCID = 1

/*

tcid Tcname

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

1 READ COMMITTED REP

(1 行受影响)

*/

查询三:

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第6张图片

5、可串行化:

概念回顾:这是交易里最健壮最严谨最高级别的隔离。通过索引键范围完全隔离其它交易的干扰,此隔离和select与锁定提示HOLDLOCK效果一样。这个级别基本解决所有的意外行为,显而易见的是并发性能下降或系统资源的损耗上升。

测试数据:

查询一:

DROP TABLE TB

GO

CREATE TABLE TB (ID INT Primary Key, COL VARCHAR(10))

GO

INSERT INTO TB SELECT 1,'A'

GO

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRAN

SELECT * FROM TB WHERE ID BETWEEN 1 AND 5--OLD数据

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

ID COL

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

1 A

(1 行受影响)

------

52

(1 行受影响)

*/

查询二:

SELECT * FROM TB WHERE ID = 1

/*

ID COL

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

1 A

(1 行受影响)

*/

INSERT TB SELECT 2,'EE'

/*

--查询一直进行中……被锁定无结果

--修改数据设置了排它锁定必须等到SPID=52的事务结束

*/

UPDATE TB

SET COL = 'SERIALIZABLE'

WHERE ID = 1

/*

--查询一直进行中……被锁定无结果

--修改数据设置了排它锁定必须等到SPID=52的事务结束

*/

查询三:

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第7张图片

可以明显的发现出现大量的索引键范围(RangeS-S……),确保在当前事务未结束之前另外的用户进程无法在索引键范围内插入数据,防此幼影意外行为的发生。可串行化后,除了数据能查询外,不可以修改、增加、删除索引键范围内的任意数据行,必须等到索引上的锁定释放。

结论:通过以的一些测试,我们知道通过隔离等级我们可以控制并发时意外行为,在实际操作的过程中我们可以用激活事务来控制锁的粒度、影响范围,以达到控制并发机制下数据的逻辑正确及数据一致性。最后我们发现通过锁定提示(LOCK HINTS)也可以改变表级锁定类型、锁定周期,达到和设置隔离等级类似的功能。

SQL2005中的事务与锁定(一到九整合版)_第8张图片

好,到目前为止我们把事务相关的东西介绍得差不多了,并且在提前介绍了部分的锁定,在下面的文章里我们重点对锁进行介绍。

在生产交易过程中多个用户同时访问数据是不可以避免的,通过不同的隔离等级对资源与数据进行各种类型的锁定保护并在适当时候释放保证交易的正确运行,使得交易完整并保证数据的一致性。不管是锁定还是行版本控制器都决定着商业逻辑的流畅、事务的完整、数据的一致。所以我们要根据实际情况进行部署,在并发性性能与资源管理成本之间找到平衡点,怎样才能找到这个平衡点呢,那我们就得对SQLSERVER如何管理资源与锁有一个了解,SQLSERVER不但管理锁定,还要管理锁定模式之间的兼容性或升级锁定及解决死锁问题。通过SQL SERVER强大的、细致的锁定机制,使得并发性能得到最大程度的发挥,但是使用尽可能少的系统资源也是我们最希望的。

SQLSERVER本身有两种锁定体系:一种是对共享数据的锁定,这种锁定就是我们大部时间讨论的锁定;一种是对内部数据结构及处理索引,这是一种称为闩锁的轻量级锁,比第一种锁定少耗资源,在sys.dm_tran_locks中是看不到这种锁的信息。我们在数据分页上放置物理记录或压缩、折分、转移分页数据时,这种锁就会发生了。我们在前面一直在说数据的逻辑一致性,那这种逻辑上的一致性就是通过锁定来控制的,而我们新提到的闩是保证物理的一致性(这种闩是系统内部使用所以我们不重点讨论了)。

并发访问数据时,SQL Server 2005使用下列机制确保事务完整并维护数据的一致性:

l 锁定
每个事务对所依赖的资源(如行、页或表)请求不同类型的锁。锁可以阻止其他事务以某种可能会导致事务请求锁出错的方式修改资源。当事务不再依赖锁定的资源时,它将释放锁。

l 行版本控制
当启用了基于行版本控制的隔离级别时,数据库引擎 将维护修改的每一行的版本。应用程序可以指定事务使用行版本查看事务或查询开始时存在的数据,而不是使用锁保护所有读取。通过使用行版本控制,读取操作阻止其他事务的可能性将大大降低。

锁定和行版本控制可以防止用户读取未提交的数据,还可以防止多个用户尝试同时更改同一数据。如果不进行锁定或行版本控制,对数据执行的查询可能会返回数据库中尚未提交的数据,从而产生意外的结果。

最后说一下锁的粒度与并发性能是矛盾的,但是对管理锁定的成本却是有利的,粒度越大并发性能下降,粒度越小管理锁定成本越大。用图示例一下:

SQL2005中的事务与锁定(一到九整合版)_第9张图片


六、锁定

1、锁粒度和可锁定资源

SQL Server2005 具有多粒度锁定,允许一个事务锁定不同类型的资源。为了尽量减少锁定的开销,数据库引擎自动将资源锁定在适合任务的级别。锁定在较小的粒度(例如行)可以提高并发度,但开销较高,因为如果锁定了许多行,则需要持有更多的锁。锁定在较大的粒度(例如表)会降低了并发度,因为锁定整个表限制了其他事务对表中任意部分的访问,但其开销较低,因为需要维护的锁较少。

SQL SERVER可以锁定表、分页、行级、索引键或范围。在这我提醒大家一下,对于聚集索引的表,因为数据行就是索引的叶级,所以锁定是键锁完成而不是行锁。

数据库引擎通常必须获取多粒度级别上的锁才能完整地保护资源。这组多粒度级别上的锁称为锁层次结构。例如,为了完整地保护对索引的读取,数据库引擎实例可能必须获取行上的共享锁以及页和表上的意向共享锁。

下表列出了数据库引擎可以锁定的资源:

查询一:

SELECT *

FROM MASTER..SPT_VALUES WHERE TYPE = 'LR'

/*

name number type low high status

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

LOCK RESOURCES 0 LR NULL NULL 0

NUL 1 LR NULL NULL 0

DB 2 LR NULL NULL 0

FIL 3 LR NULL NULL 0

TAB 5 LR NULL NULL 0

PAG 6 LR NULL NULL 0

KEY 7 LR NULL NULL 0

EXT 8 LR NULL NULL 0

RID 9 LR NULL NULL 0

APP 10 LR NULL NULL 0

MD 11 LR NULL NULL 0

HBT 12 LR NULL NULL 0

AU 13 LR NULL NULL 0

(13 行受影响)

*/

备注:

RID RID 锁定堆中行的行标识符

KEY KEY 序列化事务中的键范围行锁

PAG PAGE 数据或索引页面,8K为单位

EXT EXTENT 数据或索引页面,连续的8*page

HBT HOBT 堆或B树,保护索引或堆表页堆的锁

TAB TABLE 整个表,包括数据及索引

FIL FILE 数据库文件

APP APPLICATION 应用程序资源

MD METADATA 元数据

AU ALLOCATION_UNIT 分配单元

DB DATABASE 数据库

注:SPT_VALUES这个大家不陌生吧,好多人用它生成一个连续的ID号的啦,当时也有人问这个表的用途,现在发现它的作用了吧。下面我们还会使用到。

2、锁定模式

我们在前提面前到的共享锁定、更新锁定、排它锁定,这是为了配合前面的事务而提及的,那么SQL SERVER2005一共有多少锁定模式呢?我们通过一个简单的查询来列表:

查询:

SELECT *

FROM MASTER..SPT_VALUES WHERE [TYPE] = 'L'

/*

NAME NUMBER TYPE LOW HIGH STATUS

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

LOCK TYPES 0 L NULL NULL 0

NULL 1 L NULL NULL 0

SCH-S 2 L NULL NULL 0

SCH-M 3 L NULL NULL 0

S 4 L NULL NULL 0

U 5 L NULL NULL 0

X 6 L NULL NULL 0

IS 7 L NULL NULL 0

IU 8 L NULL NULL 0

IX 9 L NULL NULL 0

SIU 10 L NULL NULL 0

SIX 11 L NULL NULL 0

UIX 12 L NULL NULL 0

BU 13 L NULL NULL 0

RANGES-S 14 L NULL NULL 0

RANGES-U 15 L NULL NULL 0

RANGEIN-NULL 16 L NULL NULL 0

RANGEIN-S 17 L NULL NULL 0

RANGEIN-U 18 L NULL NULL 0

RANGEIN-X 19 L NULL NULL 0

RANGEX-S 20 L NULL NULL 0

RANGEX-U 21 L NULL NULL 0

RANGEX-X 22 L NULL NULL 0

(23 行受影响)

*/

我们可以看到一共有22种锁定模式 ,我简单的对上述[NAME]进行简单的枚举:

l S --- 共享锁定(Shared)

l U --- 更新锁定(Update)

l X --- 排它锁定(Exclusive)

l I --- 意向锁定(Intent)

l Sch --- 架构锁定(Schema)

l BU --- 大量更新(Bulk Update)

l RANGE --- 键范围(Key-Range)

l 其它是在上述锁定的变种组合,比如IS --- 意向共享锁定

其实对这些锁定模式没什么介绍,大家可以参考联机帮助:访问和更改数据库数据 -> 锁定和行版本控制 -> 数据库引擎中的锁定。其实这些锁定模式在前一篇基本都有出现,大家可以在看下面的定义再回头看看前一篇的相关内容。下面我就简单的说说:

共享锁(S 锁)

当我们查询(select)数据时SQL SERVER2005会尝试在数据上申请共享锁定,但是前提是在当前的数据上不存在与共享锁定互斥的锁定。资源上存在共享锁时,任何其他事务都不能修改数据但是可以读取数据。读取操作一完成,就立即释放资源上的共享锁,除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示(HOLDLOCK)保留共享锁。

更新锁(U 锁)

更新新是一种介于共享锁与排它锁之间的锁定,是一种中继锁定,像一个中间闸门,把从共享锁定转为排它锁的请求进行排队,有效的防止常见的死锁。在可重复读或可序列化事务中,一个事务读取数据 [获取资源(页或行)的共享锁(S 锁)],然后修改数据 [此操作要求锁转换为排他锁(X 锁)]。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排他锁(X 锁)。共享模式到排他锁的转换必须等待一段时间,因为一个事务的排他锁与其他事务的共享模式锁不兼容;发生锁等待。第二个事务试图获取排他锁(X 锁)以进行更新。由于两个事务都要转换为排他锁(X 锁),并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。而有了更新锁则可避免这种潜在的死锁问题,在查找到要更新的数据后SQL SERVER首先给数据设置更新锁定,因为共享锁定与更新锁定不互斥,在其它事务设置共享锁定时依然可以设置更新锁定,继而因更新锁定斥的,如果其它要修改数据的事务必须等待。如果事务修改资源,则更新锁转换为排他锁(X 锁)。

排他锁(X 锁)

排他锁可以防止并发事务对资源进行访问。使用排他锁(X 锁)时,任何其他事务都无法修改数据;仅在使用 NOLOCK 提示或未提交读隔离级别时才会进行读取操作。

数据修改语句(如 INSERT、UPDATE 和 DELETE)合并了修改和读取操作。语句在执行所需的修改操作之前首先执行读取操作以获取数据。因此,数据修改语句通常请求共享锁和排他锁。例如,UPDATE 语句可能根据与一个表的联接修改另一个表中的行。在此情况下,除了请求更新行上的排他锁之外,UPDATE 语句还将请求在联接表中读取的行上的共享锁。

排他锁定随事务结束而释放。

意向锁(I锁)

数据库引擎使用意向锁来保护共享锁(S 锁)或排他锁(X 锁)放置在锁层次结构的底层资源上。意向锁之所以命名为意向锁,是因为在较低级别锁前可获取它们,因此会通知意向将锁放置在较低级别上。

意向锁有两种用途:

l 防止其他事务以会使较低级别的锁无效的方式修改较高级别资源。

l 提高数据库引擎 在较高的粒度级别检测锁冲突的效率。

例如,在该表的页或行上请求共享锁(S 锁)之前,在表级请求共享意向锁。在表级设置意向锁可防止另一个事务随后在包含那一页的表上获取排他锁(X 锁)。意向锁可以提高性能,因为数据库引擎仅在表级检查意向锁来确定事务是否可以安全地获取该表上的锁。而不需要检查表中的每行或每页上的锁以确定事务是否可以锁定整个表。

意向锁包括意向共享 (IS)、意向排他 (IX)、意向排他共享 (SIX)、意向更新 (IU)、共享意向更新 (SIU ,S和 IU 锁的组合)、更新意向排他 (UIX,U 锁和 IX 锁的组合)。

在这儿的SIX,SIU,UIX我们可以理解成一种转换锁定,并不是由SQLSERVER直接申请的,是由一种模式向另一种模式转换时中间状态。比如说SIX表示一种正持有共享锁定的进程正在企图申请意向排它锁定,或是这样理解一个持有共享锁定的资源中有部分分页或行被另一个进程的排它锁定锁定了。其它同理可以理解。

为了更好的说明一点, 大家先看一个图:

SQL2005中的事务与锁定(一到九整合版)_第10张图片


这是我在TA表上加Where条件的一个更新动作,然后通过我以前写的一个工具:sp_us_lockinfo查看锁的信息,其实我的update只是影响一个行记录,但是我们发现有三个锁存在,只要当前事务不结束,其它事物对这个表申请不管是页面的锁定还是表级的锁定一定会与现在的表或页意向锁冲突,进而发生阻塞,而且我们在前面的隔离等级的实例中也有例子,你会发现它的请求状态是WAIT 而不是GRANT。

架构锁(架构修改锁 Sch-M 锁、架构稳定性锁Sch-S 锁)

执行表的数据定义语言 (DDL) 操作(例如添加列或删除表)时使用架构修改锁。在架构修改锁起作用的期间,会防止对表的并发访问。这意味着在释放架构修改锁(Sch-M 锁)之前,该锁之外的所有操作都将被阻止。

当编译查询时,使用架构稳定性锁。架构稳定性锁不阻塞任何事务锁,包括排他锁(X 锁)。因此在编译查询时,其他事务 [包括在表上有排他锁(X 锁)的事务] 都能继续运行。但不能在表上执行 DDL 操作。

大容量更新锁(BU 锁)

当将数据大容量复制到表,且指定了 TABLOCK 提示或者使用 sp_tableoption 设置了 table lock on bulk 表选项时,将使用大容量更新锁。大容量更新锁允许多个线程将数据并发地大容量加载到同一表,同时防止其他不进行大容量加载数据的进程访问该表。

键锁、键范围锁(Key-range锁)

在SQL SERVER2005有两种类型键锁:键锁及键范围锁。采用哪种类型的键锁取决于隔离级别。对于已提交读、可重复读、快照隔离时SQLSERVER锁定实际的索引键(如果是堆表除了实际非聚集索引上的键锁同时有实际行上的行锁),如果是可串行化隔离时就可以看到键范围锁。在早期的版本中我们实验可以看到SQLSERVER是通过分页锁定或表锁来实现的,也许键范围锁不是最完美的,但是我们应该看到它比分页或表锁定所锁定的范围要小得多,在保证不出现幻影的前提下键范围锁比以前版本采用锁定提供了更高的并发性能。

键范围锁放置在索引上,指定开始键值和结束键值。此锁将阻止任何要插入、更新或删除任何带有该范围内的键值的行的尝试,因为这些操作会首先获取索引上的锁。键范围锁包括按范围-行格式指定的范围组件和行组件,是一种组合锁模式(Range范围-索引项的锁模式)。比如:RangeI-N ,RangeI 表示插入范围,N(NULL) 表示空资源,它表示在索引中插入新键之前测试范围。

在SELECT * FROM MASTER..SPT_VALUES WHERE [TYPE] = 'L'查询结果的最后9条个就是键范围锁。这种锁定因持续时间比较短一般在sys.dm_tran_locks中很难见到。比如RangeI_N这个锁定,是在键范围内插入记录时获得的,在键范围内找到位置立即升级为X锁定,这个过程很短,我们在sys.dm_tran_locks中很难找到它的踪影,不过我们是可以模拟出来的,下面我们来模拟一下:

查询一:

DROP TABLE TB

GO

CREATE TABLE TB (ID INT primary key, COL VARCHAR(16))

GO

INSERT INTO TB SELECT 1,'A'

GO

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRAN

SELECT * FROM TB WHERE id BETWEEN 1 AND 5 --OLD数据

--COMMIT TRAN --Don't commit

SELECT @@SPID

/*

(1 行受影响)

ID COL

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

1 A

(1 行受影响)

------

52

(1 行受影响)

*/

查询二:

INSERT TB SELECT 2,'E'

查询三:

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第11张图片


在使用可序列化事务隔离级别时,对于 Transact-SQL 语句读取的记录集,键范围锁在索引获上取锁定阻止一切尝试在包含索引键值落入范围内增删改的数据行,可以隐式保护该记录集中包含的行范围。键范围锁可防止幻读。通过保护行之间键的范围,它还防止对事务访问的记录集进行幻像插入或删除。

例如我们在上面有例子的可串行化隔离级别下,选择索引键值在’1-5’的数据时,SQL SERVER 对落在1-5之间键值设置键范围锁定,避免包含在这个范围内的键值的插入及这个范围内键值的删除及更新。

最后强调一下键范围键产生的条件:

1、 务隔离级别必须设置为 SERIALIZABLE。

2、询处理器必须使用索引来实现范围筛选谓词。例如,SELEC中的 WHERE 子句。

3、锁兼容性矩阵

锁兼容性控制多个事务能否同时获取同一资源上的锁。如果资源已被另一事务锁定,则仅当请求锁的模式与现有锁的模式相兼容时,才会授予新的锁请求。如果请求锁的模式与现有锁的模式不兼容,则请求新锁的事务将等待释放现有锁或等待锁超时间隔过期。

SQL2005中的事务与锁定(一到九整合版)_第12张图片

4、深入可锁定资源及特殊锁定

可资源资源有哪些呢,我在前面已经用

SELECT * FROM MASTER..SPT_VALUES WHERE TYPE = 'LR'

进行了列表,一共有12种之多,其实我们从本篇的开始到现在一直在接触的行锁(RID),键锁(KEY),分页(PAG)、表(TAB)及对象(OBJECT)都是我们可锁定的资源,这几类我们已经接触到很多了,下面我就不常关注的几个进行一下说明。

EXT 这是数据或索引页面扩展。这一块如果以后有时间整理表的数据存储或索引分页的结构时可以细细说说扩展,现在我们可以简单的理解为:扩展是一个64K的分配单元,是由连续的8个8K分页组成。SQLSERVER在表或索引分配扩展时会分配8个连续的8K空间,每一个扩展的首页号是8的倍数,但是扩展间本身不一定连续哦,这个不连续就是碎片了。在扩展上也可以加锁定,其实这也好理解,在不同的表或索引需要新的扩展时,系统为了让同一扩展不被错误使用(比如两个表同时得到一个扩展,那比较恐怖哦)而进行共享或排它锁定。不过是系统自发进行的,我们一般看不到。这种物理上的一致性我们在前面提到过一种闩锁,嘿嘿有印象不?我们也可以把这个当作一种事实上的闩锁。

DB数据库(DATABAES)。其实只要我打开一个连接,如果你使用sp_us_lockinfo一定得到一条相应当前连接的DB类型的锁定。结果如图61

select @@spid

go

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第13张图片

那么在DATABASE这种类型下有几种锁呢,为别对应哪些操作呢?能不能模拟出来呢?好,下面我们来模拟一些吧,比如删除库操作:

1、 先打开一个managerment studio,我们先创建一个数据库,库名为dblock,建好后运行一下我们前面那个工具sp_us_lockinfo

2、 打开另一个managerment studio,右击dblock进行删除操作,在弹出的窗口点确认

3、 在第二步操作后迅速切换到第一个managerment staido并运行如下代码 ,并得到图63

select @@spid

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第14张图片

由上图很明显我们看到我们第一个打开的连接对Dblock有一个DB资源类型的共享锁定排斥了第二个删除操作的排它锁。再比如我们来把Dblock设置成只读,这个更好玩,多了几个锁定哦!先打开两个查询(一定要同时打开两个哦!!)

查询一:

alter database dblock SET READ_ONLY

/*

查询一直进行中……

*/

查询二:(结果图63)

select @@spid

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第15张图片

其它动作大家可以自己模拟 。

APP应用程序资源。有一个应用程序锁定模式与之对应。应用程序类型的资源锁定与我们前面讨论的所有锁定模式都不一样,前面所有的锁定全是SQLSERVER自己管理的,应用程序级锁定利用了SQLSERVER的检测阻塞及死锁的机制来锁定自己想要锁定的任何对象。比如我们现在想要达到这一种效果:一个表或过程同时只有一个进程能够运行。为了来模拟这种类型的锁定,先看如下语法:

sp_getapplock [ @Resource = ] 'resource_name', [ @LockMode = ] 'lock_mode' [ , [ @LockOwner = ] 'lock_owner' ] [ , [ @LockTimeout = ] 'value' ] [ , [ @DbPrincipal = ] 'database_principal' ][ ; ]

下面我们来模拟一下:

查询一:

exec sp_getapplock 'testapplock','exclusive','session';

go

select * from ta

--sp_releaseapplock 'testapplock','session';

查询二:

exec sp_getapplock 'testapplock','exclusive','session';

go

select * from ta

/*

查询一直进行中。。。。。。

*/

查询三:结果图64

--create database dblock

select @@spid

exec sp_us_lockinfo

SQL2005中的事务与锁定(一到九整合版)_第16张图片

METADATA 元数据。这种类型的可锁定资源我们其实前面已经有看过,我们在修改当前库为只读时,看如图65

SQL2005中的事务与锁定(一到九整合版)_第17张图片



好,到目前为止对可锁定的资源就说完了。嘿嘿,另外几个比如堆或B树类型锁定(HBT)不如以后整理索引时再旧事重提了。

5、锁的本质、生命周期及请求锁定的实体类型

首先我告诉大家SQLSERVER并不知道(或不关心)被它锁定的对象,有时即使我们能关联到对象却无法正确解析被锁定对象的结构,你相信吗?猛一听有点吓人吧,这么重要的任务交给它管理,它不知道被管理的东西是何方神圣?呵呵,下面稍稍说一点内部的结构。

锁是SQLSERVER的一种内存结构,并不是一种物理的数据结构,所以这种元数据追踪是无法被记录的。关闭或中止一个连接相应的部分锁定信息就会消失。这个结构书上都称锁块(Lock block),用这个锁块来跟踪数据、锁定及对锁相关的信息描述。显然这些锁块是要被不同进程所拥有的,SQL有另一个叫所有者块来管理这些,所有者块与锁块之间通过所有者的指针相连。

在锁块里有一个专门对锁定资源的描述结构叫资源块,资源块负责维护资源名称、锁块的指针、已经授权的所有者指针列表、转换中的所有者指针列表、等待的所有者列表。对资源块有一个类型描述,占用一个字节,这个类型描述我在前面已经通过SQL语句进行列表过:

查询一:

SELECT *

FROM MASTER..SPT_VALUES WHERE TYPE = 'LR'

对具体的资源描述有12个字节的内存结构来记录。我们在2000下使用过一个表syslockinfo,这些信息在rsc_bin中有体现,这个字段对资源进行描述,根据类型的不同,这12个字节有着不同的分工组合。在这里我还要提醒2000下的用户,rsc_bin在2005里意义不再等同于2000,但是仅是微小的差别。为了说明我上面的论点,所以对syslockinfo进行一下深入,证明SQLSERVER对被的对象是不关心的。我们先来实际看看这个表的数据,结果集如图66

SQL2005中的事务与锁定(一到九整合版)_第18张图片


通过这个图我们应该看到rsc_bin的最两个字节和2000下不一样了,这两个字节交换后就是资源块的类型,这一点大家要注意哦。

Query 1:

CREATE TABLE TB (ID INT primary key, COL VARCHAR(16))

GO

INSERT INTO TB SELECT 1,'A'

GO

SET TRANSACTION ISOLATION LEVEL repeatable read

BEGIN TRAN

SELECT * FROM TB WHERE id BETWEEN 1 AND 5

SQL2005中的事务与锁定(一到九整合版)_第19张图片


  对上图我们很容易理解类型为objec及page的rsc_bin意义。重点说一下KEY类型的共享锁定。这一条记录在syslockinfo中rsc_bin列,前6个字节是分区ID,我们可以通过sys.partitions查找到相应的分区。紧接着的6个字节(上图用红粗线标识的部分)来头不小

你可能感兴趣的:(数据结构,sql,sql,server,配置管理,Go)