分析SIX锁和锁分区导致的死锁
什么是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 的系统,它是自动启用的,而且无法禁用。只有对象锁可以分区。
锁任务访问几个共享资源,其中两个通过锁分区进行优化:
调节锁(Spinlock)。它控制对锁资源(例如行或表)的访问。
不进行锁分区,一个调节锁就得管理单个锁资源的所有锁请求。在具有大量活动的系统上,在锁请求等待释放调节锁时会出现资源争用的现象。在这种情况下,获取锁可能变成了一个瓶颈,并且可能会对性能造成负面影响。
为了减少对单个锁资源的争用,锁分区将单个锁资源拆分成多个锁资源,以便将负荷分布到多个调节锁上。
内存。它用于存储锁资源结构。
获取调节锁后,锁结构将存储在内存中,然后即可对其进行访问和可能的修改。将锁访问分布到多个资源中有助于消除在 CPU 之间传输内存块的需要,这有助于提高性能。
获取已分区资源的锁时:
只能获取单个分区的 NL、SCH-S、IS、IU 和 IX 锁模式。
对于以分区 ID 0 开始并且按照分区 ID 顺序排列的所有分区,必须获取非 NL、SCH-S、IS、IU 和 IX 模式的共享锁 (S)、排他锁 (X) 和其他锁。已分区资源的这些锁将比相同模式中未分区资源的锁占用更多的内存,因为每个分区都是一个有效的单独锁。内存的增加由分区数决定。Windows 性能监视器中 SQL Server 锁计数器将显示已分区和未分区锁所使用内存信息。
启动一个事务时,它将被分配给一个分区。对于此事务,可以分区的所有锁请求都使用分配给该事务的分区。按照此方法,不同事务对相同对象的锁资源的访问被分布到不同的分区中。
通过一个示例观察一下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
e.g.
这个实例有24颗CPU,所以通过resource_lock_partition看到分区编号最到23了。因为SIX模式要获取所有锁分区,所以看到所有分区上都有SIX。
从图中可以看出同一个事务中,不同的锁资源可以使用不同的锁分区。
实际案例分析
最近在做性能review时发现某些实例的Ring Buffer中记录了一些死锁,其中一个如下:
会话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 > 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 > 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>
dl
概括一下:
1.两者执行同样的语句。插入一条数据,然后把刚才插入的这条数据的自增ID取出来。堆表,无索引。
(@0 bigint,@1 varchar(20))insert [dbo].[CustomerVerify]([CustomerId], [VerifyRegisterCode]) values (@0, @1) select [VerifyID] from [dbo].[CustomerVerify] where @@ROWCOUNT > 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
分析SIX锁和锁分区导致的死锁的更多相关文章
- SQL Server锁分区特性引发死锁解析
锁分区技术使得SQL Server可以更好地应对并发情形,但也有可能带来负面影响,这里通过实例为大家介绍,分析由于锁分区造成的死锁情形. 前段时间园友@JentleWang在我的博客锁分区提升并发,以 ...
- java中锁与@Transactional同时使用导致锁失效的问题
示例代码 @Transactional public void update(int id) { boolean lock = redisLock.lock(id); if (!lock) { thr ...
- 可重入锁 & 自旋锁 & Java里的AtomicReference和CAS操作 & Linux mutex不可重入
之前还是写过蛮多的关于锁的文章的: http://www.cnblogs.com/charlesblc/p/5994162.html <[转载]Java中的锁机制 synchronized &a ...
- mysql的锁--行锁,表锁,乐观锁,悲观锁
一 引言--为什么mysql提供了锁 最近看到了mysql有行锁和表锁两个概念,越想越疑惑.为什么mysql要提供锁机制,而且这种机制不是一个摆设,还有很多人在用.在现代数据库里几乎有事务机制,aci ...
- JVM中锁优化,偏向锁、自旋锁、锁消除、锁膨胀
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt364 本文将简单介绍HotSpot虚拟机中用到的锁优化技术. 自旋锁 互斥同 ...
- MySQL 行锁 表锁机制
MySQL 表锁和行锁机制 行锁变表锁,是福还是坑?如果你不清楚MySQL加锁的原理,你会被它整的很惨!不知坑在何方?没事,我来给你们标记几个坑.遇到了可别乱踩.通过本章内容,带你学习MySQL的行锁 ...
- Python之路(第三十八篇) 并发编程:进程同步锁/互斥锁、信号量、事件、队列、生产者消费者模型
一.进程锁(同步锁/互斥锁) 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理. 例 ...
- Mysql 锁和锁算法
相关命令: show engines; 查看数据库支持的引擎 show variables like '%storage_engine%'; 查看数据库默认的引擎 select @@global ...
- synchronized底层实现原理&CAS操作&偏向锁、轻量级锁,重量级锁、自旋锁、自适应自旋锁、锁消除、锁粗化
进入时:monitorenter 每个对象有一个监视器锁(monitor).当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:1 ...
随机推荐
- Express ejs 3.* layout.ejs
新版本改成了 <%- include file.ejs %> 具体使用方法如下:1. views文件夹 下新建header.ejs,插入代码 <html><head> ...
- Codeforces Round #382 (Div. 2)B. Urbanization 贪心
B. Urbanization 题目链接 http://codeforces.com/contest/735/problem/B 题面 Local authorities have heard a l ...
- Oracle中Kill session的研究(转 出自eagle)
itpub link: http://www.itpub.net/235873.html 我们知道,在Oracle数据库中,可以通过kill session的方式来终止一个进程,其基本语法结构为: a ...
- Cubieboard2裸机开发之(三)C语言操作LED
前言 前面通过汇编语言点亮LED,代码虽然简单,但并不是很直观.这次使用熟悉的C语言来控制LED,但是需要注意的地方有两点,第一,要想使用C语言,首先需要在调用C语言代码之前设置好堆栈:第二,调用C语 ...
- Cocos2dx 把 glview 渲染到 Qt 控件上(Mac 环境)
本文原链接:http://www.cnblogs.com/zouzf/p/4423256.html 环境:Mac 10.9.2 Xcode5.1.1 Qt5.3 cocos2dx-2.2.4 ...
- 从今天起,记录CEF使用开发心得经验
已经使用CEF来呈现桌面程序界面大半年了,从来没有写过相关博文.发现网上的中文资料甚至英文已经无法满足我的开发需求,不少问题只得自己探索.在此先谢过网络上各位CEF使用开发博文的贡献者,没有你们我也难 ...
- cocos2dx的lua绑定
一.cocos2dx对tolua++绑定的修正 A.c对lua回调函数的引用 在使用cocos2dx编写游戏时,我们经常会设置一些回调函数(时钟.菜单选择等).如果采用脚本方式编写游戏的话,这些回调函 ...
- Android实战技巧:深入解析AsyncTask
AsyncTask的介绍及基本使用方法 关于AsyncTask的介绍和基本使用方法可以参考官方文档和Android实战技巧:多线程AsyncTask这里就不重复. AsyncTask引发的一个问题 上 ...
- There is no mode by that name loaded / mode not given 产生原因(个案)
使用jQM DateBox用于界面显示日期选择控件,结果发现之前是正常的.今天用就不行啦.提示There is no mode by that name loaded / mode not given ...
- IOS8Preview-xCode_6
IOS8Preview-xCode_6 what's new What's new in xCode 6 Xcode 6 introduces a radically new way to desig ...