MyBatisPuls多數據源操作數據源偶爾報錯問題
MyBatisPuls多數據源操作數據源偶爾報錯
昨天同事在開發(fā)一個項目的時候使用了 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 直覺告訴我 可能是它的問題 但是現(xiàn)在沒有證據 (不能冤枉一個好的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的某個業(yè)務要調B的方法,B的方法需要調用C的方法。一級一級調用切換,形成了鏈。
* 傳統(tǒng)的只設置當前線程的方式不能滿足此業(yè)務需求,必須模擬棧,后進先出。
* </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>
* 如果當前線程是連續(xù)切換數據源 只會移除掉當前線程的數據源名稱
* </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的某個業(yè)務要調B的方法,B的方法需要調用C的方法。一級一級調用切換,形成了鏈。
* 傳統(tǒng)的只設置當前線程的方式不能滿足此業(yè)務需求,必須模擬棧,后進先出。
* </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>
* 如果當前線程是連續(xù)切換數據源 只會移除掉當前線程的數據源名稱
* </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();
}
}
當我查看日志的時候 讓我發(fā)現(xiàn)了一個 令我震驚的是 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();
}
所以出現(xiàn)了上述的查詢數據異常 !!!
但是這不符合常理的 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)時的值的函數創(chuàng)建線程。
此方法從父類內部調用 子線程啟動之前的線程。
就是子線程可以基礎父線程的 ThreadLocal 中的變量 看到這終于明白了 就是這個的問題了

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


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

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

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

