很久没有写博客了,这里面的原因有很多。最近的一个项目由于客户明确提出要做下性能压力测试,使用的工具就是VS自带的压力测试工具。以前其它项目做压力测试后反馈的其中一个重要问题就是数据库的死锁。没想到我们这个项目测试时死锁同样的发生了,我之前的项目由于很少参与压力测试,基本上也不会去了解死锁,以及死锁如何解决的问题。
既然有了这个需求,那么要想解决死锁就需要对死锁的相关知识有一定的了解,对于非DBA的来讲并不需要了解的特别深,知道基本概念以及常见分析方法即可,毕竟我们不靠这个吃饭,没必要达到特别细的境界。这里我找到了一个微软MVP写的一系统博客,对我理解死锁非常重要,这里分享下目前我为解决死锁所采用过的方案。
压力测试的业务场景:
1.模拟用户提交申请
a) 涉及到的表
i. 申请主表,一次申请生成一条数据。
ii. 申请的医生明细,一次申请包含多个医生,一个申请包含100个医生
iii. 单个医生明细,每个医生一条明细数据
综上所述一条申请的创建,需要插入201条数据。
b) 申请逻辑
i. 首先调用保存
ii. 然后执行提交逻辑
各种逻辑验证,这是历史原因造成的(一个维护了几年的项目代码逻辑的混乱是难以想象的),总是在提交数据时做双重较验 ,比如数据是否有重复行的逻辑,这里会反复读取申请医生表以及医生明细这两个申请明细子表。这也是产生死锁的主要原因,此场景已经满足了同时读取以及修改同一表的情况。除此还会往其它表中插入数据,比如一些状态跟踪信息,发邮件等。
至于为什么被分割成两个逻辑来处理这原本是同一动作的需求,已经法考正最初的设计者了,这些逻辑包含各种EntityFramwork的查询写法,很难做有效的优化。
2. 压力设置
a) 并发8个用户
b) 每1分钟增加5个用户
曾经学过的方法:
- 降低事务隔离级别为read uncommitted,结果是并不能消除死锁,但死锁的次数有所降低,主要时共享锁引发的死锁次数降低了。
- 分段分析法,也可以说是排除法。只执行一部分逻辑,比如我们上面的一个申请分为两步,先保存后提交,只保存的结果是死锁依旧。
- 寻找死锁跟踪的方法,试图寻找死锁的本质原因。我简单的按我自己的理解翻译了一些MVP写的文章,大家可以参考 。
解释了SQL 中的各种锁以及它们之间的兼容性,只有了解了这些才能知道锁发生的场景,比如知道了共享锁之间是兼容的就能马上反应出纯读的操作是不会发生阻塞的
事务隔离级别的不同会影响锁的行为,其中重要说明了降低事务隔离级别并不能消除死锁
只有出现了阻塞才会升级成死锁,所有了解阻塞是第一件事
这篇通过两种方式说明如何去跟踪分析死锁的本质原因,通过SQL自带的性能监控工且可以很方便的导出出现死锁的相关信息
这篇非常详细的说明了死锁产生的原理
死锁文章的重要结论:
大部分死锁是因为未经过优化的查询导致的,但因为我们项目在处理这个申请的逻辑中有太多逻辑,不太可能在短时间内进行有效的优化,所以我暂时采用了一个也许不是很好的方案,即想办法降低排它锁的相互竞争,说的简单点说是在程序中通过一定的手段避免并发去调用更新或者插入数据的逻辑。
偏门解决方案:
通过一个取票排队的队列去解决数据插入以及更新的并发,原理就是一个线程想要插入数据时,先取票然后排队,当号轮到它时才能执行数据库操作,其它线程正在执行时,我们通过自族锁来实现排队。这个方法最大程序上解决死锁的问题,但不推荐这么做,之所以采用这种非常规手段,也是受制于现有程序的逻辑。如果大家在EF中也解决过死锁问题,可将经理分享给我。
public int AddWithSpinLock(ObjectModel.Request svarRequest)
{
bool lockTaken = false;
svarRequest.Ticket = Guid.NewGuid();
var newRequestId = 0;
try
{
_spinlock.Enter(ref lockTaken);
_queue.Enqueue(svarRequest);
while (null != _queue && _queue.Count > 0 && _queue.Peek().Ticket == svarRequest.Ticket)
{
// do something
_queue.Dequeue();
return newRequestId;
}
}
catch (Exception ex)
{
if (lockTaken) _spinlock.Exit(false);
_queue.Dequeue();
throw ex;
}
finally
{
if (lockTaken) _spinlock.Exit(false);
}
return newRequestId;
}