MyBatisPuls多數(shù)據(jù)源操作數(shù)據(jù)源偶爾報(bào)錯(cuò)問(wèn)題
MyBatisPuls多數(shù)據(jù)源操作數(shù)據(jù)源偶爾報(bào)錯(cuò)
昨天同事在開(kāi)發(fā)一個(gè)項(xiàng)目的時(shí)候使用了 MybatisPlus 的多數(shù)據(jù)源, 但是在登陸的時(shí)候偶然就會(huì)報(bào)錯(cuò) 如下 說(shuō)使用錯(cuò)庫(kù)了
但是刷新幾次有好了 我去看了看這個(gè)問(wèn)題 我當(dāng)時(shí)表示十分震驚 debug 了 一個(gè)多小時(shí)也沒(méi)找到錯(cuò)誤 正當(dāng)我快放棄的時(shí)候 我想起了我以前排除過(guò)的一個(gè)問(wèn)題 mybatis的 幽靈分頁(yè) (錯(cuò)誤的使用分頁(yè)插件 導(dǎo)致的ThreadLocal 重復(fù)使用的問(wèn)題)
版本是
<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)
雖然一個(gè)多小時(shí)沒(méi)有找到問(wèn)題的原因 看到了mybatis plus 在切換數(shù)據(jù)源的時(shí)候使用了 ThreadLocal 直覺(jué)告訴我 可能是它的問(wèn)題 但是現(xiàn)在沒(méi)有證據(jù) (不能冤枉一個(gè)好的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ù)源工具類(lèi) * * @author TaoYu Kanyuxia * @since 1.0.0 */ public final class DynamicDataSourceContextHolder { /** * 為什么要用鏈表存儲(chǔ)(準(zhǔn)確的是棧) * <pre> * 為了支持嵌套切換,如ABC三個(gè)service都是不同的數(shù)據(jù)源 * 其中A的某個(gè)業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級(jí)一級(jí)調(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ù)源名稱(chēng) */ public static String peek() { return LOOKUP_KEY_HOLDER.get().peek(); } /** * 設(shè)置當(dāng)前線程數(shù)據(jù)源 * <p> * 如非必要不要手動(dòng)調(diào)用,調(diào)用后確保最終清除 * </p> * * @param ds 數(shù)據(jù)源名稱(chēng) */ 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ù)源 只會(huì)移除掉當(dāng)前線程的數(shù)據(jù)源名稱(chēng) * </p> */ public static void poll() { Deque<String> deque = LOOKUP_KEY_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } /** * 強(qiáng)制清空本地線程 * <p> * 防止內(nèi)存泄漏,如手動(dòng)調(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ù)源工具類(lèi) * * @author TaoYu Kanyuxia * @since 1.0.0 */ public final class DynamicDataSourceContextHolder { /** * 為什么要用鏈表存儲(chǔ)(準(zhǔn)確的是棧) * <pre> * 為了支持嵌套切換,如ABC三個(gè)service都是不同的數(shù)據(jù)源 * 其中A的某個(gè)業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級(jí)一級(jí)調(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ù)源名稱(chēng) */ 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("時(shí)間 %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> * 如非必要不要手動(dòng)調(diào)用,調(diào)用后確保最終清除 * </p> * * @param ds 數(shù)據(jù)源名稱(chēng) */ public static void push(String ds) { Deque<String> strings = LOOKUP_KEY_HOLDER.get(); System.out.printf("時(shí)間 %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("時(shí)間 %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ù)源 只會(huì)移除掉當(dāng)前線程的數(shù)據(jù)源名稱(chēng) * </p> */ public static void poll() { Deque<String> deque = LOOKUP_KEY_HOLDER.get(); System.out.printf("時(shí)間 %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("時(shí)間 %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(); } } /** * 強(qiáng)制清空本地線程 * <p> * 防止內(nèi)存泄漏,如手動(dòng)調(diào)用了push可調(diào)用此方法確保清除 * </p> */ public static void clear() { LOOKUP_KEY_HOLDER.remove(); } }
當(dāng)我查看日志的時(shí)候 讓我發(fā)現(xiàn)了一個(gè) 令我震驚的是 ThreadLocal 維護(hù)的value 竟然兩個(gè)線程共享了 震驚!!!
/** * 設(shè)置當(dāng)前線程數(shù)據(jù)源 * <p> * 如非必要不要手動(dòng)調(diào)用,調(diào)用后確保最終清除 * </p> * * @param ds 數(shù)據(jù)源名稱(chēng) */ public static void push(String ds) { Deque<String> strings = LOOKUP_KEY_HOLDER.get(); System.out.printf("時(shí)間 %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("時(shí)間 %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 肯定沒(méi)有線程安全問(wèn)題 我將這個(gè)變量修改成 NamedInheritableThreadLocal 改成 ThreadLocal 測(cè)試發(fā)現(xiàn)也沒(méi)有問(wèn)題了
我們來(lái)看看這個(gè) NamedInheritableThreadLocal 類(lèi)吧
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; } }
這個(gè)childValue 方法的描述很有趣
為這個(gè)可繼承的線程局部計(jì)算子線程的初始值變量作為父變量在子變量出現(xiàn)時(shí)的值的函數(shù)創(chuàng)建線程。
此方法從父類(lèi)內(nèi)部調(diào)用 子線程啟動(dòng)之前的線程。
就是子線程可以基礎(chǔ)父線程的 ThreadLocal 中的變量 看到這終于明白了 就是這個(gè)的問(wèn)題了
來(lái)仔細(xì)聊聊這個(gè)InheritableThreadLocal
首先我們知道ThreadLocal解決的是變量在不同線程間的隔離性,也就是不同線程擁有自己的值。類(lèi)ThreadLocal的主要作用是將數(shù)據(jù)放入當(dāng)前線程對(duì)象中的Map中,類(lèi)ThreadLocal自己不管理、不存儲(chǔ)任何數(shù)據(jù),它只是數(shù)據(jù)和Map之間的橋梁,Map中的key存儲(chǔ)的是ThreadLocal對(duì)象,value就是存儲(chǔ)的值。每個(gè)Thread中的Map值只對(duì)當(dāng)前線程可見(jiàn),其他線程不可以訪問(wèn)當(dāng)前線程對(duì)象中Map的值。
當(dāng)前線程銷(xiāo)毀,Map隨之銷(xiāo)毀,Map中的數(shù)據(jù)如果沒(méi)有被引用、沒(méi)有被使用,則隨時(shí)GC收回。由于Map中的key不可以重復(fù),所以一個(gè)ThreadLocal對(duì)象對(duì)應(yīng)一個(gè)value。
Thread類(lèi)中有一個(gè)init方法,每次創(chuàng)建線程的時(shí)候會(huì)執(zhí)行這個(gè)init方法,并且inheriThreadLocals默認(rèn)傳的參數(shù)是true,所以當(dāng)前線程對(duì)象每次都會(huì)從父線程繼承值,子線程將父線程中的table對(duì)象以復(fù)制的方式賦值給子線程的table數(shù)組,這個(gè)過(guò)程是在創(chuàng)建Thread類(lèi)對(duì)象時(shí)發(fā)生的,也就說(shuō)明當(dāng)子線程對(duì)象創(chuàng)建完畢后,子線程中的數(shù)據(jù)就是主線程中舊的數(shù)據(jù),主線程使用新的數(shù)據(jù)時(shí),子線程還是使用舊的數(shù)據(jù),因?yàn)橹髯泳€程使用兩個(gè)Entry[]對(duì)象數(shù)組各自存儲(chǔ)自己的值。
這個(gè)復(fù)制其實(shí)一個(gè)淺拷貝,如果存的值是可變對(duì)象的時(shí)候,只是復(fù)制了對(duì)象的引用而已,如果父線程修改對(duì)象的屬性值,子線程也是可以感知到的。
在我這個(gè)問(wèn)題是 就是 http-nio-82-exec-9
創(chuàng)建了線程http-nio-82-exec-10
導(dǎo)致了這個(gè)問(wèn)題
解決方法
重寫(xiě)childValue
或者 直接使用 ThreadLocal
/** * 為什么要用鏈表存儲(chǔ)(準(zhǔn)確的是棧) * <pre> * 為了支持嵌套切換,如ABC三個(gè)service都是不同的數(shù)據(jù)源 * 其中A的某個(gè)業(yè)務(wù)要調(diào)B的方法,B的方法需要調(diào)用C的方法。一級(jí)一級(jí)調(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é)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Mybatis-plus配置多數(shù)據(jù)源,連接多數(shù)據(jù)庫(kù)方式
- MyBatis-Plus多數(shù)據(jù)源的示例代碼
- SpringBoot集成Mybatis實(shí)現(xiàn)對(duì)多數(shù)據(jù)源訪問(wèn)原理
- Seata集成Mybatis-Plus解決多數(shù)據(jù)源事務(wù)問(wèn)題
- 詳解SpringBoot Mybatis如何對(duì)接多數(shù)據(jù)源
- Mybatis操作多數(shù)據(jù)源的實(shí)現(xiàn)
- 一文搞懂MyBatis多數(shù)據(jù)源Starter實(shí)現(xiàn)
- Mybatis-plus多數(shù)據(jù)源配置的兩種方式總結(jié)
- MyBatis-Plus 集成動(dòng)態(tài)多數(shù)據(jù)源的實(shí)現(xiàn)示例
- Mybatis-Plus的多數(shù)據(jù)源你了解嗎
- mybatis-flex實(shí)現(xiàn)多數(shù)據(jù)源操作
相關(guān)文章
Java中println輸出漢字亂碼問(wèn)題一招解決方案
這篇文章主要介紹了Java中println輸出漢字亂碼問(wèn)題一招解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12mybatis和mybatis-plus設(shè)置值為null不起作用問(wèn)題及解決
Mybatis-Plus的FieldStrategy主要用于控制新增、更新和查詢時(shí)對(duì)空值的處理策略,通過(guò)配置不同的策略類(lèi)型,可以靈活地處理實(shí)體對(duì)象的空值問(wèn)題2025-02-02mybatis-plus?查詢傳入?yún)?shù)Map,返回List<Map>方式
這篇文章主要介紹了mybatis-plus?查詢傳入?yún)?shù)Map,返回List<Map>方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12jdk8?FunctionalInterface注解源碼解讀
這篇文章主要介紹了jdk8?FunctionalInterface注解源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11Spring boot開(kāi)發(fā)web應(yīng)用JPA過(guò)程解析
這篇文章主要介紹了Spring boot開(kāi)發(fā)web應(yīng)用JPA過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Spring Boot集成MyBatis實(shí)現(xiàn)通用Mapper的配置及使用
關(guān)于MyBatis,大部分人都很熟悉。MyBatis 是一款優(yōu)秀的持久層框架,它支持定制化 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射。這篇文章主要介紹了Spring Boot集成MyBatis實(shí)現(xiàn)通用Mapper,需要的朋友可以參考下2018-08-08Lambda表達(dá)式下訪問(wèn)外部變量問(wèn)題
這篇文章主要介紹了Lambda表達(dá)式下訪問(wèn)外部變量問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11解析Mybatis連續(xù)傳遞多個(gè)參數(shù)的方法
MyBatis是一個(gè)支持普通SQL查詢,存儲(chǔ)過(guò)程和高級(jí)映射的優(yōu)秀持久層框架,這篇文章主要介紹了Mybatis連續(xù)傳遞多個(gè)參數(shù)的方法,需要的朋友可以參考下2016-08-08