1 问题描述
支付平台上线之后在payorder insert的时候时不时会报dead lock detected 的错误日志,在流量高峰的时候更易发生,在流量不是很高的时候也有发生。
追查发现这种情况下往往是同一个业务订单同一时刻并发支付请求,这种情况不是用户正常的支付行为,有可能是恶意刷或者请求重发。
同一个业务订单同一个时刻并发的支付请求会造成unique key冲突(partnerid + orderno),因为orderno是业务订单号和时间戳的组合。
在处理这种unique key冲突支付平台是会允许第一个获得锁的成功,后续key冲突的会insert ignore并且判断如果affected rows为0直接返回系统错误。
那为什么在事务开始insert 记录的时候就发生死锁呢?
(PS: 联想到之前iuserlastpaytype那个死锁问题推断也是这个问题,userid作为的主键,当时解决办法是支付成功之前不更新iuserlastpaytype,但是没有找到死锁原因)
2 问题原因
要回答这个问题就要理解innodb 在insert时候的加锁机制,官方文档http://dev.mysql.com/doc/refman/5.5/en/innodb-locks-set.html
insert加锁策略:
在插入row之前,会设置插入意向锁(是一种区间锁),这个意向锁通知其他事务我在这个区间会插入一条记录,但是其它事务如果在这个区间和我的插入位置不冲突的话就无需等待。
insert操作会对插入的行加排他锁,首先应该明确的是innodb的锁都是加到索引上的,并且是index-record lock也就是记录锁而不是区间锁。
如果并发事务插入的记录不产生dulicate key冲突就不会有问题,发生dulicate key冲突时候innodb会先对这个记录加shared lock,当先获得该记录排他锁的事务回滚后,其它事务申请的shared lock会被授予,然后都会同时申请该记录的排它锁,当2个以上事务占有该记录的共享锁又要申请该记录的排它锁的时候死锁就产生了。
结合业务情况,因为alipaywap,tenpaywap,微信支付,联动优势都会先通过服务端先向第三方支付请求下单,这个过程有失败的概率会导致事务最终没有提交而回滚。当发生3个以上并发请求同一个支付订单号,而且第一个先获得insert排他锁的事务因为第三方支付交互失败回滚就必然导致死锁发生。 通常在第三方服务不稳定的时候发生。
下面来测试模拟这种死锁情况,表结构如下,a字段建了unique key
结果表明,无论是insert ignore还是insert ... on duplicate key update ... 都会产生死锁。
|
- insert ignore的情况
Transaction 1 | Transaction 2 | Transaction 3 |
---|---|---|
roll back | ||
insert ignore into yxg (a,b) values (9, 'a'); | insert ignore into yxg (a,b) values (9, 'a'); | |
- insert ... on duplicate key update
Transaction 1 | Transaction 2 | Transaction 3 |
---|---|---|
insert into yxg (a,b) values (8, 'a') on duplicate key update b='b'; | insert into yxg (a,b) values (8, 'a') on duplicate key update b='b'; | |
rollback | ||
3 解决办法
解决思路:
就是让先获得插入排它锁的insert操作事务不产生回滚。
解决办法:
insert payorder之后立刻先提交一次事务,避免其因为和第三方交互失败而产生事务回滚。这样大不了增加了少许payorder记录,而这样的payorder也应该记录参与统计支付成功率的。
4-25 17:15该解决方案上线,观察3天没有再报dead lock的错误(之前是周末必报并且频率不低)。
4 后续思考
4.1 为什么innodb在insert发生duplicate key冲突后会对记录先加shared lock然后再申请排它锁,而不是直接申请排它锁呢? 这样不就不产生死锁了吗?