时间:2021-07-01 10:21:17 帮助过:22人阅读
在开始谈有关锁的内容前,我们先看几个我们在日常开发中经常碰到的场景:
以上两种情况或许是因为锁在其中"作怪"。下面我们就开始从一些锁的基本概念谈起,了解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:数据库引擎不会将行锁或键范围锁升级到页锁,而是将它们直接升级到表锁。同样,页锁始终升级到表锁。
如何查看锁
1、使用sys.dm_tran_locks这个DMV
2、使用Profiler来捕捉锁信息
二、死锁成因分析
什么是死锁
死锁的本质是一种僵持状态,是多个主体对于资源的争用而导致的。在两个或多个任务中,如果每个任务锁定了其他任务试图锁定的资源,此时会造成这些任务永久阻塞,从而出现死锁。理解死锁首先需要对死锁所涉及的相关观念有一个理解。
在上图的例子中,每队汽车都占有一条道路,但都需要另外一队汽车所占有的另一条道路,因此互相阻塞,谁都无法前行,因此造成了死锁。
死锁产生的原因及四个必要条件
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的两种类型
1、循环死锁:两个进程请求不同资源上的锁,每一个进程都需要对方持有的该资源上的锁,这时将发生循环死锁。如下图
2、转换死锁:两个或多个进程都在事务中持有同一资源上的共享锁,并且都想把它升级为独占锁,但是,谁也没法升级直到其他的进程释放共享锁,如下图
SQL Server中产生死锁的一些情况
1、由书签查找产生的死锁:这类死锁产生的原因是书签查找和更新数据产生的僵持状态。简单来说,就是由于Update语句对基本表产生X锁,然后需要对表上的索引也进行更新,而表上的索引正好被另一个连接进行查找,加了S锁,此时又产生书签查找去基本表加了X锁的数据进行书签查找,此时形成死锁
书签查找:当查询优化器使用非聚集索引进行查找时,如果所选择的列或查询条件中的列只部分包含在使用的非聚集索引和聚集索引中时,就需要一个查找(lookup)来检索其他字段来满足请求。对一个有聚簇索引的表来说是一个键查找(key lookup),对一个堆表来说是一个RID查找(RID lookup),这种查找即是——书签查找(bookmark lookup)。简单的说就是当你使用的sql查询条件和select返回的列没有完全包含在索引列中时就会发生书签查找。
解决方案:这种死锁可以通过Include列来减少书签查找,从而减少这种类型死锁发生的概率。
2、由外键产生的死锁: 这类死锁产生的原因来自外键约束。当主表(也就是主键是从表外键的那个表)更新数据时,需要查看从表,以确定从表的外键列满足外键约束。此时会在主表上加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 这种写法。
SqlServer锁机制与实践
标签:不可重复读 更新 有一个 语句 支持 ase 查找 而不是 并发控制