浅析Jeecgboot中mybatisplus不支持Postgres SKIP LOCKED语法问题

目录

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 &lt; #{inquireTime} or inquire_time is null)
          and inquire_day &lt; #{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的。当然最终还是要看实测效果的。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值