死锁问题

  1. 什么是死锁?如图所示是一个死锁报错:
  2. 产生原因:
  1. 存在互斥的资源竞争,就会导致如上的报错,它就是死锁;
  2. 根据操作系统中的定义:死锁是指一组进程中的各个进程不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
  3. 比如,两个查询在一个表上持有共享锁,但是两个查询都需要将其锁升级为互斥锁才能执行更新。 由于无法进行任何查询,因此需要干预。
  4. SQL Server常规执行死锁检查,并将选择终止其中一个进程以允许其他进程继续进行。 这个被杀死的过程称为死锁受害者。
  1. 死锁的四个必要条件:
  1. 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
  2. 请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。
  3. 非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
  4. 循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。

对应到SQL Server中,当在两个或多个任务中,如果每个任务锁定了其他任务试图锁定的资源,此时会造成这些任务永久阻塞,从而出现死锁;这些资源可能是:单行(RID,堆中的单行)、索引中的键(KEY,行锁)、页(PAG,8KB)、区结构(EXT,连续的8页)、堆或B树(HOBT) 、表(TAB,包括数据和索引)、文件(File,数据库文件)、应用程序专用资源(APP)、元数据(METADATA)、分配单元(Allocation_Unit)、整个数据库(DB)。

如何修复死锁

  1. 使用WITH(NOLOCK) 避免锁竞争。从WITH(NOLOCK) 来讲,它的目的是允许脏读,不锁定表,以达到快速查询的目的;SELECT查询时未使用WITH(NOLOCK)会产生共享锁,且在查询后立即使用了DELETE 删除此数据,则会变成独占锁;如果同一个seller在短时间内重复操作两次可能会引起两个事务竞争,产生死锁。在允许脏读的业务中,尤其是对同一条数据存在Select 和 UPDATE/DELETE/INSERT 的时候,就容易导致死锁问题;由于SQL SERVER 的强大功能,在查询数据的还可以做一些业务逻辑逻辑,这就容易发生在SELECT 之后进行UPDATE/DELETE/INSERT操作的可能性,就容易导致死锁问题,所以当我们在允许脏读的业务场景下,尽可能的加上WITH(NOLOCK),它也可以提高查询速度。使用这种方式避免死锁。
  2. 使用临时表修复死锁问题,首先清楚一个条件,就是在进行UPDATE/DELETE/INSERT操作,会产生排他锁,此操作会锁住要操作的数据。尤其是在Update 的时候,我们又INNER JOIN 关联查询了其他数据,极有可能导致死锁,请看如下这段sql:
UPDATE TOP 100 X
SET X.ShipFromCountryCode = TT.ShipFromCountryCode_Real,
X.FastestDeliveryDays = TT.FastestDeliveryDays,
X.SlowestDeliveryDays = TT.SlowestDeliveryDays,
X.LastEditDate = GETDATE(), X.LastEditUser = '模拟测试'
FROM Table1 TT
INNER JOIN Table2 X
ON X.ItemNumber = TT.ItemNumber AND X.CompanyCode = XXX AND X.ShipToCountryCode = TT.ShipToCountryCode
WHERE X.ShipFromCountryCode <> TT.ShipFromCountryCode_Real
OR X.FastestDeliveryDays IS NULL OR X.FastestDeliveryDays <> TT.FastestDeliveryDays
OR X.SlowestDeliveryDays IS NULL OR X.SlowestDeliveryDays <> TT.SlowestDeliveryDays

针对这段sql 产生死锁,我们可以做出如下优化考虑:

  • 先观察参数是否特殊,查询耗时多少,查询数据量大小,搜索条件字段是否都有索引;
  • 缩小查询粒度,减少Top操作数量。因为Update 并不是查询好了再去锁定资源,而是边查询边锁定。如果我们操作的数据过多,就会导致加锁的时间变长,更容易造成死锁问题;
  • 增加临时表,先将要操作的数据加入到临时表,然后通过临时表再关联Update 语句 执行逻辑操作,同时可以给临时表存一个要关联表的主键并增加primary key,以此关联进行关联查询;
  • 当数据量较少时,可以通过exists的方式挂在where 里面,使用exists 的方式在我们日常中也可以大量使用;
  1. 减少事务,减少事务持续。死锁简单来说是,事务越大,事务持续时间越长,越容易死锁;我们可以优化sql 的执行速度,避免一条语句执行过长。优化sql 执行速度的方式就有很多了,可以从索引,查询方式,筛选条件,查询参数等等入手;同时还可以在代码中进行优化,比如使用缓存来减少db查询,在代码中控制多线程顺序性访问,在代码中控制查询参数和条件筛选量,在代码中控制业务以保证单个method事务简短等等。
  2. 整体上来讲,尽可能避免死锁的发生,就是要减少查询范围,减少操作数据数量,避免锁冲突,是否允许脏读,减少UPDATE/DELETE/INSERT操作,减少SQL 逻辑,避免互斥访问。可以参考如下几个点:
  • 按同一顺序访问对象,如果所有并发事务按照同一顺序访问对象,则发生死锁的可能性就会降低;
  • 保持事务简短并在一个批处理中
  • 使用低隔离级别
  • SELECT语句增加WITH(NOLOCK)