vlambda博客
学习文章列表

小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!

小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!
一、引言


性能测试中发现死锁,是比较严重的问题。笔者从事主机性能测试多年,死锁问题却较为少见。有幸在一个小项目的测试中,发现死锁,倒成了一次难得的学习机会。

在笔者进行该项目测试时,选用同一行部100个账户,执行关联账户和解除关联交易测试。当开展解除关联交易测试因成功率不足而排查问题时,发现其逆交易关联账户交易出现少量死锁。进一步定位该问题过程中,解除关联交易也出现一例因死锁导致交易失败的情况。异常表中记录了发生死锁的程序Z1和表A,除此之外,CQM中显示程序Z1、Z2、Z3存在SQL大于1秒情况,SQLCODE显示-913(因为死锁或超时导致不成功执行)

打印系统死锁报告进一步分析问题。


小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!
二、DB2锁机制


为了讲清楚本次测试发现的死锁问题,需要先对DB2数据库常用锁类型和隔离级别进行简单回顾。

常用锁类型:

  • Share — 共享锁,即S锁,只能进行读操作。

  • Update— 更新锁,即U锁,可以进行更新操作。

  • Exclusive — 排它锁,即X锁。

它们三者的使用关系见下表。

以目前所持有的为S锁举例,请求锁如果是S锁和U锁,则允许同时存在,X锁不行。简单来说,就是事务对一个数据对象加了S锁,可以允许其它事务对这同一个数据对象再加S锁或U锁,但不允许加X锁。


小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!


数据库隔离级别:


小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!

RR — Repeatable Read

如果使用这种隔离级,在一个事务中所有被读取过的行上都会被加上S锁,直到该事务被提交或回滚,行上的锁才会被释放。这样可以保证在一个事务中即使多次读取同一行,得到的值也不会改变。另外,在同一事务中如果以同样的搜索标准重新打开已被处理过的游标,得到的结果集不会改变。即通常所说的无幻象。

RS — Read Stability

如果使用这种隔离级,在一个事务中所有符合条件的行上都会被加上S锁,直到该事务被提交或回滚,行上的锁才会被释放。这样可以保证在一个事务中即使多次读取同一行,得到的值不会改变。但是,如果使用这种隔离级,在一个事务中,如果使用同样的搜索标准重新打开已被处理过的游标,则结果集可能改变(可能会增加某些行),这是因为RS隔离级不能阻止通过插入或更新操作在结果集中加入新行,即通常说的有幻象。相对于RS,RR的加锁范围更大。对于RS,应用程序只对符合要求的所有行加锁;而对于RR,如果是表扫描的方式访问,则整个表都加锁,如果是索引访问,则将在索引上加特殊的锁,保证满足游标添加的记录不会被其它线程插入。

CS — Cursor Stability

如果使用这种隔离级,在一个事务中,结果集中只有正在被读取的那一行(游标指向的行)将被加上S锁(注意,在我们实际使用中连S锁都不会加,而是使用LOCK AVOIDANCE机制保证不脏读),其他未被处理的行上不被加锁。这种隔离级只能保证正在被处理的行的值不会被其他并发的程序所改变(如果是LOCK AVOIDANCE机制下,正在被处理的行也会被其它并发程序所修改)。该隔离级是DB2缺省的隔离级。

UR — Uncommitted Read

如果使用这种隔离级,对于只读操作,不加行锁。该隔离级可以改善应用程序的性能,同时可以最大程度的允许并发。但是,应用程序的数据完整性将受到威胁。如果需要读取未提交的数据,该隔离级是唯一选择。


小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!
三、死锁问题分析与解决

根据定位到发生死锁的程序和表再进一步排查代码,发现交易调用到一个很底层的程序中使用了RS隔离级别。结合锁类型和RS隔离级别的特点,对本次死锁问题进行分析。

关联账户交易需要读取A表和B表,再更新A表记录的一个字段。其中,读取A表的select语句使用了WITH RS。在RS隔离级下,所有满足条件的记录都会被加上S锁,直到该交易被提交或回滚,S锁才会被释放。更新A表时,又会加上X锁,在更新操作没有完成前,A表的S锁不会释放。

鉴于行锁开销较大,主机DB2常用的最小锁单位是页。

本次死锁问题可简化为以下语句:

SELECT xxx(CURSOR) WITH RS

S-LOCK(P1)

UPDATE xxx(CURSOR)

X-LOCK(P1)

小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!

并发执行的两个事务,事务1对A表的某一数据页加了S锁,事务2也对该数据页加了S锁,S锁和S锁兼容。同时事务1和事务2又都去修改这一数据页,都去申请X锁,但X锁和S锁不兼容,因此就互相等对方的S锁释放。但事务没被提交前,S锁不会释放,因而造成死锁。

修改RS隔离级别应能改善此次死锁问题,但SELECT xxx(CURSOR) WITH RS语句所在的程序是一个很底层程序,不确定改动它是否会影响其它上层应用。

再次分析得出一个方案:调整程序顺序,以改变对A表的访问次序。

UPDATE xxx(CURSOR)

X-LOCK(P1)

SELECT xxx(CURSOR) WITH RS

S-LOCK(P1)

小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!


即事务1先对A表做更新操作,同时加X锁。因为X锁是排它的,事务2只能等待X锁。事务1提交,释放X锁后,事务2才会获得X锁,开始进行更新操作。理论上应该就不会发生死锁了。更重要的是,改变更新和查询的顺序,不会影响业务逻辑。

变更程序后,重新对关联账户和解除关联两支交易进行测试,还是选用之前的100个账户。关联账户交易平均响应时间由之前的545毫秒下降到49毫秒;解除关联交易的成功率由85%提高到100%。程序Z1、Z2、Z3也再没有出现SQL大于1秒的情况。

小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!
END
小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!


小伙伴找我讨论性能测试中的死锁问题,怎么都没听过?!

推荐阅读

点击阅读☞

点击阅读☞

点击阅读☞

点击阅读☞

点击阅读☞

“阅读原文”一起来充电吧!