浅析一个sql server数据库事务死锁问题

以前遇到过一个sql server数据库事务死锁问题,这里记录下来分享给你们。html

问题的原型java

为了描述方便,这里抽象问题的原型以下:sql

一个学生管理系统,数据库是sql server,有一个Web API用于建立student。student对象的表结构以下:数据库

CREATE TABLE [public].[A_Student](
  [id] [int] IDENTITY(1,1) NOT NULL,
  [name] [nvarchar](50) NOT NULL,
  [remark] [nvarchar](50) NULL
) ON [PRIMARY]

其中id是primary key。(note: primary key会自动建立一个clustered index)c#

建立一个student的实现逻辑能够简化为下面一个事务(包含一个插入语句和一个查询语句):并发

BEGIN TRAN
INSERT INTO public.[A_Student] ([name] ,[gender] ,[remark]) VALUES ('john', 'male','good student!')
SELECT [id] from public.[A_Student] where name = 'john'
COMMIT TRAN

在高并发测试过程当中发现,这段逻辑会发生事务死锁问题,异常信息以下:分布式

"Transaction (Process ID 60) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction."ide

问题的缘由高并发

后来研究发现,当上面的建立逻辑有两个并行事务(T1和T2)交叉执行时,死锁问题就会发生。具体缘由以下:post

T1和T2同时执行完insert语句,都会对新增的行加X锁;而后,当T1和T2都执行select语句时,都须要申请全部行的S锁(note: 因为name字段没有加index,因此须要执行clustered index scan),这时T1就pending在T2的X锁上,T2则pending在T1的X锁上,死锁就发生了。

针对这个问题,有两个解决方案:

1.把name字段加一个index;

2.把select语句加上with nolock

对于方案1,加上index以后,select语句就不会再有一个clustered index scan,只会是index seek,意味着只会申请某条记录的S锁,因此就不会发生死锁。

对于方案2,把select语句加上with nolock后,语句执行时直接就不加锁,锁循环依赖就不存在了,死锁也就解决了。固然,不加锁,必定程度会出现脏读,可是在这个业务场景下,不影响。

延申

1、没有添加任何索引的时候,查询语句(select id from table where name = 'john')的执行计划是table scan;

当给id加上clustered index以后,语句的执行计划是clustered index scan;

当给name加上index以后,语句的执行计划就是index seek了。

为何select的字段是id,where的条件是字段name,这里会走index seek呢?

通常来讲,select的字段须要是执行计划用到的index包含的字段,这样才会走index seek,以下面语句:

select name from table where name = 'john'

但这里走index seek却应用到了另一个概念”覆盖查询“,具体含义以下:

当索引包含查询中的全部列时,性能能够提高。 查询优化器能够找到索引内的全部列值;不会访问表或汇集索引数据,这样就减小了磁盘 I/O 操做。 使用具备包含列的索引来添加覆盖列,而不是建立宽索引键。

若是表有汇集索引,则该汇集索引中定义的列将自动追加到表上每一个非汇集索引的末端。 这能够生成覆盖查询,而不用在非汇集索引定义中指定汇集索引列。 例如,若是一个表在 C列上有汇集索引,则 B 和 A 列的非汇集索引将具备其本身的键值列 B、 A和 C。

https://docs.microsoft.com/zh-cn/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver15#Nonclustered

从上面介绍能够看到,汇集索引会自动加到每一个非汇集索引的后面造成覆盖查询,这就是为何上面select id直接走index seek的缘由。

2、另外,在测试过程当中发现,当给name加上index以后,下面这条语句(select全部字段)的执行计划是clustered index scan,而不是index seek + key lookup。

select * from table where name = 'John'

缘由是,在sql server中当表的数据量达到一个阈值(tipping point)的时候,执行计划可能会发生变化。当时测试过程当中,表的数据量都很小,因此执行计划是clustered index scan;后来,向表中插入1503条记录以后,执行计划就变成了make sense的index seek + key lookup。关于这个机制,能够参考:

扩展

关于Index的实现原理,通常来讲,index的实现都是基于B树或者B+树(在二叉查找树BST的基础上,减小磁盘IO);同时,不少数据库都还支持一些其余类型的index,好比哈希index,其实哈希index的底层原理就相似于java里面的HashMap,c#里面的Dictionary。

关于汇集索引和非汇集索引,其实有的数据库并无实现这个概念,好比postgres。sql server实现了这两个概念,详细的介绍能够参考(Clustered index: https://docs.microsoft.com/en-us/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver15#Clustered和non-clusterd index:https://docs.microsoft.com/en-us/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver15#Nonclustered

Microsoft sql server managment studio中查看执行计划快捷键Ctrl+L;查看锁使用状况EXEC sp_lock。

相关阅读

分布式锁在JPA ID生成器中的应用

liquibase和flyway中分布式锁实现的区别?

关于文本排序的那些事

相关文章
相关标签/搜索