修复seata的HikariCP中加载驱动程序类的问题

引言

大家好!今天我们一起探讨一下一个在seata 2.5.0版本修复的小bug,如标题所言,是和数据库连接池有关的驱动加载有关的问题,让我们一起来看看吧。

问题引入

在之前的代码中,如果连接池使用的是druid,那就完全没有问题,如果使用的是Hikari,就会报错。

示例seata服务端配置如下:

yaml 复制代码
server:
  port: 8091
spring:
  application:
    name: seata-server
  main:
    web-application-type: none
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: file
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: file
  store:
    # support: file 、 db 、 redis 、 raft
    mode: db
    session:
      mode: db
    lock:
      mode: db
    db:
      # If use druid, it is OK. If change to hikari, and then threw an error.
      datasource: hikari
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3396/db_seata?useUnicode=true&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
      user: admin_seata
      password: 123456
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      vgroup-table: vgroup_table
      query-limit: 1000
      max-wait: 5000
      druid:
        time-between-eviction-runs-millis: 120000
        min-evictable-idle-time-millis: 300000
        test-while-idle: true
        test-on-borrow: false
        keep-alive: false
      hikari:
        idle-timeout: 600000
        keepalive-time: 120000
        max-lifetime: 1800000
        validation-timeout: 5000
  #  server:
  #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'

如果我使用它datasource: hikari,则会抛出错误

vbnet 复制代码
Caused by: java.lang.RuntimeException: Failed to load driver class com.mysql.cj.jdbc.Driver in either of HikariConfig class loader or Thread context classloader

从报错很容易看出来就是连接池初始化时无法加载MYSQL的驱动类。

问题分析

既然issue里面说到使用druid是正常的,而使用HikariCP则不行。肯定这两个进行加载驱动时有所不同。

在druid时

在seata的DruidDataSourceProvider可以看到下面的代码,Druid 正确使用了这个类加载器:setDriverClassLoader(getDriverClassLoader()),所以druid是正常加载驱动的

java 复制代码
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(getDriverClassName());
//这里明显加载了数据员的驱动类
ds.setDriverClassLoader(getDriverClassLoader());
ds.setUrl(getUrl());
ds.setUsername(getUser());
ds.setPassword(getPassword());
ds.setInitialSize(getMinConn());
ds.setMaxActive(getMaxConn());
ds.setMinIdle(getMinConn());
ds.setMaxWait(getMaxWait());

在Hikari时

在seata的HikariDataSourceProvider可以看到下面的代码,可以看到,并没有加载到驱动, 但 HikariConfig 类没有setDriverClassLoader方法,所以 Hikari 无法使用专门的类加载器,导致 Hikari 使用默认类加载器无法找到 MySQL 驱动

java 复制代码
HikariConfig config = new HikariConfig(properties);
        config.setDriverClassName(getDriverClassName());
        config.setJdbcUrl(getUrl());
        config.setUsername(getUser());
        config.setPassword(getPassword());

问题解决

经过分析我们可以知道,由于Hikari的设计局限性,我们并不能将驱动直接注册在config里面,难道我们就不能解决这个问题了吗?当然不是,解决方法总是有的。

一般我们要知道,HikariCP 在设置 driverClassName 时,会尝试通过以下两种 ClassLoader 加载驱动类:

  1. HikariConfig 所在的 ClassLoader
  2. 当前线程的上下文 ClassLoader(Thread Context ClassLoader)

如果两者都找不到 com.mysql.cj.jdbc.Driver 类,就会抛出这个异常

清楚这个点之后,我们可以尝试解决这个问题。 在seata的所有provider的抽象父类中提供了获取目标驱动的方法

java 复制代码
protected ClassLoader getDriverClassLoader() {
    return DRIVER_LOADERS.getOrDefault(getDriverClassName(), this.getClass().getClassLoader());
}

我们可以采用一种"保护现场、恢复现场"的典型设计模式,在有关多线程场景经常使用。

我们先通过抽象父类获得目标驱动与驱动名,然后获取当前线程上下文原驱动,再将当前线程的驱动设置成我们第一个获取到的目标驱动并显式注册,确保类加载发生在正确的 ClassLoader 上下文中,最后finally还原线程驱动即可。

seata通过显式加载类和注册驱动到DriverManager ,后续驱动实例都会存在在DriverManagergetConnection(...) 就能用这个实例

java 复制代码
HikariConfig config = new HikariConfig(properties);

// Get the correct class loader
ClassLoader driverClassLoader = getDriverClassLoader();
String driverClassName = getDriverClassName();

// Set driver class name in the correct class loader context
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(driverClassLoader);

    // 1. Explicitly load and register the driver
    try {
        Class<?> driverClass = Class.forName(driverClassName, true, driverClassLoader);
        Driver driver = (Driver) driverClass.newInstance();
        DriverManager.registerDriver(new DriverWrapper(driver));
    } catch (Exception e) {
        logger.warn("Failed to explicitly register driver {}", driverClassName, e);
    }

    // 2. Set configuration
    config.setDriverClassName(driverClassName);
    config.setJdbcUrl(getUrl());
    config.setUsername(getUser());
    config.setPassword(getPassword());

} finally {
    Thread.currentThread().setContextClassLoader(originalClassLoader);
}

总结

  1. 绕过 HikariCP 的自动类加载机制
    Seata 不依赖 HikariCP 自己去加载 driverClassName,而是 提前手动加载并注册 Driver
  2. 确保类加载发生在正确的 ClassLoader 上下文中
    通过 Thread.currentThread().setContextClassLoader(driverClassLoader),使得后续 config.setDriverClassName(...) 调用时,HikariCP 内部使用的上下文 ClassLoader 正是能加载驱动的那个。
  3. DriverWrapper 保证后续 getConnection() 使用正确驱动
    即使 DriverManager 中有多个 Driver,包装后的 Driver 也能确保使用我们显式加载的那个实例,避免 ClassLoader 混乱

还有其他问题欢迎评论区友好讨论❤️

相关推荐
不是笨小孩1352 分钟前
多元算力融合实践:openEuler在中等配置硬件环境下的性能验证
后端
绝顶少年16 分钟前
高性能短信发送架构:批量合并与延迟发送的设计艺术
架构
q_191328469517 分钟前
基于SpringBoot2+Vue2+uniapp的考研社区论坛网站及小程序
java·vue.js·spring boot·后端·小程序·uni-app·毕业设计
稚辉君.MCA_P8_Java24 分钟前
Gemini永久会员 深度解析jvm内存结构
jvm·后端·架构
武子康25 分钟前
大数据-174 Elasticsearch 查询 DSL 实战:match/match_phrase/query_string/multi_match 全解析
大数据·后端·elasticsearch
一水鉴天27 分钟前
专题讨论 类型理论和范畴理论之间的关系:闭关系/闭类型/闭范畴 与 计算式(ima.copilot)
开发语言·算法·架构
壹米饭32 分钟前
Kubernetes 节点 DNS 解析异常问题排查与解决方案
后端·kubernetes
码界奇点34 分钟前
Spring Boot 全面指南从入门到精通构建高效Java应用的完整路径
java·spring boot·后端·微服务
ytadpole35 分钟前
若依验证码渲染失效问题
java·linux·后端
懂AI的老郑36 分钟前
Transformer架构在大语言模型中的优化技术:原理、方法与前沿
语言模型·架构·transformer