SQL Server 锁详解

锁是一种防止在某对象执行动做的一个进程与已在该对象上执行的其余进行相冲突的机制。也就是说,若是有其余人在操做某个对象,那么你旧不能在该对象上进行操做。你可否执行操做取决于其余用户正在进行的操做。数据库

经过锁能够防止的问题

  锁能够解决如下4种主要问题:安全

  • 脏读
  • 非重复性读取
  • 幻读
  • 丢失更新

  一、脏读服务器

  若是一个事务读取的记录是另外一个未完成事务的一部分,那么这时就发生了脏读。若是第一个事务正常完成,那么就有什么问题。可是,若是前一个事务回滚了呢,那将从数据库从未发生的事务中获取了信息。并发

  二、非重复性读取性能

  很容易将非重复性读取和脏读混淆。若是一个事务中两次读取记录,而另外一个事务在这期间改变了数据,就会发生非重复性读取。
例如,一个银行帐户的余额是不容许小于0的。若是一个事务读取了某帐户的余额为125元,此时另外一事务也读取了125元,若是两个事务都扣费100元,那么这时数据库的余额就变成了-75元。优化

  有两种方式能够防止这个问题:code

  • 建立CHECK约束并监控547错误
  • 将隔离级别设置为REPEATABLEREAD或SERIALIZABLE

  CHECK约束看上去至关直观。要知道的是,这是一种被动的而非主动的方法。然而,在不少状况下可能须要使用非重复性读取,因此这在不少状况下是首选。对象

  三、幻读blog

  幻读发生的几率很是小,只有在很是偶然的状况下才会发生。递归

  好比,你想将一张工资表里全部低于100的人的工资,提升到100元。你可能会执行如下SQL语句:

  UPDATE tb_Money SET Salary = 100
  WHERE Salary < 100

  这样的语句,一般状况下,没有问题。可是若是,你在UPDATE的过程当中,有人刚好有INSERT了一条工资低于100的数据,由于这是一个全新的数据航,因此没有被锁定,并且它会被漏过Update。

  要解决这个问题,惟一的方法是设定事务隔离级别为SERIALIZABLE,在这种状况下,任何对表的更新都不能放入WHERE子句中,不然他们将被锁在外面。

  四、丢失更新

  丢失更新发生在一个更新成功写入数据库后,而又意外地被另外一个事务重写时。这是怎么发生的呢?若是有两个事务读取整个记录,而后其中一个向记录写入了更新信息,而另外一个事务也向该记录写入更新信息,这是就会出现丢失更新。

  有个例子写得很好,这里照敲下来吧。假如你是公司的一位信用分析员,你接到客户X打开的电话,说他已达到它的信用额度上限,想申请增长额度,因此你查看了这位客户的信息,你发现他的信用额度是5000,而且看他每次都能按时付款。

  当你在查看的时候,信用部门的另外一位员工也读取了客户X的记录,并输入信息改变了客户的地址。它读取的记录也显示信用额度为5000。

  这时你决定把客户X的信用额度提升到10000,而且按下了Enter键,数据库如今显示客户X的信用额度为10000。

  Sally如今也更新了客户X的地址,可是她仍然使用和您同样的编辑屏幕,也就是说,她更新了整个记录。还记得她屏幕上显示的信用额度吗?是5000.数据库如今又一次显示客户X的信用额度为5000。你的更新丢失了。

  解决这个问题的方法取决于你读取某数据和要更新数据的这段时间内,代码以何种方式识别出另外一链接已经更新了该记录。这个识别的方式取决于你所使用的访问方法。

能够锁定的资源

对于SQL Server来讲,有6种可锁定的资源,并且它们造成了一个层次结构。锁的层次越高,它的粒度就越粗。按粒度由粗到细排列,这些资源包括:

  • 数据库:锁定整个数据库。这一般发生在整个数据库模式改变的时候。
  • 表:锁定整个表。这包含了与该表相关联的全部数据相关的对象,包括实际的数据行(每一行)以及与该表相关联的全部索引中的键。
  • 区段:锁定整个区段。由于一个区段由8页组成,因此区段锁定是指锁定控制了区段、控制了该区段内8个数据或索引页以及这8页中的全部数据航。
  • 页:锁定该页中的全部数据或索引键。
  • 键:在索引中的特定键或一系间上有锁。相同索引页中的其余键不受影响。
  • 行或行标识符:虽然从技术上将,锁是放在行标识符上的,可是本质上,它锁定了整个数据行。

锁升级和锁对性能的影响

  升级是指可以认识到维持一个较细的粒度(例如,行锁而不是页锁),只在被锁定的项数较少时有意义。而随着愈来愈多的项目被锁定,维护这些锁的系统开销实际上会影响性能。这会致使所持续更长的时间。

  当维持锁的数量达到必定限度时,则锁升级为下一个更高的层次,而且将不须要再如此紧密地管理低层次的锁(释放资源,并且有助于提高速度)。

  注意,升级是基于锁的数量,而不是用户的数量。这里的重点是,能够经过执行大量的更新来单独地锁定表-行锁能够升级为页锁,页锁能够升级为表锁。这意味着可能将全部其余用户锁在该表以外。若是查询使用了多个表,则它极可能将每一个人锁在这些表以外。

锁定模式

  除了须要考虑锁定的资源层次之外,还要考虑查询将要得到的锁定模式,就像须要对不一样的资源进行锁定同样,也有不一样的锁定模式。一些模式是互相排斥的。一些模式什么都不作,只修改其余的模式。模式是否能够一块儿使用取决于他们是不是兼容的。

  一、共享锁

  这是最基本的一种锁。共享锁用于只须要读取数据的时候,也就是说,共享锁锁定时,不会进行改变内容的操做,其余用户容许读取。

  共享锁能和其余共享锁兼容。虽然共享锁不介意其余锁的存在,可是有些锁并不能和共享锁共存。

  共享锁告诉其余锁,某用户已经在那边了,它们并不提供不少的功能,可是不能忽略它们。然而,共享锁能作的是防止用户执行脏读。

  二、排它锁

  排它锁顾名思义,排它锁不与其余任何锁兼容。若是有任何其余其余锁存在,则不能使用排他锁,并且当排他锁仍然起做用时,他们不容许在资源之上建立任何形式的新锁。这能够防止两我的同时更新、删除或执行任何操做。

  三、更新锁

  更新锁是共享锁和排他锁的混合。更新锁是一种特殊的占位符。为了能执行UPDATE,须要验证WHERE子句来指出想要更新的具体的数据行。这意味着只须要一个共享锁,直到真正地进行物理更新。在物理更新期间,须要一个排他锁。

  • 第一个阶段指出了知足WHERE子句条件的内容,这是更新查询的一部分,该查询有一个更新锁。
  • 第二个阶段是若是决定执行更新,那么锁将升级为排他锁。不然,将把锁转换为共享锁。

  这样作的好处是它防止了死锁。死锁自己不是一种锁定类型,而是一种已经造成矛盾的情况,两个锁在互相等待,多个锁造成一个环在等待前面的事务清除资源。

  若是没有更新锁,死锁会一直出现。两个更新查询会在共享模式下运行。Query A完成了它的查询工做并准备进行物理更新。它想升级为排他锁,可是不能够这么作,由于Query B正在完成查询。除非Query B须要进行物理更新,不然它会完成查询。为了作到这点,Query B必须升级为排他锁,可是不能这么作,由于Query A正在等待。这样就形成了僵局。

  而更新锁阻止创建其余的更新锁。第二个事务只要尝试取得一个更新锁,它们就会进入等待状态,直到超时为止-将不会授予这个锁。若是第一个锁在锁超时以前清除的话,则锁定会授予给新的请求者,而且这个处理会继续下去。若是不清楚,则会发生错误。

  更新锁只与共享锁以及意向共享锁相兼容。

  四、意向锁

  意向锁是什么意思呢?就是说,加入你锁定了某一行,那么同时也加了表的意向锁(不容许其余人经过表锁来妨碍你)。

  意向锁是真正的占位符,它用来处理对象层次问题的。假设一下以下状况:已对某一行创建了锁,可是有人想在页上或区段上创建所,或者是修改表。你确定不想让另外一个事务经过达到更高的层次来妨碍你。
  若是没有意向锁,那么较高层次的对象将不会知道在较低层次上有锁。意向锁可改进性能,由于SQL Server只须要在表层次上检查意向锁(而不须要检查表上的每一个行锁或者页锁),以此来决定事务是否能够安全地锁定整个表。

  意向锁分为如下3种不一样的类型:

  • 意向共享锁:该意向锁指已经或者将要在层次结构的一些较低点处创建共享锁。
  • 意向排他锁:它与意向共享锁同样,可是将会在低层项上设置排他锁。
  • 共享意向排他锁:它指已经或将会在对象层次结构下面创建共享锁,但目的是为了修改数据,因此它会在某个时刻成为意向排它锁。

  五、模式锁

  模式锁分为如下两种。

  •   模式修改锁:对对象进行模式改变。在Sch-M锁期间,不能对对象进行查询或其余CREATE、ALTER或DROP语句的操做。
  •   模式稳定锁锁定:它和共享锁很类似;这个锁的惟一目的是方式模式修改锁,由于在该对象上已有其余查询(或CREATE、ALTER、DROP语句)的锁。它与其余全部的锁定相兼容。

  六、批量更新锁

  批量更新锁(BU)只是一种略有不一样的表锁定变体形式。批量更新锁容许并行加载数据。也就是说,对于其余任何普通操做(T-SQL)都会将表锁定,但能够同时执行多个BULK INSERT或bcp操做。

锁的兼容性

  锁的资源锁定模式的兼容性表格,现有锁以列显示,要兼容的锁以行显示。

锁的类型 意向共享锁(IS) 共享锁(S) 更新锁(U) 意向排他锁(IX) 共享意向排它锁(SIX) 排他锁(X)
意向共享锁(IS)
共享锁(S)
更新锁(U)
意向排他锁(IX)
共享意向排它锁(SIX)
排他锁(X)

  另外:

  •   Sch-S与出Sch-M之外的全部锁定模式相兼容。
  •   Sch-M和全部的锁定模式不兼容。
  •   BU只与模式稳定性锁以及其余的批量更新锁相兼容。

  有时想要在查询中或在整个事务中对锁定有更多的控制。能够经过使用优化器提示(optimizer hints)来实现这一点。

  优化器提示明确告诉SQL Server将一个锁升级为特有的层次。这些提示信息包含在将要影响的表的名称以后。

  优化器提示是一个高级主题,有经验的SQL Server开发人员会常用它,而且他们至关重视它。

  使用Management Studio肯定锁

  查看锁的最好方式是使用Management Studio。经过使用Activity Monitor,Management Studio会以两种方式显示锁-经过processId或object。

  为了使用Management Studio显示锁,只要导航到<Server>的Activity Monitor节点,其中的<Server>是监控其活动的服务器的顶级节点。

  展开感兴趣的节点(Overview部分默认展开),能够经过滚动条查看大量度量值-包括当前系统中有效的锁。

  

  显示界面以下:
  

设置隔离级别

  事务和锁之间的联系是很紧密的。默认状况下,一旦建立了任何与数据修改相关的锁,该锁定就会在整个事务期间存在。若是有一个大型事务,就意味着将在很长一段时间内阻止其余进程访问锁定的对象。这明显是有问题的。

事务有5种隔离级别:

  • READ COMMITTED
  • READ UNCOMMITTED
  • REPEATABLE READ
  • SERIALIZABLE
  • SNAPSHOT

  在这些隔离级别之间进行切换的语法也至关直观:

  SET TRANSACTION ISOLATION LEVEL < READ COMMITTED | READ UNCOMMITTED | REPEATABLE READ | SERIALIZABLE | SNAPSHOT >

  对隔离级别的修改只会影响到当前的链接-因此没必要担忧会影响到其余的用户。其余用户也影响不了你。

  一、READ COMMITTED

  默认状况就是这个,经过READ COMMITTED,任何建立的共享锁将在建立它们的语句完成后自动释放。也就是说,若是启动了一个事务,运行了一些语句,而后运行SELECT语句,再运行一些其余的语句,那么当SELECT语句完成的时候,与SELECT语句相关联的锁就会释放 - SQL Server并不会等到事务结束。

  动做查询(UPDATE、DELETE、INSERT)有点不一样。若是事务执行了修改数据的查询,则这些锁将会在事务期间保持有效。

  经过设置READ COMMITTED这一默认隔离级别,能够肯定有足够的数据完整性来防止脏读。然而,仍会发生非重复性读取和幻读。

  二、READ UNCOMMITTED

  READ UNCOMMITTED是全部隔离级别中最危险的,可是它在速度方面有最好的性能。
  设置隔离级别为READ UNCOMMITTED将告诉SQL Server不要设置任何锁,也不要事先任何锁。
  锁既是你的保护者,同时也是你的敌人。锁能够防止数据完整性问题,可是锁也常常妨碍或阻止你访问须要的数据。因为此锁存在脏读的危险,所以此锁只能应用于并不是十分精确的环境中。

  三、REPEATABLE READ

  REPEATABLE READ会稍微地将隔离级别升级,并提供一个额外的并发保护层,这不只能防止脏读,并且能防止非重复性读取。
防止非重复性读取是很大的优点,可是直到事务结束还保持共享锁会阻止用户访问对象,所以会影响效率。推荐使用其余的数据完整性选项,例如CHECK约束,而不是采用这个选择。
  与REPEATABLE READ隔离级别等价的优化器提示是REPEATABLEREAD(除了一个空格,二者并没有不一样)。

  四、SERIALIZABLE

  SERIALIZABLE是堡垒级的隔离级别。除了丢失更新之外,它防止全部形式的并发问题。甚至能防止幻读。

  若是设置隔离级别为SERIALIZABLE,就意味着对事物使用的表进行的任何UPDATE、DELETE、INSERT操做绝对不知足该事务中任何语句的WHERE子句的条件。从本质上说,若是用户想执行一些事务感兴趣的事情,那么必须等到该事务完成的时候。

  SERIALIZABLE隔离级别也能够经过查询中使用SERIALIZABLE或HOLDLOCK优化器提示模拟。再次申明,相似于READ UNCOMMITTED和NOLOCK,前者不须要每次都设置,然后者须要把隔离级别设置回来。

  五、SNAPSHOT

  SNAPSHOT是最新的一种隔离级别,很是想READ COMMITTED和READ UNCOMMITTED的组合。要注意的是,SNAPSHOT默认是不可用的-只有为数据库打开了ALLOW_SNAPSHOT_ISOLATION特殊选项时,SNAPSHOT才可用。
  和READ UNCOMMITED同样,SNAPSHOT并不建立任何锁,也不实现人和所。二者的主要区别是它们识别数据库中不一样时段发生的更改。数据库中的更改,无论什么时候或是否提交,都会被运行READ UNCOMMITTED隔离级别的查询看到。而使用SNAPSHOT,只能看到在SNAPSHOT事务开始以前提交的更改。从SNAPSHOT事务一开始执行,全部查看到的数据就和在时间开始时提交的同样。

处理死锁

  死锁的错误号是1205。

  若是一个锁因为另外一个锁占有资源而不能完成应该作的清除资源工做,就会致使死锁;反之亦然。当发生死锁时,须要其中的一方赢得这场斗争,因此SQL Server选择一个死锁牺牲者,对死锁牺牲者的事务进行回滚,而且经过1205错误来通知发生了死锁。另一个事务将继续正常地运行。

  一、判断死锁的方式

  每隔5秒钟,SQL Server就会检查全部当前的事务,了解他们在等待什么还未被授予的锁。而后再一次从新检查全部打开的锁请求的状态,若是先前请求中有一个尚未被授予,则它会递归地检查全部打开的事务,寻找锁定请求的循环链。若是SQL Server找到这样的村换连,则将会选择一个或更多的死锁牺牲者。

  二、选择死锁牺牲者的方式

  默认状况下,基于相关事务的"代价",选择死锁牺牲者。SQL Server将会选择回滚代价最低的事务。在必定程度上,可使用SQL Server中的DEADLOCK_PRIORITY SET选项来重写它。

  三、避免死锁

避免死锁的经常使用规则

    按相同的顺序使用对象
  • 使事务尽量简短而且在一个批处理中。
  • 尽量使用最低的事务隔离级别。
  • 在同一事务中不容许无限度的中断。
  • 在控制环境中,使用绑定链接。

  一、按相同的顺序使用对象

  例若有两个表:Suppliers和Products。假设有两个进程将使用这两个表。进程1接受库存输入,用手头新的产品总量更新Products表,接下来用已经购买的产品总量来更新Suppliers表。进程2记录销售数据,它在Supperlier表中更新销售产品的总量,而后在Product中减小库存数量。

  若是同时运行这两个进程,那么就有可能碰到麻烦。进程1试图获取Product表上的一个排他锁。进程2将在Suppliers表上获取一个排他锁。而后进程1将试图获取Suppliers表上的一个锁,可是进程1必须等到进程2清除了现有的锁。同时进程2也在等待进程1清除现有锁。

  上面的例子是,两个进程用相反的顺序,锁住两个表,这样就很容易发生死锁。

  若是咱们将进程2改为首先在Products减小库存数量,接着在Suppliers表中更新销售产品的总数量。两个进程以相同的顺序访问两张表,这样就可以减小死锁的发生。

  二、使事务尽量简短

  保持事务的简短将不会让你付出任何的代价。在事务中放入想要的内容,把不须要的内容拿出来,就这么简单。它的原理并不复杂-事务打开的时间越长,它触及的内容就越多,那么其余一些进程想要获得正在使用的一个或者多个对象的可能性就越大。若是要保持事务简短,那么就将最小化可能引发死锁的对象的数量,还将减小锁定对象的时间。原理就如此简单。

  三、尽量使用最低的事务隔离级别

  使用较低的隔离级别和较高的隔离级别相比,共享锁持续的时间更短,所以会减小锁的竞争。

  四、不要采用容许无限中断的事务

当开始执行某种开放式进程时间,不要建立将一直占有资源的锁。一般,指的是用户交互,但它也多是容许无限等待的任何进程。

相关文章
相关标签/搜索