目录
1、场景及问题
2、数据库及各框架版本信息
3、错误回放
4、根因分析及确认
5、解决问题
6、总结
1、场景及问题
场景:
在调用腾讯位置服务时有用到key值,因为每个key值都有自己的额度,所以在表里存了多个key,简称key池;
并发场景下当从key池获取可用的key并更新额度时,行锁(for update)也就尤为重要;
高并发场景下 SKIP LOCKED对于资源竞争特别有用,减少事务等待(阻塞)从而提高并发性能;
问题:
Postgres中直接使用 for update SKIP LOCKED 是没问题的,然而在mybatisplus中却报错
2、数据库及各框架版本信息
是在开源框架jeecgboot微服务版中遇到了该问题,其本质仍是该框架引用mybatisplus及其依赖版本太低导致的。
- Postgres:PostgreSQL 12.17 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 7.3.1 20180712 (Red Hat 7.3.1-12), 64-bit
- jeecgboot:3.6.3
- mybatisplus: 3.5.3.1
-
jsqlparser:4.4
3、错误回放
报错的Sql:
<--获取一个未被锁定的可用key-->
<select id="getOptimalNumber" resultType="org.jeecg.modules.**.entity.MdmNumber">
select id, mdm_number.user, password, token, inquire_day
from primary_mdm.mdm_number
where type = #{type}
and (inquire_time < #{inquireTime} or inquire_time is null)
and inquire_day < #{inquireDay}
and status = '1'
ORDER BY inquire_day ASC LIMIT 1 for
update SKIP LOCKED
</select>
报错堆栈:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select id, mdm_number.user, password, token, inquire_day
from primary_mdm.mdm_number
where type = ?
and (inquire_time < ? or inquire_time is null)
and inquire_day < ?
and status = '1'
ORDER BY inquire_day ASC LIMIT 1 for
update SKIP LOCKED
### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select id, mdm_number.user, password, token, inquire_day
from primary_mdm.mdm_number
where type = ?
and (inquire_time < ? or inquire_time is null)
and inquire_day < ?
and status = '1'
ORDER BY inquire_day ASC LIMIT 1 for
update SKIP LOCKED
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
at com.sun.proxy.$Proxy168.selectOne(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:160)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:89)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89)
at com.sun.proxy.$Proxy169.getOptimalNumber(Unknown Source)
at org.jeecg.modules.odm.number.service.MdmNumberService.getOptimalNumber(MdmNumberService.java:69)
at org.jeecg.modules.odm.number.service.MdmNumberService$$FastClassBySpringCGLIB$$7fcd0ad.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy.invokeMethod(CglibAopProxy.java:386)
at org.springframework.aop.framework.CglibAopProxy.access$000(CglibAopProxy.java:85)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:703)
at org.jeecg.modules.odm.number.service.MdmNumberService$$EnhancerBySpringCGLIB$$7d736354.getOptimalNumber(<generated>)
at org.jeecg.modules.odm.hco.service.MdmWjwHcoService.initLocation(MdmWjwHcoService.java:42)
at org.jeecg.modules.odm.hco.service.MdmWjwHcoService$$FastClassBySpringCGLIB$$3de5fea9.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:792)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:707)
at org.jeecg.modules.odm.hco.service.MdmWjwHcoService$$EnhancerBySpringCGLIB$$3ced8ac8.initLocation(<generated>)
at org.jeecg.modules.xxljobtask.OdmJobHandler.retry(OdmJobHandler.java:49)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.xxl.job.core.handler.impl.MethodJobHandler.execute(MethodJobHandler.java:29)
at com.xxl.job.core.thread.JobThread.run(JobThread.java:152)
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select id, mdm_number.user, password, token, inquire_day
from primary_mdm.mdm_number
where type = ?
and (inquire_time < ? or inquire_time is null)
and inquire_day < ?
and status = '1'
ORDER BY inquire_day ASC LIMIT 1 for
update SKIP LOCKED
### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select id, mdm_number.user, password, token, inquire_day
from primary_mdm.mdm_number
where type = ?
and (inquire_time < ? or inquire_time is null)
and inquire_day < ?
and status = '1'
ORDER BY inquire_day ASC LIMIT 1 for
update SKIP LOCKED
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427)
... 33 more
Caused by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Failed to process, Error SQL: select id, mdm_number.user, password, token, inquire_day
from primary_mdm.mdm_number
where type = ?
and (inquire_time < ? or inquire_time is null)
and inquire_day < ?
and status = '1'
ORDER BY inquire_day ASC LIMIT 1 for
update SKIP LOCKED
at com.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:39)
at com.baomidou.mybatisplus.extension.parser.JsqlParserSupport.parserSingle(JsqlParserSupport.java:52)
at com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.beforeQuery(TenantLineInnerInterceptor.java:68)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:78)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62)
at com.sun.proxy.$Proxy266.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
... 41 more
Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "SKIP" "SKIP"
at line 8, column 16.
Was expecting one of:
";"
"NOWAIT"
"OF"
<EOF>
at net.sf.jsqlparser.parser.CCJSqlParser.generateParseException(CCJSqlParser.java:31468)
at net.sf.jsqlparser.parser.CCJSqlParser.jj_consume_token(CCJSqlParser.java:31301)
at net.sf.jsqlparser.parser.CCJSqlParser.Statement(CCJSqlParser.java:163)
at net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatement(CCJSqlParserUtil.java:188)
at net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(CCJSqlParserUtil.java:63)
at net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(CCJSqlParserUtil.java:38)
at com.baomidou.mybatisplus.extension.parser.JsqlParserSupport.parserSingle(JsqlParserSupport.java:49)
... 46 more
4、根因分析及确认
在只用 for update 时运行是正常的,加上SKIP LOCKED 就异常了,所以问题就出于此;
由于SKIP LOCKED 在Postgres里执行是正常的,所以不存在其版本不支持问题;
仔细看报错堆栈不难发现,Was expecting one of 后面有个NOWAIT ,猜测大概率是jsqlparser 不认SKIP LOCKED导致的。
下面就该求证这个猜测了
首先我们去jsqlparser 的github 去看其各Releases的更新,查找SKIP LOCKED后确认之前猜测是正确的;
我们再去看看mybatisplus官网有没有更新过这个组件,还真有:
最后看下项目中引用,jsqlparser 4.4 肯定是有问题的了。
5、解决问题
如果你觉得只需要把mybatisplus升级成3.5.6或者把jsqlparser升级成4.6(更激进的去升级jeecgboot),将会发现有些代码编译报错(运行时更是暗礁满满)。
解决办法总是多次实践,进而权衡风险和工作量等因素后给出,这也是当下任何AI 都无法做到的。
最终解决如下:
升级mybatisplus到3.5.3.2并排除其jsqlparser引用,最后引用jsqlparser 4.6。
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.6</version>
</dependency>
当然本次升级并非全无改动,有个ConstAnalyzer.java 有俩接口要实现,这并不影响业务运行,如图:
6、总结
只要是组件或框架的调整,少不了到处多测试下,稳定优先;
如果不升级组件有没有办法实现类似效果呢?其实可以在获取一个可用key时不加锁,获取到后在加行锁(for update NOWAIT),反正4.4版本看样子是支持NOWAIT的。当然最终还是要看实测效果的。