¶性能优化-解决MyBatis高并发下OGNL反射调用方法的锁竞争
摘要: 继解决OGNL安全属性检查的性能问题后,线上服务再次因MyBatis查询阻塞而超时。分析发现,近千个线程阻塞在OgnlRuntime.invokeMethod方法,竞争同一个Method对象的锁。根本原因在于,MyBatis-Plus动态SQL在解析时,会高并发地通过反射调用Wrapper对象的固定几个Getter方法(如getSqlSegment)。OGNL为缓存每个方法的访问权限,在首次检查时使用了synchronized(method),导致严重锁竞争。解决方案是为其缓存机制引入“双重检查锁定”优化,避免后续调用仍需同步,从而根治此瓶颈。该修复已向OGNL社区提交并已于3.4.10版本发布。
¶背景
经过上一篇文章,性能优化-巧解MyBatis高并发下OGNL安全检查导致的全局锁瓶颈.md,我们成功解决了OGNL高并发调用System.getProperty("ognl.security.manager")导致的性能瓶颈。
但好景不长,没过几天线上环境又频繁报接口响应超时,很多节点同时报[HSF-Provider] HSF thread pool is full.,dump线程信息进行分析,发现大部分线程还是卡在mybatis查询数据库,这又是为什么呢?
¶问题排查过程
¶程堆栈分析
于是我们又dump线程信息进行分析,结果还是OgnlRuntime这个类导致,不过这次有981个线程阻塞在org.apache.ibatis.ognl.OgnlRuntime.invokeMethod(OgnlRuntime.java:1151),具体线程堆栈信息如下:
1 | # 980个线程被阻塞 |
¶源码分析
从上面的线程堆栈信息可以看到所有的线程都阻塞在org.apache.ibatis.ognl.OgnlRuntime.invokeMethod(OgnlRuntime.java:1151),看看源码这里在干啥呢:
1 |
|
完整源码请查看:https://github.com/orphan-oss/ognl/blob/OGNL_3_3_0/src/main/java/ognl/OgnlRuntime.java#L1151
我们项目mybatis的版本是3.5.9,对应的ognl版本是3.3.0,从
/mybatis-3.5.9.jar!/META-INF/maven/ognl/ognl/pom.xml中可以看到对应的版本,mybatis是将ognl的class直接构建到了mybatis的jar包中了。
从上面的源码分析结合堆栈分析,所有的线程都阻塞在1151行synchronized(method) {,等待获取method的访问权限和执行权限,证明有大量线程要去调用同一个method对象,这是为什么呢?
¶问题剖析
为什么有大量线程在mybatis的查询中都在反射调用相同的方法呢,这就要从MyBatis-Plus说起了,项目使用的是MyBatis-Plus做动态SQL拼装,例如下面的动态SQL:
1 | LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); |
实际上调用的是com.baomidou.mybatisplus.core.mapper.BaseMapper.selectList方法,其动态SQL的XML如下:
1 |
|
其中
BaseMapper.selectList方法的SQL脚本是由com.baomidou.mybatisplus.core.injector.methods.SelectList注入的。
SQL注入器的详细文档请参考:https://baomidou.com/guides/sql-injector/
从上面的动态SQL分析可以发现,每一个动态SQL在mybatis解析OGNL表达式时都必然会通过反射获取Wrapper对象这些属性的值:sqlFirst、sqlSelect、sqlSegment、nonEmptyOfEntity、sqlComment。再高并发的时候,都会通过反射调用这些属性对应的get方法获取这些属性的值,那么synchronized(method)锁的都是相同的方法也就不奇怪了。
¶解决方案
从上面的线程堆栈以及源码分析,我们知道了问题产生的原因,那么我们将synchronized(method)锁的的竞争降低就可以解决问题,于是我们可以这样修改Ognl的源码,给method加上Double null ckeck:
1 | public static Object invokeMethod(Object target, Method method, Object[] argsArray) |
上面是基于https://github.com/orphan-oss/ognl最新的源码修改,和本文项目中用到的
3.3.0版本有细微差异,但问题也同样存在。
经过如上修改OgnlRuntime#invokeMethod源码升级项目后,再也没出现过OgnlRuntime.invokeMethod阻塞的问题了。
具体的issue和Pull request请查看:
- https://github.com/mybatis/mybatis-3/issues/3589
- https://github.com/orphan-oss/ognl/pull/521
OGNL社区已采纳,已合并到3.4.10版本发布。
参考资料:
-
OGNL 源码:https://github.com/orphan-oss/ognl/blob/OGNL_3_3_0/src/main/java/ognl/OgnlRuntime.java#L1151
-
MyBatis-Plus 官方文档(SQL注入器):https://baomidou.com/guides/sql-injector/
-
向MyBatis社区提交的Issue:https://github.com/mybatis/mybatis-3/issues/3589
-
向OGNL社区提交的Pull Request:https://github.com/orphan-oss/ognl/pull/521