记一次死锁

业务场景:微信支付模块,每一次请求都会在pay_log表里插入订单号等记录,支付回调时会更新pay_log表的记录

同时还有一个定时任务用来定时将所有1小时前的订单标记为交易超时

编写单元测试,模拟支付回调通知,结果出了异常,更新记录时出现死锁

大致SQL如下:

事务A

1
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

事务B

1
2
INSERT pay_log VALUES(NULL,?,NOW(),?,?,?,?,?,?,?,?,?,?,?,?) 
Update pay_log SET tradeState =? WHERE outTradeNo = ? AND tradeState = ? AND payType = ?

然后事务B被回滚了

解决方案:要么禁用定时任务,要么去掉单元测试的事务手动回滚,要么单元测试不要打断点调试(定时任务设置了项目启动后10秒后启动)

附上MySQL死锁日志,其实这玩意只能大致的定位故障,因为可以看出事务2的SQL只有最后执行的那一条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2019-08-20 16:23:07 0x7ff1b2039700
*** (1) TRANSACTION:
TRANSACTION 43869195, ACTIVE 3 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 269 row lock(s)
MySQL thread id 746830, OS thread handle 140676071700224, query id 184518127 192.168.3.108 root updating
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869195 lock_mode X waiting
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0


*** (2) TRANSACTION:
TRANSACTION 43869181, ACTIVE 7 sec starting index read
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 747008, OS thread handle 140676050425600, query id 184518193 192.168.3.49 root updating
Update pay_log SET tradeState =1 WHERE outTradeNo = 'T9744695806' AND tradeState = 0 AND payType = 0

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X locks rec but not gap
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 4 n bits 168 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** WE ROLL BACK TRANSACTION (2)

关于数据库删除数据的同时保持完整性的个人做法。

进了单位,要我从头写一个商城出来。
完成了初步的数据库表格设计之后,发现如下一个问题:

我的address表记录了用户的收货地址,而订单中采用aid指向address表的某行。

那么如果用户后期删除了这个aid对应的收货地址,或者修改了地址,会引发bug:早期订单对应的地址会改变或者无法找到。

一开始考虑用外键约束,但是外键约束只能达成这样的目标:

  • 如果存在采用这个aid的订单,就拒绝删除aid
  • 删除aid同时删除所有带这个aid的订单
    这明显不符合常识和业务需求。

我想出的解决办法是:给address表加一个valid的int字段,长度为1(实际上就是boolean,记录这个地址有没有效),删除地址的时候不直接从数据库删除,而是将valid改为0。

修改地址的时候,也不直接改,而是创建一个新的地址,再把老的地址valid值改成0。

这样,如果订单采用了valid为0的address,也能够正常显示,但是用户在选择自己收货地址的时候是看不到这条“被删除”的地址的(WHERE valid =1)。

类似的业务需求也是这样,例如product也加一个valid字段,出现一个包含了已删除产品的订单时,能正常显示,但是这个产品不能被加入购物车和订单。

这样业务需求倒是实现了,但是service感觉会异常复杂……需要重新梳理。

幸好Mapper层不需要太多修改,把删除方法全去掉,再在某些条件里加上valid=1就行了。