2023_08—第四周
Druid driverClassName
Context: 数据库从 Oracle 迁移至 OceanBase(Oracle 租户模式),顺便将数据库配置从项目文件迁移至 Apollo,代码无改动。框架为定制化的 Spring。
OK,下面讲问题,,
不出意外,出意外了,,发版时,服务启动成功后,查询数据库报错:👇🏻
1 | nested exception is org.apache.ibatis.exceptions.PersistenceException: |
报错的堆栈信息其实并不能说明特别具体的问题,因为执行任何一条 SQL 都会报错,可以排除跟代码无关。
另外,经过深入查找还发现其他报错信息:unknown jdbc driver : ***
,位于 Druid 包中,以 1.1.9 版本为例,代码如下:👇🏻
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 /**
* 该方法主要使用来通过数据库链接串来确定对应数据库的驱动名称
* https://github.com/alibaba/druid/blob/1.1.9/src/main/java/com/alibaba/druid/util/JdbcUtils.java
*/
public static String getDriverClassName(String rawUrl) throws SQLException {
if (rawUrl == null) {
return null;
}
if (rawUrl.startsWith("jdbc:derby:")) {
return "org.apache.derby.jdbc.EmbeddedDriver";
} else if (rawUrl.startsWith("jdbc:mysql:")) {
if (mysql_driver_version_6 == null) {
mysql_driver_version_6 = Utils.loadClass("com.mysql.cj.jdbc.Driver") != null;
}
if (mysql_driver_version_6) {
return MYSQL_DRIVER_6;
} else {
return MYSQL_DRIVER;
}
}
...
} else if (rawUrl.startsWith("jdbc:clickhouse:")) {
return JdbcConstants.CLICKHOUSE_DRIVER;
} else {
throw new SQLException("unkow jdbc driver : " + rawUrl); // 👈🏻👈👈
}
那基本确定问题了,,当前 Druid 版本有点低,没有对 OceanBase 做兼容,理论上升级高版本的可以解决。事实上,升级到 2.X 版本后也确实解决了问题[^1],但还有个疑点,测试环境没问题啊。
此时已经将近凌晨 12 点,不少关联方同事都在等待验证,先试探性的找到运维同学帮忙替换高版本 jar 之后,重启试试。重启之后,业务一切正常,验证完自己负责的业务功能后,继续寻找答案,,
此情此景,不免心生疑问,,Druid 这么 Low 的么?通过这种方式查询对应的数据库驱动,那岂不是很耽误事儿,,市场每出一款数据库,就要立即出版本做兼容?不是可以指定驱动名称的么?有妖气!!!
其实到这里,问题基本定位到了,,就是驱动名称配置的问题。搞笑的是,出问题的不是配置中的驱动名称:com.alipay.oceanbase.jdbc.Driver
,而是对应的 Key:driverClassName
。。测试环境配置的没问题,生产环境配置的是 driverClass
😂。
以👆🏻上内容都是后知后觉,因为当初核对生产配置时,有三个开发在场,大家的关注点都在 value 上,key 反而忽略了。。
继续追查妖气,,在 druid 包下查找 JdbcUtils.getDriverClassName()
的上层引用,发现了蛛丝马迹:👇🏻
1 | public static DataSourceProxyConfig parseConfig(String url, Properties info) throws SQLException { |
接着追查 parseConfig()
可以发现 druid 的数据源工厂在生产数据源时,会通过配置文件组装实例:👇🏻
1 | public class DruidDataSourceFactory implements ObjectFactory { |
在定制框架中,在 DruidBean 在实现 InitializingBean#afterPropertiesSet()
的方法中, 指定了配置项 A,如果该配置项开启,则执行 dataSource.init();
方法,遗憾的是,,该配置项并未开启,导致问题在服务启动阶段并未暴露。。
看了下 SpringBoot 框架通过 @EnableAutoConfiguration
机制注入的 DruidDataSource
执行流程与上述有差异,大家需要注意下,挖个坑,慢慢填。。
OceanBase ORA-24761
在 OceanBase Oracle 租户模式下,存在一个特殊的配置:ob_trx_idle_timeout
,数据库迁移时需额外注意下。官方解释为:事务空闲超时指当事务两条语句执行间隔超过指定阈值。可以通过以下语句查看数据库配置:
obclient> SHOW variables like ‘ob_trx_idle_timeout’;
| ob_trx_idle_timeout | 120000000 |
需要注意的是,单位为微秒,即默认 120s。官方在 v4.2 版本[^2]中的例子比较好理解:(吐槽下..下载的 v3.2.3 版本的 PDF 文档例子不容易理解):👇🏻
obclient [SYS]> SELECT * FROM ordr;
+—-+——+——-+——————————+
| ID | NAME | VALUE | GMT_CREATE |
+—-+——+——-+——————————+ |
| 1 | CN | NULL | 04-NOV-22 06.06.16.843024 PM |
| 2 | UK | NULL | 04-NOV-22 06.06.16.843024 PM |
| 3 | US | NULL | 04-NOV-22 06.06.16.843024 PM |
+—-+——+——-+——————————+
3 rows in set
obclient [SYS]> UPDATE ordr SET value=1003 WHERE id=3;
Query OK, 1 rows affected
Rows matched: 1 Changed: 1 Warnings: 0
/* 等待较长一段时间不操作*/ // 👈👈👈 时间超过 ob_trx_idle_timeout 即报错
obclient [SYS]> SELECT * FROM ordr;
ORA-24761: transaction rolled back: transaction idle timeout
简单来说就是,,开启事务后,事务内两条语句执行间隔超过 ob_trx_idle_timeout
就会报 ORA-24761。
快速解决方案就是将间隔时间配置长一点,要从根本上解决还得是分析业务逻辑,重新评审代码实现方案缩小事务范围,可以考虑最终一致 + 局部事务 +补偿,保证不会产生脏数据,再结合报警+运营手段解决。
[^1]: JdbcUtils.java V2.X
[^2]: 事务空闲超时,错误代码 ORA-24761