昨天同事在开发一个项目的时候使用了 MybatisPlus 的多数据源, 但是在登陆的时候偶然就会报错 如下 说使用错库了
但是刷新几次有好了 我去看了看这个问题 我当时表示十分震惊 debug 了 一个多小时也没找到错误 正当我快放弃的时候 我想起了我以前排除过的一个问题 mybatis的 幽灵分页 (错误的使用分页插件 导致的ThreadLocal 重复使用的问题)
版本是
com.baomidou dynamic-datasource-spring-boot-starter 3.1.0
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() 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( ) at vip.xiaonuo.sys.modular.dict.controller.SysDictTypeController.tree(SysDictTypeController.java:170)
虽然一个多小时没有找到问题的原因 看到了mybatis plus 在切换数据源的时候使用了 ThreadLocal 直觉告诉我 可能是它的问题 但是现在没有证据 (不能冤枉一个好的ThreadLocal)
package com.baomidou.dynamic.datasource.toolkit;
import java.util.ArrayDeque;
import java.util.Deque;
import org.springframework.core.NamedInheritableThreadLocal;
import org.springframework.util.StringUtils;
public final class DynamicDataSourceContextHolder {
@SuppressWarnings("unchecked")
private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal("dynamic-datasource") {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
private DynamicDataSourceContextHolder() {
}
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
public static void poll() {
Deque deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
最后修改他的源码 增加日志
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;
public final class DynamicDataSourceContextHolder {
private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal>("dynamic-datasource") {
// @Override
// protected Deque childValue(Deque parentValue) {
// return new ArrayDeque<>();
// }
@Override
protected ArrayDeque initialValue() {
return new ArrayDeque<>();
}
};
private DynamicDataSourceContextHolder() {
}
public static String peek() {
Deque 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;
}
public static void push(String ds) {
Deque 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();
}
public static void poll() {
Deque 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();
}
}
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
当我查看日志的时候 让我发现了一个 令我震惊的是 ThreadLocal 维护的value 竟然两个线程共享了 震惊!!!
http-nio-82-exec-9 http-nio-82-exec-10 这两个线程的value的hashCode 竟然一样
public static void push(String ds) {
Deque 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 NamedInheritableThreadLocalextends InheritableThreadLocal { 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; } }
但是这个类 基本没事改动 就是加了个name 改了个构造方法 但是他的父类 是InheritableThreadLocal 而不是 ThreadLocal
那我们来看看 InheritableThreadLocal 类吧
package java.lang; import java.lang.ref.*; public class InheritableThreadLocalextends ThreadLocal { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
这个 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
private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal>("dynamic-datasource") {
@Override
protected Deque childValue(Deque parentValue) {
return new ArrayDeque<>();
}
@Override
protected ArrayDeque initialValue() {
return new ArrayDeque<>();
}
};
最后去看官方文档
人家修复了 呜呜呜呜呜呜呜



