MyBatisPuls多数据源操作数据源偶尔报错问题
作者:Q z1997
MyBatisPuls多数据源操作数据源偶尔报错
昨天同事在开发一个项目的时候使用了 MybatisPlus 的多数据源, 但是在登陆的时候偶然就会报错 如下 说使用错库了
但是刷新几次有好了 我去看了看这个问题 我当时表示十分震惊 debug 了 一个多小时也没找到错误 正当我快放弃的时候 我想起了我以前排除过的一个问题 mybatis的 幽灵分页 (错误的使用分页插件 导致的ThreadLocal 重复使用的问题)
版本是
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.1.0</version> </dependency>
org.springframework.jdbc.BadSqlGrammarException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Table 'constdatacenterx_company.sys_dict_type' doesn't exist
### The error may exist in vip/xiaonuo/sys/modular/dict/mapper/SysDictTypeMapper.java (best guess)
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT id,name,code,sort,remark,status,create_time,create_user,update_time,update_user FROM sys_dict_type WHERE (status <> ?)
### Cause: java.sql.SQLSyntaxErrorException: Table 'constdatacenterx_company.sys_dict_type' doesn't exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'constdatacenterx_company.sys_dict_type' doesn't exist
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:235)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:88)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
at com.sun.proxy.$Proxy124.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:223)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:173)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:78)
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.$Proxy353.selectList(Unknown Source)
at com.baomidou.mybatisplus.extension.service.IService.list(IService.java:279)
at vip.xiaonuo.sys.modular.dict.service.impl.SysDictTypeServiceImpl.tree(SysDictTypeServiceImpl.java:199)
at vip.xiaonuo.sys.modular.dict.service.impl.SysDictTypeServiceImpl$$FastClassBySpringCGLIB$$5dedd210.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor.invoke(DynamicDataSourceAnnotationInterceptor.java:50)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at vip.xiaonuo.sys.modular.dict.service.impl.SysDictTypeServiceImpl$$EnhancerBySpringCGLIB$$8ee9f21c.tree(<generated>)
at vip.xiaonuo.sys.modular.dict.controller.SysDictTypeController.tree(SysDictTypeController.java:170)
虽然一个多小时没有找到问题的原因 看到了mybatis plus 在切换数据源的时候使用了 ThreadLocal 直觉告诉我 可能是它的问题 但是现在没有证据 (不能冤枉一个好的ThreadLocal)
/** * Copyright © 2018 organization baomidou * <pre> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * <pre/> */ package com.baomidou.dynamic.datasource.toolkit; import java.util.ArrayDeque; import java.util.Deque; import org.springframework.core.NamedInheritableThreadLocal; import org.springframework.util.StringUtils; /** * 核心基于ThreadLocal的切换数据源工具类 * * @author TaoYu Kanyuxia * @since 1.0.0 */ public final class DynamicDataSourceContextHolder { /** * 为什么要用链表存储(准确的是栈) * <pre> * 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。 * </pre> */ @SuppressWarnings("unchecked") private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal("dynamic-datasource") { @Override protected Object initialValue() { return new ArrayDeque(); } }; private DynamicDataSourceContextHolder() { } /** * 获得当前线程数据源 * * @return 数据源名称 */ public static String peek() { return LOOKUP_KEY_HOLDER.get().peek(); } /** * 设置当前线程数据源 * <p> * 如非必要不要手动调用,调用后确保最终清除 * </p> * * @param ds 数据源名称 */ public static void push(String ds) { LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds); } /** * 清空当前线程数据源 * <p> * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 * </p> */ public static void poll() { Deque<String> deque = LOOKUP_KEY_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } /** * 强制清空本地线程 * <p> * 防止内存泄漏,如手动调用了push可调用此方法确保清除 * </p> */ public static void clear() { LOOKUP_KEY_HOLDER.remove(); } }
最后修改他的源码 增加日志
/** * Copyright © 2018 organization baomidou * <pre> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * <pre/> */ package com.baomidou.dynamic.datasource.toolkit; import cn.hutool.core.date.LocalDateTimeUtil; import com.alibaba.fastjson.JSON; import org.springframework.core.NamedInheritableThreadLocal; import org.springframework.util.StringUtils; import java.util.ArrayDeque; import java.util.Deque; /** * 核心基于ThreadLocal的切换数据源工具类 * * @author TaoYu Kanyuxia * @since 1.0.0 */ public final class DynamicDataSourceContextHolder { /** * 为什么要用链表存储(准确的是栈) * <pre> * 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。 * </pre> */ private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal<Deque<String>>("dynamic-datasource") { // @Override // protected Deque<String> childValue(Deque<String> parentValue) { // return new ArrayDeque<>(); // } @Override protected ArrayDeque<String> initialValue() { return new ArrayDeque<>(); } }; private DynamicDataSourceContextHolder() { } /** * 获得当前线程数据源 * * @return 数据源名称 */ public static String peek() { Deque<String> strings = LOOKUP_KEY_HOLDER.get(); String peek = strings.peek(); String format = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"); System.out.printf("时间 %s 当前线程 %s 获得当前线程数据源 %s 栈针 %s ", format, Thread.currentThread().getName(), peek, JSON.toJSONString(strings)); System.out.println(); return peek; } /** * 设置当前线程数据源 * <p> * 如非必要不要手动调用,调用后确保最终清除 * </p> * * @param ds 数据源名称 */ public static void push(String ds) { Deque<String> strings = LOOKUP_KEY_HOLDER.get(); System.out.printf("时间 %s 当前线程 %s 设置当前线程数据源 %s 栈针 %s 引用 %s", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"), Thread.currentThread().getName(), ds, JSON.toJSONString(strings), strings.hashCode()); System.out.println(); strings.push(StringUtils.isEmpty(ds) ? "" : ds); System.out.printf("时间 %s 当前线程 %s 之后设置当前线程数据源 %s 栈针 %s 引用 %s", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"), Thread.currentThread().getName(), ds, JSON.toJSONString(strings), strings.hashCode()); System.out.println(); } /** * 清空当前线程数据源 * <p> * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 * </p> */ public static void poll() { Deque<String> deque = LOOKUP_KEY_HOLDER.get(); System.out.printf("时间 %s 清空当前线程 %s 栈针 %s ", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"), Thread.currentThread().getName(), JSON.toJSONString(deque)); System.out.println(); String poll = deque.poll(); System.out.printf("时间 %s 当前线程 %s 清空 %s 栈针 %s ", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"), Thread.currentThread().getName(), poll, JSON.toJSONString(deque)); System.out.println(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } /** * 强制清空本地线程 * <p> * 防止内存泄漏,如手动调用了push可调用此方法确保清除 * </p> */ public static void clear() { LOOKUP_KEY_HOLDER.remove(); } }
当我查看日志的时候 让我发现了一个 令我震惊的是 ThreadLocal 维护的value 竟然两个线程共享了 震惊!!!
/** * 设置当前线程数据源 * <p> * 如非必要不要手动调用,调用后确保最终清除 * </p> * * @param ds 数据源名称 */ public static void push(String ds) { Deque<String> strings = LOOKUP_KEY_HOLDER.get(); System.out.printf("时间 %s 当前线程 %s 设置当前线程数据源 %s 栈针 %s 引用 %s", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"), Thread.currentThread().getName(), ds, JSON.toJSONString(strings), strings.hashCode()); System.out.println(); strings.push(StringUtils.isEmpty(ds) ? "" : ds); System.out.printf("时间 %s 当前线程 %s 之后设置当前线程数据源 %s 栈针 %s 引用 %s", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyy-MM-dd HH:mm:ss:SSS"), Thread.currentThread().getName(), ds, JSON.toJSONString(strings), strings.hashCode()); System.out.println(); }
所以出现了上述的查询数据异常 !!!
但是这不符合常理的 ThreadLocal 肯定没有线程安全问题 我将这个变量修改成 NamedInheritableThreadLocal 改成 ThreadLocal 测试发现也没有问题了
我们来看看这个 NamedInheritableThreadLocal 类吧
package org.springframework.core; import org.springframework.util.Assert; public class NamedInheritableThreadLocal<T> extends InheritableThreadLocal<T> { private final String name; public NamedInheritableThreadLocal(String name) { Assert.hasText(name, "Name must not be empty"); this.name = name; } public String toString() { return this.name; } }
这个childValue 方法的描述很有趣
为这个可继承的线程局部计算子线程的初始值变量作为父变量在子变量出现时的值的函数创建线程。
此方法从父类内部调用 子线程启动之前的线程。
就是子线程可以基础父线程的 ThreadLocal 中的变量 看到这终于明白了 就是这个的问题了
来仔细聊聊这个InheritableThreadLocal
首先我们知道ThreadLocal解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值。类ThreadLocal的主要作用是将数据放入当前线程对象中的Map中,类ThreadLocal自己不管理、不存储任何数据,它只是数据和Map之间的桥梁,Map中的key存储的是ThreadLocal对象,value就是存储的值。每个Thread中的Map值只对当前线程可见,其他线程不可以访问当前线程对象中Map的值。
当前线程销毁,Map随之销毁,Map中的数据如果没有被引用、没有被使用,则随时GC收回。由于Map中的key不可以重复,所以一个ThreadLocal对象对应一个value。
Thread类中有一个init方法,每次创建线程的时候会执行这个init方法,并且inheriThreadLocals默认传的参数是true,所以当前线程对象每次都会从父线程继承值,子线程将父线程中的table对象以复制的方式赋值给子线程的table数组,这个过程是在创建Thread类对象时发生的,也就说明当子线程对象创建完毕后,子线程中的数据就是主线程中旧的数据,主线程使用新的数据时,子线程还是使用旧的数据,因为主子线程使用两个Entry[]对象数组各自存储自己的值。
这个复制其实一个浅拷贝,如果存的值是可变对象的时候,只是复制了对象的引用而已,如果父线程修改对象的属性值,子线程也是可以感知到的。
在我这个问题是 就是 http-nio-82-exec-9
创建了线程http-nio-82-exec-10
导致了这个问题
解决方法
重写childValue
或者 直接使用 ThreadLocal
/** * 为什么要用链表存储(准确的是栈) * <pre> * 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。 * </pre> */ private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal<Deque<String>>("dynamic-datasource") { @Override protected Deque<String> childValue(Deque<String> parentValue) { return new ArrayDeque<>(); } @Override protected ArrayDeque<String> initialValue() { return new ArrayDeque<>(); } };
最后去看官方文档
人家修复了 呜呜呜呜呜呜呜
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。