MyBatisPuls多数据源操作数据源偶尔报错问题

 更新时间:2024年06月15日 14:09:56   作者:Q z1997  
这篇文章主要介绍了MyBatisPuls多数据源操作数据源偶尔报错问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

Java技术迷

MyBatisPuls多数据源操作数据源偶尔报错

昨天同事在开发一个项目的时候使用了 MybatisPlus 的多数据源, 但是在登陆的时候偶然就会报错 如下 说使用错库了

但是刷新几次有好了 我去看了看这个问题 我当时表示十分震惊 debug 了 一个多小时也没找到错误 正当我快放弃的时候 我想起了我以前排除过的一个问题 mybatis的 幽灵分页 (错误的使用分页插件 导致的ThreadLocal 重复使用的问题)

版本是

1
2
3
4
5
<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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
 * 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();
  }
}

最后修改他的源码 增加日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
 * 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 竟然两个线程共享了 震惊!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 设置当前线程数据源
 * <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 类吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 为什么要用链表存储(准确的是栈)
 * <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<>();
    }
};

最后去看官方文档

人家修复了 呜呜呜呜呜呜呜

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

蓄力AI

微信公众号搜索 “ 脚本之家 ” ,选择关注

程序猿的那些事、送书等活动等着你

原文链接:https://blog.csdn.net/weixin_43939924/article/details/126165935

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 reterry123@163.com 进行投诉反馈,一经查实,立即处理!

相关文章

  • Java中使用MinIO的常用操作示例

    Java中使用MinIO的常用操作示例

    这篇文章主要介绍了Java中MinIO的常用操作示例,MinIO 是一款基于Go语言发开的高性能、分布式的对象存储系统,客户端支持Java,Net,Python,Javacript, Golang语言,需要的朋友可以参考下
    2024-01-01
  • java并发编程之cas详解

    java并发编程之cas详解

    这篇文章主要介绍了java并发编程之cas详解,涉及cas使用场景和cas用作原子操作等内容,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11
  • SpringBoot整合Apache Ignite的实现

    SpringBoot整合Apache Ignite的实现

    本文主要介绍了SpringBoot整合Apache Ignite的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-10-10
  • struts2静态资源映射代码示例

    struts2静态资源映射代码示例

    这篇文章主要介绍了struts2静态资源映射的相关内容,涉及了具体代码示例,具有一定参考价值,需要的朋友可以了解下。
    2017-09-09
  • 搭建MyBatis-Plus框架并进行数据库增删改查功能

    搭建MyBatis-Plus框架并进行数据库增删改查功能

    这篇文章主要介绍了搭建MyBatis-Plus框架并进行数据库增删改查,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • 使用MyBatis-Generator如何自动生成映射文件

    使用MyBatis-Generator如何自动生成映射文件

    这篇文章主要介绍了使用MyBatis-Generator如何自动生成映射文件,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • java面向对象基础_final详细介绍

    java面向对象基础_final详细介绍

    本文将详细介绍java final 对象的使用,需要了解更多的朋友可以参考下
    2012-11-11
  • idea打不开双击IDEA图标没反应的快速解决方案

    idea打不开双击IDEA图标没反应的快速解决方案

    这篇文章主要介绍了idea打不开双击IDEA图标没反应的快速解决方案,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • 浅析Java中对象的创建与对象的数据类型转换

    浅析Java中对象的创建与对象的数据类型转换

    这篇文章主要介绍了Java中对象的创建与对象的数据类型转换,是Java入门学习中的基础知识,需要的朋友可以参考下
    2016-01-01
  • java基本教程之线程休眠 java多线程教程

    java基本教程之线程休眠 java多线程教程

    本文对javaThread中sleep()方法进行介绍,sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”,大家参考使用吧
    2014-01-01

最新评论