今天项目中突然告警报错,打出了多条相似的错误日志。查看了下,具体报错内容如下:
HintManagerHolder has previous value, please clear first.
从错误日志我们可以看出是使用到Sharding-JDBC的相关代码出问题了。而具体出错的业务代码也很容易定位到(这里没有把所有日志贴出来,后面会贴出简化处理过的问题代码)。
不过,分析和解决问题之前,先介绍下背景吧:强哥项目中用Sharding-JDBC主要是为了读写分离。而发生问题的代码,又是因为项目中用到的表和其他人的业务表在一个数据库中。之前有遇到其他业务瞬时数据库写入量过大导致数据库整体主从延迟过久,进而造成我们这边的业务写主库读从库无法读取到数据而失败报错的情况。
但是别人的业务一时半会又没法处理,所以只能我们自己项目上将涉及到比较容易受影响的业务代码进行强制读走主库。
而这次的问题就是因为修改的同学代码没写好出了问题,具体的代码如下:
public void executeTask(String jobId) { //强制查询主库,避免从库延迟导致查不到刚刚插入的任务 ReportRequestJob reportRequestJob; if (status != 1) { //这里也没有报错 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); updateJobStatus(reportRequestJob); } } //下面代码没有报错 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); reportRequestJob = reportRequestJobRepository.findOne(jobId); } //下面代码没有报错 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); reportRequestJob = reportRequestJobRepository.findOne(jobId); }}
updateJobStatus()代码如下:
@Overridepublic void updateJobStatus(ReportRequestJob reportRequestJob) { reportRequestJob.setJobStatus(2); //这里报错了 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); reportRequestJobRepository.save(reportRequestJob); }}
不知道小伙伴们从上面的代码中有没有看出问题,不过不管有没有用过Sharding-JDBC,这样的代码显然不是非常友好。我们看到每次需要对数据库进行走主库操作时,都需要使用HintManager.setMasterRouteOnly()进行手动强制路由,代码重复且容易出错。
那么这次报错的原因又是因为什么呢?我们从最开始的报错信息其实可以看出来:
截图的最后一行告诉我们错误是HintManager.getInstance()导致的。而且是updateJobStatus方法里的HintManager.getInstance()报的错(这里强哥没有把全部错误信息截图进去,小伙伴知道就好)。
那么我们先来看看HintManager.getInstance()方法内部实现:
public static HintManager getInstance() { HintManager result = new HintManager(); HintManagerHolder.setHintManager(result); return result;}
没看出什么,那就到 HintManagerHolder.setHintManager(result);里看看:
public static void setHintManager(final HintManager hintManager) { Preconditions.checkState(null == HINT_MANAGER_HOLDER.get(), "HintManagerHolder has previous value, please clear first."); HINT_MANAGER_HOLDER.set(hintManager);}
我们可以看到,错误信息就是在这个方法里打印出来的。可是为什么第一次获取HintManager时不会报错呢?
这是因为,第一次获取getInstance()时,HINT_MANAGER_HOLDER为空,断言处自然就不会报错。然后走下面代码
HINT_MANAGER_HOLDER.set(hintManager);
HINT_MANAGER_HOLDER被设置了内容。
然而,一个关键的一点,我们看到:executeTask方法中在获取到HintManager后并没有直接操作数据库,而是跳到了updateJobStatus方法。而在updateJobStatus方法中,又再次调用HintManager.getInstance()获取HintManager。而这第二次的调用就是问题的所在。
聪明的小伙伴应该看出来,第二次再调用,HINT_MANAGER_HOLDER已经不是空的了,那么setHintManager方法里的这句断言不成立,就会导致报错。
Preconditions.checkState(null == HINT_MANAGER_HOLDER.get(), "HintManagerHolder has previous value, please clear first.");
至此问题找到了,解决办法相对来说就很简单,直接删除外层获取HintManager.getInstance()的方法,使用内层一次切换就行,或者在每次要切主库前如错误提示所言,加一个clear处理,这样不管使用多少次都不会报错:
//强制读主库if (!HintManagerHolder.isMasterRouteOnly()) { HintManagerHolder.clear(); HintManager hintManager = HintManager.getInstance(); hintManager.setMasterRouteOnly();}
不过,既然解决办法出来了。强哥还想问一个问题:当executeTask方法里的status是1时,为什么没走if,之后连续调用了两次HintManager.getInstance()却都不会报错呢?
限于篇幅,强哥就不多卖关子了,原因就是和Java的try-with-resources有关了。
我们看HintManager的源码
public final class HintManager implements AutoCloseable
可以看到HintManager实现了AutoCloseable 接口,而我们获取HintManager是使用的try-with-resources语法。也就是说try结束会自动调用close方法。HintManager的close方法如下:
@Overridepublic void close() { HintManagerHolder.clear();}
clear方法如下:
public static void clear() { HINT_MANAGER_HOLDER.remove();}
所以在try块结束后,就直接自动把 HINT_MANAGER_HOLDER的内容清空了,下次获取就不会报错了。而如果我们在同一个try中,多次获取HintManager而没有手动clear就会报错。
至此,所有相关的问题就都说清楚啦~
好了,今天就到这了,这个问题也是因为在写代码的过程中,逻辑比较复杂,一没注意就容易掉到坑里。之后小伙伴们有用到Sharding-JDBC强制切主库也要多加注意哦~