一、业务背景
手机端调用本服务A,需要实时获取用户openId,该openId由第三方服务B返回。
二、业务处理
由于服务B返回值的时间不确定,故通过发布订阅的方式,设定一个主题,接收第三方服务返回信息,一旦获取到,则将openId入库。与此同时,手机端还在实时等待openId返回,所以在服务A中设定了一个TimeTask,每个一秒去查询库中是否获取到openId,超过3秒还没结果,则直接报错给手机端。
三、现象
第三方服务早早地已经把结果返回回来了,但是始终没有入库,直到服务A的定时任务超时后,报错给手机端,此时立即完成了openId的入库。也就是入库这个动作似乎一直在等服务A执行完才开始运行,可见大概率是被什么锁定住了。
四、分析
第三方服务和主题订阅中的业务,都涉及同一张表同一条数据的操作,所以大概率是记录被锁定了。
本地尝试重现该问题:用postman充当手机端发送请求,为了有更充裕的时间来分析该现象,所以将定时任务的执行次数设定为1000次,同时新增一个接口模拟订阅方对openId的入库。
准备工作完成,那么先调用定时任务接口,一直等待openId的入库。再调用订阅方的接口,模拟接收到了openId。问题重现!通过查看mysql锁的情况查出结果如下:
可以看出对主键7170这条记录存在锁定的情况。主题接收方一直想去入库,无奈有人抢先拿到了这条记录的锁,可见程序A中有加锁操作。程序A中也要对此条记录进行update,什么时候加锁的呢?
就是这玩意,加上该注解,涉及修改的操作会加上锁,直到程序A的接口执行完,锁才释放。
五、解决方案
去掉注解Transanction即可。服务A接口只涉及到一张表的操作,无须此注解。
如果涉及多表修改要加上注解,因为本业务是对同一张表同一条记录进行update,才会出现此问题。可以新增一张表,一张表是主业务表,一张是类似中间临时表,可以避免同表同记录的操作,最后把中间表的数据读入主表即可(只是举个例子,其实可以存redis或静态容器里)。
也可以不加事务注解,自己写回滚补偿操作(不是很推荐)。
六、补充
最后放上一些可能用得上的mysql锁信息查询语句:
show PROCESSLIST; --查询正在处理中的进程
KILL id(例如:57532165) ;可以将上述查询到的可能是死锁的id杀掉
show OPEN TABLES where In_use > 0; --查询锁定的表
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; --查询innodb引擎下的锁记录
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; ----查询innodb引擎下等待中的锁记录
SELECT VERSION(); --查看数据库服务端版本
SELECT @@global.tx_isolation; --查询事务隔离级别
SELECT * FROM information_schema.engines; --查看引擎类型