此文为转载文章,描述的很好,没有验证过。

最近遇到了一个看上去很奇怪,分析起来很有意思的死锁问题。这个死锁看上去难以理解。而分析过程中,又使用了很多分析SQL Server死锁的典型方法。记录下来整个分析过程还是很有意义的。

问题重现步骤:

经过提炼,问题重现的步骤非常简单,在SQL 2008上可以很容易地重现。

1.         首先,创建一张表格,上面有一个clustered index,两个non-clustered index。

create table tt(id int identity primary key,a char(36),b char(36),d varchar(max))

go

create index ix_a_bc on tt(a)include(d)

create index ix_b_cd on tt(b)include(d)

2.         向这个表格里插入10000条记录。

insert into tt select NEWID(),'bbb','ddd'

go 10000

3.         查询某一条记录,找到它的a字段的值。(这里以第十条为例)

select * from tt where id = 10

假设我们得到a字段的值是’EF211985-EA72-4A40-81DA-0AAB076E7AA3’。(这是一个随机值,每次测试会不一样。)

4.         现在按照这个值,对表格进行反复更新。如果两个连接同时运行,死锁就一定会发生。(测试1)

while 1 =1

update tt with(rowlock) set d='cd'

where a='EF211985-EA72-4A40-81DA-0AAB076E7AA3'

5.         但是如果把两个non-clustered index里的include (d)去掉,然后运行同样的update语句循环,死锁就不会发生。(测试2)

drop index ix_a_bc on tt

drop index ix_b_cd on tt

create index ix_a_bc on tt(a)include(d)

create index ix_b_cd on tt(b)include(d)

6.         或者把d字段的字段类型,从varchar(max)改成varchar(200),死锁也不会发生。(测试3)

drop index ix_a_bc on tt

drop index ix_b_cd on tt

alter table tt alter column d varchar(200)

create index ix_a_bc on tt(a)include(d)

create index ix_b_cd on tt(b)include(d)

问题分析步骤

为什么两条一模一样的update语句,会相互死锁呢?为什么索引上去掉一个include选项,或者改掉一个数据类型,就不会死锁了呢?要回答这个问题,还是要先仔细分析一下,死锁是如何发生的。

了解SQL Server里死锁发生的直接原因,有两种办法:(1) 收集SQL Trace。(2) 开启1222开关。因为问题可以稳定地在测试环境里重现,我们可以尽可能多地收集信息,把两种方法都用上。

首先我们用下面的脚本打开1222开关。

dbcc traceon(1222, -1)

然后,在运行update语句的连接里,运行下面的脚本,了解连接的SPID。后面我们可以用来做SQL Trace的filter。

select @@spid

假设我们得到的结果,一个是54,一个是60。

现在我们开启SQL Profiler,连到SQL Server上,新建一个Trace,在选择事件的时候,先勾上”Show all events”、”Show all columns”,然后选上Locks下面的这些events。

在TSQL下面,选上这些events。

点击Column Filters,在SPID上设置只跟踪SPID为54,60,6和20的连接。

然后运行两个连接里的update循环,让问题发生。

随后,我们运行sp_readerrorlog指令。可以在SQL errorlog里发现下面的,关于上次死锁的记录。记录比较长,关键的地方我用黄色背景标出。

deadlock-list

deadlock victim=process5e27708

process-list

process id=process5e27708 taskpriority=0 logused=0 waitresource=KEY: 8:72057594066108416(ef8a9edf5a1e) waittime=1750 ownerId=11864845 transactionname=UPDATE lasttranstarted=2011-12-01T16:41:39.540 XDES=0xca9a7950 lockMode=X schedulerid=4 kpid=6380 status=suspended spid=60sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2011-12-01T16:41:39.407 lastbatchcompleted=2011-12-01T16:37:13.077 lastattention=2011-12-01T16:20:33.170 clientapp=Microsoft SQL Server Management Studio - Query hostname=AOBAI hostpid=4672 loginname=FAREAST\haiwxu isolationlevel=read committed (2) xactid=11864845 currentdb=8 lockTimeout=4294967295 clientoption1=671090784 clientoption2=390200

executionStack

frame procname=adhoc line=2 stmtstart=24 stmtend=188 sqlhandle=0x020000001cf53c334179280da1f74e588b7179146497bde9

8000),@2 varchar(8000))UPDATE [tt] WITH(rowlock)  set [d] = @1  WHERE [a]=@2

frame procname=adhoc line=2 stmtstart=24 stmtend=188 sqlhandle=0x02000000a1ec593be12cd271e013ded3a22a397943e8f879

update tt with(rowlock) set d='cd'

where a='EF211985-EA72-4A40-81DA-0AAB076E7AA3

inputbuf

while 1 =1

update tt with(rowlock) set d='cd'

where a='EF211985-EA72-4A40-81DA-0AAB076E7AA3'

process id=process5e09dc8 taskpriority=0 logused=0 waitresource=KEY: 8:72057594065518592(d08358b1108f) waittime=1750 ownerId=11864847 transactionname=UPDATE lasttranstarted=2011-12-01T16:41:39.540 XDES=0xcb98d3c0 lockMode=U schedulerid=1 kpid=4200 status=suspended spid=54sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2011-12-01T16:41:37.473 lastbatchcompleted=2011-12-01T16:37:19.577 lastattention=2011-12-01T16:37:19.577 clientapp=Microsoft SQL Server Management Studio - Query hostname=AOBAI hostpid=4672 loginname=FAREAST\haiwxu isolationlevel=read committed (2) xactid=11864847 currentdb=8 lockTimeout=4294967295 clientoption1=671090784 clientoption2=390200

executionStack

frame procname=adhoc line=2 stmtstart=24 stmtend=188 sqlhandle=0x020000001cf53c334179280da1f74e588b7179146497bde9

8000),@2 varchar(8000))UPDATE [tt] WITH(rowlock)  set [d] = @1  WHERE [a]=@2

frame procname=adhoc line=2 stmtstart=24 stmtend=188 sqlhandle=0x02000000a1ec593be12cd271e013ded3a22a397943e8f879

update tt with(rowlock) set d='cd'

where a='EF211985-EA72-4A40-81DA-0AAB076E7AA3

inputbuf

while 1 =1

update tt with(rowlock) set d='cd'

where a='EF211985-EA72-4A40-81DA-0AAB076E7AA3'

resource-list

keylock hobtid=72057594066108416 dbid=8 objectname=PerfAnalysis.dbo.tt indexname=ix_a_bcid=lock5e66c00 mode=U associatedObjectId=72057594066108416

owner-list

owner id=process5e09dc8 mode=U

waiter-list

waiter id=process5e27708 mode=X requestType=wait

keylock hobtid=72057594065518592 dbid=8 objectname=PerfAnalysis.dbo.tt indexname=PK__tt__3213E83F10E07F16 id=lock6648e80 mode=X associatedObjectId=72057594065518592

owner-list

owner id=process5e27708 mode=X

waiter-list

waiter id=process5e09dc8 mode=U requestType=wait

从executionStack里可以看到,死锁的确是发生在两条update语句上。连接1(spid=54,process id=process5e09dc8),另外一个连接2(spid=60,process id=process5e27708)。

分析resource-list的内容,可以知道死锁发生的直接原因,是连接1在ix_a_bc索引上持有一个U-key锁,要申请索引PK__tt__3213E83F10E07F16上的U-key锁。而连接2在PK__tt__3213E83F10E07F16上有一个X锁,要在ix_a_bc索引上申请一个X锁。(SQL Trace里的Deadlock Graph事件也能告诉你类似的信息。不过1222的输出可能更全面一些。)

为什么会有这种情况发生呢?1222的输出已经不能告诉我们更多的信息了。我们需要分析SQL Trace里的锁申请和释放记录,来了解当时究竟发生了些什么。

我们先看一次成功地update,SQL Server是怎么申请和释放锁资源的。Lock:Acquired和Lock:Released这两个event能够告诉我们。不过这两个event的输出比较难读,需要把字段Mode, ObjectID, ObjectID2和Type拖到前面来看。

ObjectID2字段里的值,也就是1222输出里的hobtid。具体的值和表格、索引名字的对照关系,可以用下面的脚本检查。

select o.name ,i.name ,i.type from

sys.indexes i inner join sys.objects o on i.object_id = o.object_id

inner join sys.partitions p on p.index_id = i.index_id and p.object_id = i.object_id

where p.partition_id = 72057594065518592

-- 72057594065518592就是其中的一个ObjectID2值。

通过上面这个脚本,我们可以知道出现在SQL Trace里的ObjectID2和实际的索引之间的对应关系:

72057594065518592:PK__tt__3213E83F10E07F16

72057594066108416:ix_a_bc

72057594066173952: ix_b_cd

所以SQL Server在Key级别加锁、放锁的顺序是:

索引名

锁类型

申请/释放

阶段

连接1(SPID 54)

连接2(SPID 60)

ix_a_bc

U

申请

1

J

J

PK__tt__3213E83F10E07F16

U

申请

1

L

J

PK__tt__3213E83F10E07F16

X

申请

1

J

ix_a_bc

U

释放

1

J

ix_a_bc

X

申请

2

L

ix_b_cd

X

申请

2

ix_b_cd

X

释放

2

ix_a_bc

X

释放

2

PK__tt__3213E83F10E07F16

X

释放

2

从上图可以看出,Update语句似乎是分两步执行。第一步SQL先通过索引ix_a_bc找到了记录本身(PK__tt__3213E83F10E07F16),将其更新。第二步SQL Server又更新了两个non-clustered index上的数据,因为这两个索引有include了发生修改的d字段。最后,把三个X锁释放掉。前面那个死锁的例子,连接1正运行到第一步,而连接2运行到了第二步。由于连接2在完成第一步以后把ix_a_bc上的U锁释放掉了,使得连接1能够开始其第一步的运行,申请到了ix_a_bc上的U锁。而连接2马上又要申请ix_a_bc上的X锁,两边就相互阻塞住,死锁就这样发生了。

为什么把non-clustered index上的include字段去掉,能够解决死锁问题呢(测试2)?我们再用SQL Trace,跟踪一下数据类型修改以后的Update的执行过程。信息的收集和分析方法跟前面的一样。

索引名

锁类型

申请/释放

ix_a_bc

U

申请

PK__tt__3213E83F10E07F16

U

申请

PK__tt__3213E83F10E07F16

X

申请

ix_a_bc

U

释放

PK__tt__3213E83F10E07F16

X

释放

如果把d字段的数据类型从varchar(max)改成varchar(200)会怎么样呢(测试3)?用同样的方法跟踪。

索引名

锁类型

申请/释放

ix_a_bc

U

申请

PK__tt__3213E83F10E07F16

U

申请

PK__tt__3213E83F10E07F16

X

申请

ix_a_bc

U

释放

PK__tt__3213E83F10E07F16

X

释放

我们可以看到,不管用那一种方法,Update都从两步简化成了一步。SQL Server只需通过索引ix_a_bc找到了记录本身(PK__tt__3213E83F10E07F16),将其更新。第二步就不需要做了。这样,死锁就不会发生。

那么为什么会有这种变化呢?要理解SQL Server加锁放锁的行为,我们必须分析语句的执行计划。这里,我用在update语句前加”set statistics profile on”的方法,得到一个文字型输出的执行计划。

set statistics profile on

go

update tt with(rowlock) set d='cd'

where a='EF211985-EA72-4A40-81DA-0AAB076E7AA3'

测试1:(d varchar(max), 在索引里included, 有死锁)

测试2:(d varchar(max), 在索引里没有included, 没有死锁)

测试3:(d varchar(200), 在索引里有included, 没有死锁)

这三个执行计划很能说明问题。第一个执行计划里,有三个Index Update。这也就说明了SQL为什么要申请3个X锁。第二个执行计划里,只有一个Index Update。这是因为d字段没有被两个non-clustered index所包含,SQL Server只需要更新clustered index即可。第三个执行计划非常有趣。它只有一次update,但是包含了三个Object。也就是说,SQL一步就把三个index一起更新掉了。所以测试三也是一步做完的,不会有死锁。

分析到这里,问题基本已经比较清楚。我们可以得出以下结论:

1.       SQL Server的加锁数量、以及顺序,跟语句的执行计划有关系。

2.       表格上的索引越多、数据类型越复杂,执行计划也会越复杂,从而导致遇到阻塞或者死锁的几率增加。

3.       消除死锁,可以先检查语句的执行计划,从调整执行计划入手,引导SQL Server申请比较少的锁。

对于这个死锁案例本身,将varchar(max)包含在一个索引里,是不太妥当的。这样会大大增加索引的复杂度,也增加了SQL Server的维护成本。解决这个索引的比较好的方法,是将字段d从索引里移除,或者把它改成一个有合适长度限制的varchar类型。

Update导致SQL Server死锁的典型方法(转载)的更多相关文章

  1. SQL Server死锁的解除方法

    如果想要查出SQL Server死锁的原因,下面就教您SQL Server死锁监控的语句写法,如果您对此方面感兴趣的话,不妨一看. 下面的SQL语句运行之后,便可以查找出SQLServer死锁和阻塞的 ...

  2. SQL Server死锁

    SQL Server死锁 多个事务之间互相等待对方的资源,导致这些事务永久等待 注意是永久等待,而非长事务 死锁的4个条件 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程 ...

  3. 服务器重启可能会导致SQL Server中部分数据库变为single user mode

    今天检查公司生产服务器的SQL Server数据库,惊讶的发现有三个生产数据库变为了single user mode.奇怪的是没有任何人和程序执行过SQL语句将这三个数据库设置为single user ...

  4. 【数据库】Sql Server 2008完全卸载方法(其他版本类似)

    本文介绍如何卸载 Microsoft SQL Server 2008的方法.当您按照本文中的步骤时,您还准备系统以便可以重新安装 SQL Server 2008版本 一.    SQL2008卸载. ...

  5. SQL Server修改标识列方法(备忘)

    原文:SQL Server修改标识列方法(备忘) SQL Server修改标识列方法 ----允许对系统表进行更新 exec sp_configure 'allow updates',1 reconf ...

  6. 删除SQL Server大容量日志的方法(转)

    删除SQL Server大容量日志的方法 亲自实践的方法 1.分享数据库,如果提示被其他连接占用,不能分离,刚勾上drop connections 2.复制下所有文件,一定要备份好,以防自己操作失误 ...

  7. C#将图片存放到SQL SERVER数据库中的方法

    本文实例讲述了C#将图片存放到SQL SERVER数据库中的方法.分享给大家供大家参考.具体如下: 第一步: ? 1 2 3 4 5 6 7 8 9 10 //获取当前选择的图片 this.pictu ...

  8. SQL Server存储过程的删除方法

    SQL Server存储过程的删除方法使我们经常会用到的,下面就为您介绍扩展存储过程的删除方法,如果您对SQL Server存储过程方面感兴趣的话,不妨一看. --清除SQL Server所有的危险扩 ...

  9. 修改Windows帐户密码,导致Sql Server 2000无法启动

    修改Windows帐户密码,导致Sql Server 2000无法启动. --现象以管理员或同等权限用户登录 Windows XP,建立 Sql Server 2000 数据库.之后,在修改此 Win ...

随机推荐

  1. cannot open clipboard 解决办法

    对于电脑本身或者一些应用程序操作的时候,会出现cannot open clipboard的问题,这是你系统没有剪切板程序 首先: 在开始->运行中输入clipbrd 回车, 如果系统弹出了剪切板 ...

  2. Java-JVM OutOfMemory 情况(JDK8)

    JVM 运行时内存结构(Run-Time Data Areas) 内存溢出分为两大类:OutOfMemoryError 和 StackOverflowError. 一.HeapOomError (JV ...

  3. mysql数据库索引和引擎

    1. 数据库索引 1.1 索引作用 当我们在数据库表中查询数据时,若没有索引,会逐个遍历表格中的所有记录,表格中数据记录量大时很耗时.建立索引就像创建目录一样,直接通过索引找到数据存储位置,加快查找. ...

  4. 关于jenkins

    启动不了时可更改端口 java -jar jenkins.war –httpPort=8090

  5. Thinkphp5 的sesssion在同一个控制器不同的方法无法获取session的原因和对策

    这一段在用thinkPHP5开发微信小程序接口的时候,在同一个控制器一个方法中存入session,在另一个方法中取出session,一直都是无法取出. 查阅各种资料得到原因:thinkPHP5里面的s ...

  6. 一百零二:CMS系统之sweetalert提示框和使用

    实现效果 css body.stop-scrolling { height: 100%; overflow: hidden; } .sweet-overlay { background-color: ...

  7. SpringBoot: 3.SpringBoot整合filter(转)

    整合方式一:通过注解扫描完成 Filter 组件的注册 1.编写filter package com.bjsxt.filter; import javax.servlet.*; import java ...

  8. 20190526 - CentOS 7 中 安装 MySQL 8 并授权 root 远程访问

    1. CentOS 7 中 安装 MySQL 8 CentOS 7 中内置 MariaDB 建议升级一下用,性能好很多.但如果一定要用 MySQL 8,就得自己装. 坦白的说,Oracle 升级 My ...

  9. 3分钟Markdown快速入门与使用

    Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式. 注意:图片为效果图 1 标题 #开头代表标题,几个#号代表几级,最高支持六级标题 ...

  10. 字符串——kmp

    目录 一.前言 二.思路 三.代码 一.前言 kmp算法是用于从文本串text的字串中,寻找含有的模板串pattern的数量/位置的算法. 例如,在文本串abcabcccabc中,模板串abc的数量有 ...