SQL Server线程事务与死锁的一次实例分析

背景

故事是这样的,在一个系统试运行阶段,发现了一个数据库死锁的异常.具体的错误是 :

"XX写入异!事务与另一个进程锁死在锁|通信缓冲区资源上,并且一杯选做死锁牺牲品"

按字面的意思理解也很简单.多个线程同时操作数据库死锁导致了问题.这里需要了解到非常多数据库相关锁的知识,具体请看有些人写的非常好的文档:

数据库系统原理

Microsoft SQL Server中的事务与并发详解

两篇文章稍微有些长,但是希望大家也读一下,否则对数据库锁不了解,很难解决死锁的问题

正文

业务其实也比较简单,这张表是个批处理表,有时候需要批量更新数据;SQL语句非常简单

update my_table set colume1 = 'xxx' where colum2=@para 

看似非常简单的语句其实里面隐藏着非常多的知识点,如 主键,索引,全表扫描,X(写)锁,IX(意向)锁,行锁,页锁,表锁,等;我们一个一个情况分析,看那些知识点在什么情况下会发生什么事情

  1. 这是一个update操作,默认会开启X锁,
    1. 默认开启查询出数据的行锁X锁,整个table 的IX意向锁
  2. 如果column2是主键
    1. 当多线程再次执行是,如果@para与其他线程的value不相等,不死锁
    2. 如果@para与其他线程的value相等,死锁
  3. 如果column2不是主键,但是是索引,执行效果与主键相同
    1. 相同的前提条件是,查询条件扫描时走了索引
  4. 如果column2既不是主键,又不是索引
    1. 同样,依然是把查询出来的数据上X锁,但是第二个线程来查询的时候,依然会造成锁的等待,由于没有索引,数据更新的时候需要全表扫描,但由于一些数据在X锁上,所以无法读取,此时相当于表锁了;

以上问题,在开发中也考虑到了,所以我们的程序给column2字段加了索引,而且我们的程序基本上不会由多个线程同时跟新一条数据,理论上不应该频繁发生死锁的问题(大概两天会出现一次死锁的错误)

这个时候需要与一些数据库本身的特性可能有关了,这里是sql server 数据库,开启模拟情况,并打印加锁日志:

-- Lock info 
SELECT -- use * to explore
  request_session_id            AS spid,
  resource_type                 AS restype,
  resource_database_id          AS dbid,
  DB_NAME(resource_database_id) AS dbname,
  resource_description          AS res,
  resource_associated_entity_id as resid,
  request_mode                  AS mode,
  request_status                AS status
FROM sys.dm_tran_locks
where DB_NAME(resource_database_id)='mydb'
-- KILL 59;

模拟语句1

BEGIN TRAN;
	update my_table set colume1 = 'xxx' where colum2='AAA'
    WAITFOR DELAY '00:00:10';  -- 模拟事务进行了10秒钟后提交
COMMIT TRAN;

模拟语句2

BEGIN TRAN;
	update my_table set colume1 = 'xxx' where colum2='BBB'
COMMIT TRAN;

两个模拟语句在两个窗口同时执行,然后查看Lock情况,执行多次发现都与预计的情况相符,理论上不会发送死锁的情况. 第一条语句查询出来的数据上的都是X锁,同时也有Page 的IX锁,Object(表)的IX锁,第二条查询的时候,通过索引查询,不需要等待第一个事务;

很迷茫,无论是正式环境还是测试环境,都模拟不出来现场bug.我这里已经模拟了10秒钟的慢查询,实际情况比这快很多.纠结了一上午,超出了已知知识的范围.猜测是应该某种情况下的update操作进行了加表锁,而不是行锁导致的.查询条件更改,添加null值,都不能模拟出表锁;

问题原因

在此处没有思路的情况下,换了一种思路去解决问题,接下去,我去分析了批处理my_table表的数据,正常情况下的column2属性值数据应该在100条以内,但由于是试运行模拟数据,有些数据在四五千条左右,抽出一个四五千的查询条件,继续进行模拟加锁操作,经过多次模拟,此时发现了最终问题所在,当update操作的数据超出一定数据条数之后,本来默认的行锁此时升级成了表锁.接下来的多线程等任何查询等语句,都要等待这个线程释放掉表的X锁.问题找到了,解决起来就简单多了;

解决方法

两个方法,第一个方法没有去验证

  1. 在update的时候指定行锁的实现,不通过sql server的内部优化引擎
  2. 在update的时候限制每次批处理的条数(测试模拟中1500条以内,不会升级成表锁)

思考

除了书本上的理论知识之外,每个框架,软件的内部优化实现不尽相同,需要在实践中多思考多积累;

你可能感兴趣的:(技术文章,死锁,事务,并发)