分析SIX锁和锁分区导致的死锁

发布时间:2020-08-05 03:21:51 作者:joe321
来源:网络 阅读:1342

什么是SIX锁?

官方文档锁模式中说到:

意向排他共享 (SIX):保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁。 顶级资源允许使用并发 IS 锁。 例如,获取表上的 SIX 锁也将获取正在修改的页上的意向排他锁以及修改的行上的排他锁。 虽然每个资源在一段时间内只能有一个 SIX 锁,以防止其他事务对资源进行更新,但是其他事务可以通过获取表级的 IS 锁来读取层次结构中的低层资源。

官方说明比较晦涩难懂,我尝试用一种易懂的方式说明SIX是什么。

关于锁有几个概念:粒度、层次结构和锁之间的兼容性。锁是用来锁定资源,而资源是包括很多种的,而这些不同的资源代表着不同的粒度。不同的资源间存在着层次结构,如表、分区、页、行、键等。锁的类型用很多种,粗略的分类包括共享锁(S)、更新锁(U)、排他锁(X)和架构锁(Sch)等,而不同类型的锁,有些是互斥的,有些是兼容的。如共享锁与其它类型的锁相互兼容,排他锁与其它的锁类型互斥。

SQL Server分配锁时,会沿着层次结构,从表级别开始分配锁,然后到最下层的行和键。在分配锁时,上级的资源会被分配意向锁(I),用来表示这个资源的下级某个资源已经被锁定了。意向锁也可以分为IS,IX,IU等类型。例如,更新表中某一行,需要在在行上分配X锁,而在行所属的数据页中分配意向锁IX,数据页所属的表上分配IX锁。

如果一个会话的事务当前持有了某个表或者数据页的S锁,而它接下来又要去修改表中的某一个行。这种情况下,事务需要获取行上的X锁和表或数据页上的IX锁,但是SQL Server只允许一个会话在一个资源上获取一个锁。也就是说没有办法在已经获得表或者页级别的S锁之后又分配IX给它。为了解决这个问题,于是就出现了两者的结合体:S+IX=SIX。 同理,如果先持有IX,再去获取S,也会得到SIX。

另外SQL Server中还有类似的锁类型UIX(U+IX),SIU(S+IU),机理也是一样的。这三种锁被称为转换锁。

 

什么是锁分区?

首先不要把锁分区(Lock Partitioning)和分区锁(Partition Lock)搞混了。

官方文档锁分区

对于大型计算机系统,在经常被引用的对象上放置的锁可能会变成性能瓶颈,因为获取和释放锁对内部锁资源造成了争用。锁分区通过将单个锁资源拆分为多个锁资源而提高了锁性能。此功能只适用于拥有 16 个或更多 CPU 的系统,它是自动启用的,而且无法禁用。只有对象锁可以分区

锁任务访问几个共享资源,其中两个通过锁分区进行优化:

获取已分区资源的锁时:

启动一个事务时,它将被分配给一个分区。对于此事务,可以分区的所有锁请求都使用分配给该事务的分区。按照此方法,不同事务对相同对象的锁资源的访问被分布到不同的分区中。

通过一个示例观察一下SIX和锁分区:

create  table t2 (    id int identity(1,1) ,     col1 int,     col2 int     )     
go     
insert into t2     values (floor(rand()*100),floor(rand()*100))     
go 20
set transaction isolation level serializable    
begin tran     
insert into t2     values (floor(rand()*100),floor(rand()*100))     
select id from t2     where @@ROWCOUNT>0 and id=SCOPE_IDENTITY()     
SELECT resource_type, request_mode, resource_description,resource_lock_partition     
FROM   sys.dm_tran_locks     
WHERE  resource_type <> 'database' and request_session_id=@@SPID     
rollback

分析SIX锁和锁分区导致的死锁

这个实例有24颗CPU,所以通过resource_lock_partition看到分区编号最到23了。因为SIX模式要获取所有锁分区,所以看到所有分区上都有SIX。

从图中可以看出同一个事务中,不同的锁资源可以使用不同的锁分区

 

实际案例分析

最近在做性能review时发现某些实例的Ring Buffer中记录了一些死锁,其中一个如下:

分析SIX锁和锁分区导致的死锁

会话113持有了对象上的IX,需要再申请SIX。说明它修改数据后要去查询数。

会话79持有了对象上的SIX,需要再申请SIX。这个就有点奇怪了,需要再仔细看看xml格式的死锁信息。

<deadlock> 
    <victim-list> 
        <victimProcess 
id="process8809b88"/> 
    </victim-list> 
    <process-list> 
        <process id="process8809b88" 
taskpriority="0" logused="6844" waitresource="OBJECT: 6:1541580530:10 " 
waittime="967" ownerId="4638862771" transactionname="user_transaction" 
lasttranstarted="2016-06-06T16:45:14.617" XDES="0x8001d050" lockMode="SIX" 
schedulerid="1" kpid="41740" status="suspended" spid="113" sbid="2" ecid="0" 
priority="0" trancount="1" lastbatchstarted="2016-06-06T16:45:14.627" 
lastbatchcompleted="2016-06-06T16:45:14.627" clientapp=".Net SqlClient Data 
Provider" hostname="xxxx" hostpid="12552" loginname="xxx" 
isolationlevel="serializable (4)" xactid="4638862771" currentdb="6" 
lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"> 
            <executionStack> 
                <frame procname="" 
line="3" stmtstart="220" 
sqlhandle="0x0200000072705a08155b23d8949f622375a2f263ba0d9099"></frame> 
            <frame procname="" line="1" 
sqlhandle="0x000000000000000000000000000000000000000000000000"></frame> 
    </executionStack> 
    <inputbuf> 
    (@0 bigint,@1 
varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) 
values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT 
&gt; 0 and [VerifyID] = scope_identity() 
</inputbuf> 
</process> 
<process id="process886c748" taskpriority="0" 
logused="7128" waitresource="OBJECT: 6:1541580530:0 " waittime="967" 
ownerId="4638862727" transactionname="user_transaction" 
lasttranstarted="2016-06-06T16:45:14.493" XDES="0xbe484e90" lockMode="SIX" 
schedulerid="11" kpid="35316" status="suspended" spid="79" sbid="2" ecid="0" 
priority="0" trancount="1" lastbatchstarted="2016-06-06T16:45:14.517" 
lastbatchcompleted="2016-06-06T16:45:14.517" clientapp=".Net SqlClient Data 
Provider" hostname="xxxxx" hostpid="29284" loginname="xxxxx" 
isolationlevel="serializable (4)" xactid="4638862727" currentdb="6" 
lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056"> 
<executionStack> 
    <frame procname="" line="3" 
stmtstart="220" 
sqlhandle="0x0200000072705a08155b23d8949f622375a2f263ba0d9099"></frame> 
<frame procname="" line="1" 
sqlhandle="0x000000000000000000000000000000000000000000000000"></frame> 
</executionStack> 
<inputbuf> 
(@0 bigint,@1 
varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) 
values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT 
&gt; 0 and [VerifyID] = scope_identity() 
</inputbuf> 
</process> 
</process-list> 
<resource-list> 
<objectlock lockPartition="10" objid="1541580530" 
subresource="FULL" dbid="6" objectname="" id="lock77e3c1b00" mode="IX" 
associatedObjectId="1541580530"> 
<owner-list> 
  <owner 
id="process886c748" mode="IX"/> 
</owner-list> 
<waiter-list> 
<waiter id="process8809b88" mode="SIX" 
requestType="wait"/> 
</waiter-list> 
</objectlock> 
<objectlock lockPartition="0" objid="1541580530" 
subresource="FULL" dbid="6" objectname="" id="lock628e080" mode="SIX" 
associatedObjectId="1541580530"> 
<owner-list> 
<owner 
id="process8809b88" mode="SIX"/> 
</owner-list> 
<waiter-list> 
<waiter id="process886c748" mode="SIX" 
requestType="wait"/> 
</waiter-list> 
</objectlock> 
</resource-list> 
</deadlock>

概括一下:

1.两者执行同样的语句。插入一条数据,然后把刚才插入的这条数据的自增ID取出来。堆表,无索引。

(@0 bigint,@1 varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT &gt; 0 and [VerifyID] = scope_identity()  
 

2. SPID 79持有锁分区10上的IX,正在等待分配锁分区0上的SIX.

    SPID 113持有锁分区0上的SIX,正等待待分配锁分区10上的SIX

3.会话的事务隔离级别都是可以序列化(isolationlevel="serializable (4)")。

基于以上,可以明白死锁是怎么发生的:

113在插入数据时持有某个锁分区的IX,假设这个锁分区为N,然后它要查询刚才插入的数据,所以转换为SIX。SIX是需要分配所有锁分区的,并且需要从第0锁分区开始。分配到10分区时,发现10分区被与SIX不兼容的IX锁给锁定了,陷入等待。

79插入数据时被分配了IX锁,这个锁分区为第10分区,然后查询数据时需要将IX转换为SIX。于是从第0锁分区开始分配SIX,但是第0分区已经被113的SIX锁定,并且SIX与SIX是不兼容的,于是也陷入等待。

 

如何解决

总结前面的死锁原因,问题就变成了:并发插入含有自增ID的堆表,并取出插入的自增ID,如何避免死锁?

这种死锁情况是非常非常罕见的。TF-1229可以禁用锁分区的功能。个人觉得高并发的应用,与其禁用锁分区来规避这种罕见情况,还不如设计好应用的重试机制。

有一点很奇怪,在上面的死锁中,事务隔离级别是可序列化,而数据库端是默认的已提交隔离级别。开发人员并没设置连接会话的事务隔离级别,而这个会话的隔离级别却改变了,这是什么原因呢?

我的分析是,程序中使用了Entity Framework,而EF在6.0之前的默认连接隔离级别是可序列化。开发人员直接拿来用,也没有注意到这种问题。

参考:

What is the default transaction isolation level in Entity Framework when I issue “SaveChanges()”?

Tips to avoid deadlocks in Entity Framework applications

推荐阅读:
  1. db2死锁和锁超时
  2. Mysql的锁(S锁和X锁的区别)

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

sql lock dead

上一篇:向未来而生:百度的技术进化

下一篇:企业实战LNMP高性能服务器_wordpress、discuz双网站部署

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》