故事背景
这是一个简单的订单拉取功能,需求是定时任务每秒定时调用订单拉取接口拉取订单,入库后调用订单确认接口。订单拉取接口最多拉取八条,因为订单量比较多,所以定时任务要设置一秒执行一次。
伪代码
@Transactional
public void receive(){
httpClient.execute(receiveUrl);
for(list){
order = mapper.select();
if(order == null){
insert(order);
}else {
update(order);
}
}
httpClient.execute(ackUrl);
}
BUG1 死锁
在程序上线了一段时间后,突然发现拉取的订单少了一部分,在查看error日志的时候居然发现: Deadlock found when trying to get lock; try restarting transaction。我内心:???我就觉得很奇怪,死锁不是循环等待吗?一开始感觉这样的情况比较难出现......而且之前刚上线的时候也是没有这个报错的,后面查日志的时候看到很多订单都重复推送了,在update的时候报死锁了。
首先是百度一下,发现show engine innodb status 命令可以看到最后一次死锁的日志。
可以看到是主键报死锁了。死锁的情况就是
事务A update id = 1(成功)
事务B update id =2(成功) update id =1(等待)
事务A update id = 2(死锁)
解决方案:
1、去掉@Transaction注解
2、方法加锁(性能较低)
3、id排序
BUG2 BeanUtils.copyProperties属性覆盖
大家应该都有用过BeanUtils.copyProperties吧,可以把A对象的属性覆盖到B对象中。我是拉取完数据,如果有这个订单号,就用copyProperties把属性覆盖到原订单号里面。结果后面发现有些订单入库后数据缺失了,查看日志是有拉取到两条一样的订单,一条是完整的,一条不完整。出现了BUG是因为第一次插入了完整的订单,第二次update的时候,BeanUtils.copyProperties会把空字符串的数据覆盖掉原来的数据
name:zhangsan sex:man
name:zhangsan sex:""
调用方法后sex变成空串
不过这个也不算bug,是使用不当导致,如果出现不完整的订单就不能使用这个copyProperties方法覆盖,需要一个一个属性判空set进去
BUG3 插入重复数据
功能上线了一段时间后,在统计导出数据的时候发现有一些订单重复插入了,按理说先select应该不会插入两条。看日志的时候发现两条数据几乎是同时拉取到的,所以在select的时候没有查到,就会插入两条。这里是有两个原因导致这个问题:
1、定时任务拉取的时候调用接口因为网络波动的原因卡住,quartz定时任务默认是并发执行,所以会在同一时间拉取两条一样的数据。
2、在拉取接口没有立刻调用ack确认接口,就会拉到一样的数据。
解决方案:
1、在select之前添加redis分布式锁,保证原子性。
2、在定时任务添加@DisallowConcurrentExecution注解,让定时任务串行执行就不会有并发问题。