如何监视锁的申请、持有和释放:
在着手分析、处理阻塞、死锁之前,首先要进行“监控”和“信息收集”
1、检查一个连接当前所持有的锁:
可以使用sp_lock来查看所有连接持有的锁的内容。
在2005以后引入的DMV,还能用过sys.dm_tran_locks来查看:
SELECT request_session_id, resource_type , resource_associated_entity_id , request_status , request_mode , resource_description FROM sys.dm_tran_locks
也可以组合其他DMV查看更详细的信息:
SELECT request_session_id, resource_type , resource_associated_entity_id , request_status , request_mode , resource_description , p.object_id , OBJECT_NAME(p.object_id) AS OBJECT_NAME , p.* FROM sys.dm_tran_locks a LEFT JOIN sys.partitions p ON a.resource_associated_entity_id = p.hobt_id WHERE resource_database_id= DB_ID('数据库名') ORDER BY request_session_id,resource_type , resource_associated_entity_id
2、监视语句执行过程中SQL Server对锁的申请和释放行为:
由于有很多锁是在语句运行过程中申请和释放的。运行之后锁会消失,所以使用上面方式很难查询。此时使用SQL Server Profiler 来跟踪是比较好的方式:
打开SQL Server Profiler→定义一个跟踪(trace),选取以下的Events(事件):Lock:Accquired,Lock:Released
由于实际过程中申请锁的情况可能会非常复杂,所以建议只在测试环境针对特定语句进行。
一般来说,使用DMV和sp_lock基本上能解决大部分问题。
锁的数量和数据库调优的关系:
一个常见的SELECT动作要申请的锁:
(1) 在连接A中,将事务隔离级别设成【可重复读】(repeatable read)
(2) 在运行查询前开启一个事务
(3) 运行查询语句,但不提交
(4) 在第二个连接里,查询sys.dm_tran_locks中分析查询结束以后连接A还持有的锁。
一个常见的UPDATE动作要申请的锁:
对于一个UPDATE操作,可以理解为先查询再修改。查询的过程先要添加S锁,找到数据后再添加U锁。最后才把U锁升级到X锁。
如果update操作借助了哪个索引,就会在这个索引的键值上有U锁。没有用到的索引不加锁。真正修改的地方会有X锁。对于查询涉及的页面。SQL SERVER加了IU锁。修改发生的页面,加了IX锁。
总结:
(1) 对于每个使用到的索引,SQL Server会对上面的键值加U锁。
(2) SQL Server只对要修改的记录或键加X锁。
(3) 使用到要修改的列的索引越多,锁的数目也会越多。
(4) 扫描的页面越多,意向锁就越多。扫描过程中,所有扫描到的记录也会加锁。哪怕没有修改。
对此,要在UPDATE过程中降低阻塞的几率,可以做以下方面:
(1) 尽量修改少的记录集。
(2) 减少无谓的索引。
(3) 严格避免表扫描的发生。如果只需要修改表的一小部分,要尽量使用Index Seek,避免全表扫描这种执行计划。
一个常见的DELETE动作要申请的锁:
Delete的时候也和update一样,需要先找出要更改的数据,然后再进行操作。
(1) DELETE 的过程先找到符合条件的记录,然后做删除。可以理解为先是一个SELECT ,然后一个DELETE。所以,如果有合适的索引,第一步申请的锁会比较少。
(2) DELETE 不但把数据行本身删除,还要删除所有相关的索引键,所以一张表上的索引数目越多,锁的数目就会越多,越容易发生阻塞。
为了防止阻塞,我们既不能绝对地不建索引,也不能随随便便建很多索引。对于没有用的索引,去掉比较好。
一个常见的INSERT动作要申请的锁:
SQL Server会对新插入的数据本身申请一个X锁。在发生变化的页面申请一个IX锁。由于是新插入的数据,被引用的概率相对小一些,所以发生阻塞的几率也很小。
小结:
数据库开发者和DBA想要影响SQL Server锁的申请和释放行为,以缓解阻塞或死锁的问题,需要考虑以下因素:
1、 事务隔离级别的选定:级别越高,隔离度越高,并发也越低。越高的级别SQL Server会不可避免地申请更多的锁。设计应用时,要和用户谈好,尽量选择默认的隔离级别(READ COMMITTED)。
2、 事务的长短和复杂度:决定了事务在SQL Server内部会持续多长时间,也决定了同时在多少张表和索引上申请和持有锁。避免在一个事务里面做很多事情。
3、 从应用整体并发度考虑,但是事务一次处理的数据量不能过多:如果一个应用的并发要求比较高,就一定要严格控制单个事务处理的数据量。如果有什么事务操作需要访问或修改表内大量数据,最好调整到并发用户比较少的时候运行。
4、 针对语句在表格上设计合适的索引:如果没有合适的索引,在做SELECT /UPDATE/DELETE 的时候,申请多得多的锁。对这种情况,可以通过加索引提高并发性。但是,索引越多,申请的锁数目也会越多。对于设计人员,要确保有足够的索引,防止语句做全表扫描。但也要去掉对语句运行贡献不大的索引,不能随便往表格上加索引。
数据库引擎中基于行版本控制的隔离级别:
在默认设置下,一个读操作会和一个写操作相互阻塞。在未提交读,虽然不会,但是读操作可能读到脏数据。大部分用户是不能接受的。
从2005以后,引入了行版本控制机制。好处是程序并发性比较高但是用户读取数据时虽然不是脏数据但是可能是一个正在被修改马上就要过期的数据值,容易产生逻辑错误。
SQLServer 有两种行版本控制:
使用行版本的已提交隔离(READ_COMMITTED_SNAPSHOT)和直接使用SNAPSHOT事务隔离级别。
l READ_COMMITTED_SNAPSHOT数据库选项为ON时,READ_COMMITTED事务通过使用行版本控制提供语句级读取一致性。
l ALLOW_SNAPSHOT_ISOLATION数据库选项为ON时,SNAPSHOT事务通过使用行版本控制提供事务级读取一致性。
可以使用ALTER DATABASE XXX SET READ_COMMITTED_SNAPSHOT/ ON;来开启。
注意:行版本控制并不是消除阻塞和死锁的万灵药。必须考虑两个问题:
1、 最终用户是否接受行版本控制下的运行结果?
2、 SQL Server 是否能支持行版本控制带来的额外负荷?因为行版本放在tempdb,对SQL Server会造成额外的负载。