MyBatisPuls多數(shù)據(jù)源操作數(shù)據(jù)源偶爾報錯問題
MyBatisPuls多數(shù)據(jù)源操作數(shù)據(jù)源偶爾報錯
昨天同事在開發(fā)一個項目的時候使用了 MybatisPlus 的多數(shù)據(jù)源, 但是在登陸的時候偶然就會報錯 如下 說使用錯庫了
但是刷新幾次有好了 我去看了看這個問題 我當(dāng)時表示十分震驚 debug 了 一個多小時也沒找到錯誤 正當(dāng)我快放棄的時候 我想起了我以前排除過的一個問題 mybatis的 幽靈分頁 (錯誤的使用分頁插件 導(dǎo)致的ThreadLocal 重復(fù)使用的問題)
版本是
<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 在切換數(shù)據(jù)源的時候使用了 ThreadLocal 直覺告訴我 可能是它的問題 但是現(xiàn)在沒有證據(jù) (不能冤枉一個好的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的切換數(shù)據(jù)源工具類
*
* @author TaoYu Kanyuxia
* @since 1.0.0
*/
public final class DynamicDataSourceContextHolder {
/**
* 為什么要用鏈表存儲(準(zhǔn)確的是棧)
* <pre>
* 為了支持嵌套切換,如ABC三個service都是不同的數(shù)據(jù)源
* 其中A的某個業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級一級調(diào)用切換,形成了鏈。
* 傳統(tǒng)的只設(shè)置當(dāng)前線程的方式不能滿足此業(yè)務(wù)需求,必須模擬棧,后進(jìn)先出。
* </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() {
}
/**
* 獲得當(dāng)前線程數(shù)據(jù)源
*
* @return 數(shù)據(jù)源名稱
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 設(shè)置當(dāng)前線程數(shù)據(jù)源
* <p>
* 如非必要不要手動調(diào)用,調(diào)用后確保最終清除
* </p>
*
* @param ds 數(shù)據(jù)源名稱
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空當(dāng)前線程數(shù)據(jù)源
* <p>
* 如果當(dāng)前線程是連續(xù)切換數(shù)據(jù)源 只會移除掉當(dāng)前線程的數(shù)據(jù)源名稱
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 強制清空本地線程
* <p>
* 防止內(nèi)存泄漏,如手動調(diào)用了push可調(diào)用此方法確保清除
* </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的切換數(shù)據(jù)源工具類
*
* @author TaoYu Kanyuxia
* @since 1.0.0
*/
public final class DynamicDataSourceContextHolder {
/**
* 為什么要用鏈表存儲(準(zhǔn)確的是棧)
* <pre>
* 為了支持嵌套切換,如ABC三個service都是不同的數(shù)據(jù)源
* 其中A的某個業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級一級調(diào)用切換,形成了鏈。
* 傳統(tǒng)的只設(shè)置當(dāng)前線程的方式不能滿足此業(yè)務(wù)需求,必須模擬棧,后進(jìn)先出。
* </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() {
}
/**
* 獲得當(dāng)前線程數(shù)據(jù)源
*
* @return 數(shù)據(jù)源名稱
*/
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 當(dāng)前線程 %s 獲得當(dāng)前線程數(shù)據(jù)源 %s 棧針 %s ", format, Thread.currentThread().getName(), peek, JSON.toJSONString(strings));
System.out.println();
return peek;
}
/**
* 設(shè)置當(dāng)前線程數(shù)據(jù)源
* <p>
* 如非必要不要手動調(diào)用,調(diào)用后確保最終清除
* </p>
*
* @param ds 數(shù)據(jù)源名稱
*/
public static void push(String ds) {
Deque<String> strings = LOOKUP_KEY_HOLDER.get();
System.out.printf("時間 %s 當(dāng)前線程 %s 設(shè)置當(dāng)前線程數(shù)據(jù)源 %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 當(dāng)前線程 %s 之后設(shè)置當(dāng)前線程數(shù)據(jù)源 %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();
}
/**
* 清空當(dāng)前線程數(shù)據(jù)源
* <p>
* 如果當(dāng)前線程是連續(xù)切換數(shù)據(jù)源 只會移除掉當(dāng)前線程的數(shù)據(jù)源名稱
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
System.out.printf("時間 %s 清空當(dāng)前線程 %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 當(dāng)前線程 %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>
* 防止內(nèi)存泄漏,如手動調(diào)用了push可調(diào)用此方法確保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
當(dāng)我查看日志的時候 讓我發(fā)現(xiàn)了一個 令我震驚的是 ThreadLocal 維護(hù)的value 竟然兩個線程共享了 震驚!!!

/**
* 設(shè)置當(dāng)前線程數(shù)據(jù)源
* <p>
* 如非必要不要手動調(diào)用,調(diào)用后確保最終清除
* </p>
*
* @param ds 數(shù)據(jù)源名稱
*/
public static void push(String ds) {
Deque<String> strings = LOOKUP_KEY_HOLDER.get();
System.out.printf("時間 %s 當(dāng)前線程 %s 設(shè)置當(dāng)前線程數(shù)據(jù)源 %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 當(dāng)前線程 %s 之后設(shè)置當(dāng)前線程數(shù)據(jù)源 %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();
}
所以出現(xiàn)了上述的查詢數(shù)據(jù)異常 !!!
但是這不符合常理的 ThreadLocal 肯定沒有線程安全問題 我將這個變量修改成 NamedInheritableThreadLocal 改成 ThreadLocal 測試發(fā)現(xiàn)也沒有問題了
我們來看看這個 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 方法的描述很有趣
為這個可繼承的線程局部計算子線程的初始值變量作為父變量在子變量出現(xiàn)時的值的函數(shù)創(chuàng)建線程。
此方法從父類內(nèi)部調(diào)用 子線程啟動之前的線程。
就是子線程可以基礎(chǔ)父線程的 ThreadLocal 中的變量 看到這終于明白了 就是這個的問題了

來仔細(xì)聊聊這個InheritableThreadLocal
首先我們知道ThreadLocal解決的是變量在不同線程間的隔離性,也就是不同線程擁有自己的值。類ThreadLocal的主要作用是將數(shù)據(jù)放入當(dāng)前線程對象中的Map中,類ThreadLocal自己不管理、不存儲任何數(shù)據(jù),它只是數(shù)據(jù)和Map之間的橋梁,Map中的key存儲的是ThreadLocal對象,value就是存儲的值。每個Thread中的Map值只對當(dāng)前線程可見,其他線程不可以訪問當(dāng)前線程對象中Map的值。
當(dāng)前線程銷毀,Map隨之銷毀,Map中的數(shù)據(jù)如果沒有被引用、沒有被使用,則隨時GC收回。由于Map中的key不可以重復(fù),所以一個ThreadLocal對象對應(yīng)一個value。


Thread類中有一個init方法,每次創(chuàng)建線程的時候會執(zhí)行這個init方法,并且inheriThreadLocals默認(rèn)傳的參數(shù)是true,所以當(dāng)前線程對象每次都會從父線程繼承值,子線程將父線程中的table對象以復(fù)制的方式賦值給子線程的table數(shù)組,這個過程是在創(chuàng)建Thread類對象時發(fā)生的,也就說明當(dāng)子線程對象創(chuàng)建完畢后,子線程中的數(shù)據(jù)就是主線程中舊的數(shù)據(jù),主線程使用新的數(shù)據(jù)時,子線程還是使用舊的數(shù)據(jù),因為主子線程使用兩個Entry[]對象數(shù)組各自存儲自己的值。
這個復(fù)制其實一個淺拷貝,如果存的值是可變對象的時候,只是復(fù)制了對象的引用而已,如果父線程修改對象的屬性值,子線程也是可以感知到的。

在我這個問題是 就是 http-nio-82-exec-9 創(chuàng)建了線程http-nio-82-exec-10 導(dǎo)致了這個問題
解決方法
重寫childValue 或者 直接使用 ThreadLocal
/**
* 為什么要用鏈表存儲(準(zhǔn)確的是棧)
* <pre>
* 為了支持嵌套切換,如ABC三個service都是不同的數(shù)據(jù)源
* 其中A的某個業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級一級調(diào)用切換,形成了鏈。
* 傳統(tǒng)的只設(shè)置當(dāng)前線程的方式不能滿足此業(yè)務(wù)需求,必須模擬棧,后進(jìn)先出。
* </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<>();
}
};
最后去看官方文檔

人家修復(fù)了 嗚嗚嗚嗚嗚嗚嗚
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
- Mybatis-plus配置多數(shù)據(jù)源,連接多數(shù)據(jù)庫方式
- MyBatis-Plus多數(shù)據(jù)源的示例代碼
- SpringBoot集成Mybatis實現(xiàn)對多數(shù)據(jù)源訪問原理
- Seata集成Mybatis-Plus解決多數(shù)據(jù)源事務(wù)問題
- 詳解SpringBoot Mybatis如何對接多數(shù)據(jù)源
- Mybatis操作多數(shù)據(jù)源的實現(xiàn)
- 一文搞懂MyBatis多數(shù)據(jù)源Starter實現(xiàn)
- Mybatis-plus多數(shù)據(jù)源配置的兩種方式總結(jié)
- MyBatis-Plus 集成動態(tài)多數(shù)據(jù)源的實現(xiàn)示例
- Mybatis-Plus的多數(shù)據(jù)源你了解嗎
- mybatis-flex實現(xiàn)多數(shù)據(jù)源操作
相關(guān)文章
mybatis和mybatis-plus設(shè)置值為null不起作用問題及解決
Mybatis-Plus的FieldStrategy主要用于控制新增、更新和查詢時對空值的處理策略,通過配置不同的策略類型,可以靈活地處理實體對象的空值問題2025-02-02
mybatis-plus?查詢傳入?yún)?shù)Map,返回List<Map>方式
這篇文章主要介紹了mybatis-plus?查詢傳入?yún)?shù)Map,返回List<Map>方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
jdk8?FunctionalInterface注解源碼解讀
這篇文章主要介紹了jdk8?FunctionalInterface注解源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
Spring boot開發(fā)web應(yīng)用JPA過程解析
這篇文章主要介紹了Spring boot開發(fā)web應(yīng)用JPA過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-09-09
Spring Boot集成MyBatis實現(xiàn)通用Mapper的配置及使用
關(guān)于MyBatis,大部分人都很熟悉。MyBatis 是一款優(yōu)秀的持久層框架,它支持定制化 SQL、存儲過程以及高級映射。這篇文章主要介紹了Spring Boot集成MyBatis實現(xiàn)通用Mapper,需要的朋友可以參考下2018-08-08

