单线程业务升级为多线程的坎坷之路

优化业务流程 ??公司的一个老项目, 主要负责给商场生成各个商户的每月账单(比如租金, 管理费)之类的. 今天产品经理过来说, 月初的生成账单的定时任务要跑接近六个小时, 从上午五点一直跑到11点才结束. 这期间,商场的财务们无法对账单进行操作, 因为操作之后的账单可能会出现问题. 因此, 希望优化一下. 一直负责维护这项目的我自热而然的也就包揽下了这个任务.
发现问题 ??我倒要看看是什么定时任务要跑六个小时. 经过一番查看, 发现定时任务中,大概有接近一千家商户, 也就是要生成接近一千个账单. 定时任务采用一个for循环遍历商户, 一家商户一家商户的生成账单, 生成账单的业务逻辑也挺复杂的,耗时也不短. 只有一个线程跑, 也难怪会这么慢了.
加入线程池 ??经过一番考量, 发现各个商户之间生成账单的流程是不冲突的, 于是我毅然选择了使用线程池. 我开启八个线程跑, 速度最少也提升四五倍, 也就是说, 五点开始跑, 六点就能跑完. 只要能在九点之前(财务上班之前)跑完, 就算完成任务. 这不是赶赶单单. 然而, 事情却远没我想象的那么简单.
??在修改成线程池之后, 第二天我兴高采烈的打算看’'优化’后的结果. 查看日志, 定时任务是在六点钟左右的时候执行结束了. 但是查看账单, 嗯? 原先的一千个账单, 怎么只生成了两百个? 不对劲.
线程池任务被抛弃? ??最开始, 我怀疑可能是生成账单的任务被抛弃了, 导致没有执行全部的任务. 于是, 我在执行业务逻辑的run方法中设置了线程的名称.
单线程业务升级为多线程的坎坷之路
文章图片

将线程名称设置成为各个商户的合同号, 当任务被执行了, 打印的线程名称就是当前任务设置的线程名称.
更改后, 发现任务都被执行了. 也就是说, 原因不是这个.
??于是, 我本地起了项目, 看看问题出在哪里.
数据库死锁 单线程业务升级为多线程的坎坷之路
文章图片

??从日志中看到, 在更新表数据的时候出现了死锁. 出现死锁, 也就意味着有一部分线程无法进行业务逻辑, 这样的话 , 账单自然而然的就比原来的少了
??于是, 我利用SHOW ENGINE INNODB STATUS语句, 该语句可以打印出mysql的死锁日志信息.
查看死锁日志
单线程业务升级为多线程的坎坷之路
文章图片

以上的两个语句就是导致数据库死锁的罪魁祸首. 那么, 问题来了, 两个update语句都根据id更新语句, id值都不一样, 为什么会造成死锁呢? 难道id列没有索引?
??查看数据库表信息, 发现id列果然没有索引, 那么,id列没有索引为什么又会导致两个更新语句出现死锁呢?
??原来, 当更新条件没有索引时, 数据库会锁住整张表. 一个语句把表锁了, 另一个语句无法获取锁来更新数据, 从而导致了死锁. 如果更新条件有索引时, 执行更新语句时只会锁住对应的索引列. 于是, 我给id列加上唯一索引. 再次执行任务, 果然, 没有再出现死锁的情况.
??当我再次自信满满的查看修改后的成果的时候, 发现账单确实是比原来的两百多个多了不少, 但是, 只有八百多个, 距离原来的一千个账单还是差了一百多个. 这又是什么情况??? 怎么还少账单???
??没办法, 只能继续排查问题了.
账单编号重复 ??再次查看日志, 发现生成的账单号有重复的情况, 因为数据库中, 账单编号是唯一的, 因此,当账单编号相同时, 由于主键冲突, 是无法保存的.
??于是我查看代码,看看账单编号是怎么生成的
单线程业务升级为多线程的坎坷之路
文章图片

单线程业务升级为多线程的坎坷之路
文章图片

??查看代码, 发现账单编号是根据时间戳来生成的, 根据时间戳来生成, 在单线程的情况下, 没什么问题, 但是在并发的情况下, 会存在多个线程在相同时间生成账单编号的情况, 这种情况下. 生成的账单编号就是一样的了. 于是, 我换了一个线程安全的方式生成账单编号.
??再次查看结果, 发现账单数量总算是对上了.
??当我又准备享受胜利的喜悦的时候, 对了对账单的数据, 发现大部分都对不上. wtf??? 这又是什么情况???
线程不安全的成员变量 ??再次查看代码, 从代码中找原因. 经过一个小时的仔细观察, 略微发现了端倪.
单线程业务升级为多线程的坎坷之路
文章图片


??以前写代码的老哥, 在生成账单的类中, 账单ID保存在成员变量中(没想明白, 为什么要这么保存), 那么, 在并发的情况下, 就有可能在存账单ID的值时, 账单ID已经被其他的线程修改了. 也就是说, 保存到数据库中的账单ID有可能是错误的账单ID. 那么, 也就导致了账单数据不对的问题.
??于是, 我将账单ID设置成了方法中的局部变量, 这样就不存在线程安全的问题了
??再次, 执行定时任务, 终于, 账单基本上都能对上了.并且, 经过后续的一些逻辑的优化, 最终, 这个定时任务在三十分钟内就执行完成了. 圆满完成任务(暂时的??)
总结 【单线程业务升级为多线程的坎坷之路】??经过这次单线程任务升级为多线程任务的优化, 也是让我踩了不少坑. 也让我意识到, 在写业务的时候, 要时刻考虑并发的情况, 这样, 才不会在以后改成多线程环境时出现各种奇奇怪怪的问题.

    推荐阅读