在现在这个云计算,大数据,移动互联网大行其道的时代,各类NoSQL数据库MongoDb、redis、HBase等使用的愈来愈普遍,大有替代关系型数据库的趋势。可是关系型数据库真的已经落伍了吗?答案是否认的。非关系型数据库不支持ACID属性,不支持事务,没法适应复杂查询的缺点。关系型数据库凭借其强一致性的特色,注定了在相似银行转帐,订单支付等场景中,仍是惟一的选择。众所周知,SQLSERVER经过锁来提供ACID属性,处理并发访问,本文尝试经过对锁机制的一些学习,让咱们明白数据库查询超时,死锁等问题是如何产生的,及相关解决方法。 redis
在开始谈有关锁的内容前,咱们先看几个咱们在平常开发中常常碰到的场景: sql
以上两种状况或许是由于锁在其中"做怪"。下面咱们就开始从一些锁的基本概念谈起,了解SQLSERVER的锁机制,死锁成因以及死锁的预防。为了让你们能对锁有一个更深刻的了解,本文前半章谈的比较多的是一些基本概念。老鸟能够自动略过。 数据库
一.锁的一些基本概念 网络
为何须要锁?架构
在任何多用户的数据库中,必须有一套用于数据修改的一致的规则,当两个不一样的进程试图同时修改同一份数据时,数据库管理系统(DBMS)负责解决它们之间潜在的冲突。任何关系数据库必须支持事务的ACID属性,因此在开始了解锁以前,首先简单了解一下数据库事务和事务的ACID属性。 并发
理论上全部的事务之间应该是彻底隔离的。可是实际上,要实现彻底隔离的成本实在是过高(必须是序列化的隔离等级才能彻底隔离)。因此, SQL Server经过锁,就像十字路口的红绿灯那样,告诉全部并发的链接,在同一时刻上,那些资源能够读取,那些资源能够修改。当一个事务须要访问的资源加了其所不兼容的锁,SQL Server会阻塞当前的事务来达成所谓的隔离性。直到其所请求资源上的锁被释放。 性能
为此,SQL Server在隔离和并发之间选择了Read Commited做为数据库的默认隔离级别。 学习
多个用户同时对数据库的并发操做时会带来如下数据不一致的问题: 测试
脏读:一个事务读取到了另一个事务没有提交的数据。 大数据
A修改了数据,随后B又读出该数据,但A由于某些缘由取消了对数据的修改,数据恢复原值,此时B获得的数据就与数据库内的数据产生了不一致
幻读:同一事务中,用一样的操做读取两次,获得的记录数不相同。
A读取数据,随后B又插入了数据,此时A再读数据是发现先后两次获取的数据行集不一致
不可重复读:在同一事务中,两次读取同一数据,获得内容不一样。
A用户读取数据,随后B用户读出该数据并修改,此时A用户再读取数据时发现先后两次的值不一致
丢失更新:事务T1读取了数据,并执行了一些操做,而后更新数据。事务T2也作相同的事,则T1和T2更新数据时可能会覆盖对方的更新,从而引发错误。
A,B两个用户读同一数据并进行修改,其中一个用户的修改结果破坏了另外一个修改的结果,好比订票系统
并发控制的主要方法是经过锁,在一段时间内禁止用户作某些操做以免产生数据不一致
理解SQL SERVER中的隔离级别
为了不上述几种事务之间的影响,SQL Server经过设置不一样的隔离等级来进行不一样程度的避免。 SQL Server提供了5种选项来避免不一样级别的事务之间的影响。隔离等级由低到高分别为:
锁的模式
键范围锁可防止幻读。经过保护行之间的键范围,它还能够防止对事务访问的记录集进行幻插入。
键范围锁放置在索引上,指定开始键值和结束键值。此锁将阻止任何要插入、更新或删除任何带有该范围内的键值的行的尝试,由于这些操做会首 先获取索引上的锁。例如,可序列化事务可能发出了一个 SELECT 语句,以读取其键值介于 'AAA' 与 'CZZ' 之间的全部行。从 'AAA' 到 'CZZ' 范围内的键值上的键范围锁可阻止其余事务插入带有该范围内的键值(例如 'ADG'、'BBD' 或 'CAL')的行。
锁兼容性
锁的粒度
所谓所粒度,从本质上说就是,为了给事务提供彻底的隔离和序列化,做为查询或更新的一部分被锁定的数据的总量(的大小)。Lock Manager须要在资源的并发访问与维护大量低级别锁的管理开销之间取得平衡。好比,锁的粒度越小,可以同时访问同一张表的并发用户的数量就越大,不过维护这些锁的管理开销也越大。锁的粒度越大,管理锁须要的开销就越少,而并发性也下降了。下图说明了锁的大小与并发性之间的权衡取舍。
SQL Server支持的锁粒度能够分为为行、页、键、键范围、索引、表或数据库获取锁
锁升级
锁升级是将许多较细粒度的锁转换成数量更少的较粗粒度的锁的过程,这样能够减小系统开销,但却增长了并发争用的可能性。
当 SQL Server 数据库引擎获取低级别的锁时,它还将在包含更低级别对象的对象上放置意向锁:
除了对象上的意向锁之外,如下对象上还须要意向页锁:非汇集索引的叶级页、汇集索引的数据页、堆数据页。
锁升级的阈值:
TIPS:数据库引擎不会将行锁或键范围锁升级到页锁,而是将它们直接升级到表锁。一样,页锁始终升级到表锁。
如何查看锁
一、使用sys.dm_tran_locks这个DMV
二、使用Profiler来捕捉锁信息
2、死锁成因分析
什么是死锁
死锁的本质是一种僵持状态,是多个主体对于资源的争用而致使的。在两个或多个任务中,若是每一个任务锁定了其余任务试图锁定的资源,此时会形成这些任务永久阻塞,从而出现死锁。理解死锁首先须要对死锁所涉及的相关观念有一个理解。
在上图的例子中,每队汽车都占有一条道路,但都须要另一队汽车所占有的另外一条道路,所以互相阻塞,谁都没法前行,所以形成了死锁。
死锁产生的缘由及四个必要条件
产生死锁的缘由主要是:
(1) 由于系统资源不足。
(2) 进程运行推动的顺序不合适。
(3) 资源分配不当等。
若是系统资源充足,进程的资源请求都可以获得知足,死锁出现的可能性就很低,不然就会因争夺有限的资源而陷入死锁。其次,进程运行推动顺序与速度不一样,也可能产生死锁。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已得到的资源保持不放。
(3) 不剥夺条件:进程已得到的资源,在末使用完以前,不能强行剥夺。
(4) 循环等待条件:若干进程之间造成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不知足,就不会发生死锁。
死锁的两种类型
一、循环死锁:两个进程请求不一样资源上的锁,每个进程都须要对方持有的该资源上的锁,这时将发生循环死锁。以下图
二、转换死锁:两个或多个进程都在事务中持有同一资源上的共享锁,而且都想把它升级为独占锁,可是,谁也无法升级直到其余的进程释放共享锁,以下图
SQL Server中产生死锁的一些状况
一、由书签查找产生的死锁:这类死锁产生的缘由是书签查找和更新数据产生的僵持状态。简单来讲,就是因为Update语句对基本表产生X锁,而后须要对表上的索引也进行更新,而表上的索引正好被另外一个链接进行查找,加了S锁,此时又产生书签查找去基本表加了X锁的数据进行书签查找,此时造成死锁
书签查找:当查询优化器使用非汇集索引进行查找时,若是所选择的列或查询条件中的列只部分包含在使用的非汇集索引和汇集索引中时,就须要一个查找(lookup)来检索其余字段来知足请求。对一个有聚簇索引的表来讲是一个键查找(key lookup),对一个堆表来讲是一个RID查找(RID lookup),这种查找便是——书签查找(bookmark lookup)。简单的说就是当你使用的sql查询条件和select返回的列没有彻底包含在索引列中时就会发生书签查找。
解决方案:这种死锁能够经过Include列来减小书签查找,从而减小这种类型死锁发生的几率。
二、由外键产生的死锁: 这类死锁产生的缘由来自外键约束。当主表(也就是主键是从表外键的那个表)更新数据时,须要查看从表,以肯定从表的外键列知足外键约束。此时会在主表上加X锁,但这并不能阻止同一时间,另外一个SPID向从表添加被修改的主表主键,为了解决这个问题,SQL Server在进行这类更新时,使用Range锁,这种锁是当隔离等级为序列化时才有的,所以在这时虽然隔离等级多是默认的已提交读,可是行为倒是序列化。这极可能就会致使死锁。
解决方案:向外键列添加索引,使得Range锁加在索引上,而不是表自己。从而下降了死锁发生的几率。
三、因为推动顺序不当产生的死锁:在多个事务对资源的使用顺序不当,造成死锁环路而引起的。
解决方案:尽可能使资源的使用顺序一致。这也是死锁问题出现最多的一种状况。
如何查看死锁
3、死锁的预防与优化
预防死锁
预防死锁就是破坏四个必要条件中的某一个和几个,使其不能造成死锁。有以下几种办法:
1)破坏互斥条件
破坏互斥条件有比较严格的限制,在SQL Server中,若是业务逻辑上容许脏读,则能够经过将隔离等级改成未提交读或使用索引提示。这样使得读取不用加S锁,从而避免了和其它查询所加的与S锁不兼容的锁互斥,进而减小了死锁出现的几率。
2)破坏请求和等待条件
这点因为事务存在原子性,是不可破坏的,由于解决办法是尽可能的减小事务的长度,事务内执行的越快越好。这也能够减小死锁出现的几率。
3)破坏不剥夺条件
因为事务的原子性和一致性,不剥夺条件一样不可破坏。但咱们能够经过增长资源和减小资源占用两个角度来考虑。
增长资源:好比说经过创建非汇集索引,使得有了额外的资源,查询不少时候就再也不索要锁基本表,转而锁非汇集索引,若是索引可以"覆盖(Cover)"查询,那更好不过。所以索引Include列不只仅减小书签查找来提升性能,还能减小死锁。
减小资源占用:好比说查询时,能用select col1,col2这种方式,就不要用select * .这有可能带来没必要要的书签查找
最大限度减小死锁的方法
优化死锁的一些建议
(1)对于查询频繁的表尽可能使用汇集索引;
(2)设法避免一次性影响大量记录的SQL语句,特别是INSERT和UPDATE语句;
(3)设法让UPDATE和DELETE语句使用合适的索引;
(4)使用嵌套事务时,避免提交和回退冲突;
(5)对数据一致性要求不高的查询使用 WITH(NOLOCK)
(6)减少事务的体积,事务应最晚开启,最先关闭,全部不是必须使用事务的操做必须放在事务外。
(7)查询只返回你须要的列,不建议使用 SELECT * FROM 这种写法。