在mybatis用mysql的代码块_【mybatis】不靠谱程序员之mysql环境下mybatis数据源驱动重复加载...

一、程序员的可能都是不靠谱的

请不要惹程序员生气,后果很严重!

程序员都是不靠谱的,但是我很靠谱,请找我,不谢!

二、再NB也抵挡不住犯错

今天再次撸一下mybatis数据源的源码,发现粗大事啦!

Apache的程序员也有屌丝啊,想想还是很激动,原来不止我一个人是屌丝程序员啊。但也可能是我理解不够深入,所以写出来,让大家瞅瞅,群撸一下。好,回正题。

1. Mysql驱动包

用JDBC第一步,就是导入数据库驱动jar包,安装mysql目录下就有,直接粘过来,build path一下就好了。

写JDBC第一步,就是注册驱动。注册驱动有两种方式,一种是通过DriverManager来进行注册,需要传入驱动包中对java.sql.Driver的实现类的实例对象;另一种是利用类加载原理来做,只需要把驱动字节码加载到内存生成对应的Class对象即可(请参看另一篇博文:《类加载机制》),一般就是Class.forName或通过Class的class属性来获取。两种方式,那么到底孰优孰劣?

因为mysql驱动包的缘故,第二种方式性能占优。mysql对java.sql.Driver的实现类中,通过静态代码块已经加载过一次数据库驱动了,因此通过第一种方式会导致驱动的重复加载,从而产生覆盖(产生覆盖,JVM判断两个对象是否相等通过equals方法,但是前提是两个对象有相同的类加载器,从而判定mysql驱动中通过静态代码块加载的驱动会被覆盖),而通过类加载的方式是不会导致重复加载驱动的,只是获得了mysql实现的驱动类的Class对象。mysql驱动包中源码如下所示,重点查看静态代码块。

package com.mysql.jdbc;

import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {

// ~ Static fields/initializers

// ---------------------------------------------

//

// Register ourselves with the DriverManager

//

static {

try {

java.sql.DriverManager.registerDriver(new Driver());

} catch (SQLException E) {

throw new RuntimeException("Can't register driver!");

}

}

// ~ Constructors

// -----------------------------------------------------------

/**

* Construct a new driver and register it with DriverManager

*

* @throws SQLException

* if a database error occurs.

*/

public Driver() throws SQLException {

// Required for Class.forName().newInstance()

}

}

实际上,根据类加载原理,如果使用的是mysql数据库,那么驱动是不需要显式加载的(如下图例证)。已经在驱动包的实现类中加载过了,直接通过DriverManager获取就可了,但是为了保证程序的通用性(例如换成Oracle数据库,Oracle数据库因为在驱动包中没有预先加载,因此需要手动加载驱动),所以还是显式加载一下最好,因此在写JDBC代码的时候,建议使用第二种方式来进行加载。

0818b9ca8b590ca3270a3433284dd417.png

那么就总结一下吧,mysql环境下使用JDBC进行数据库操作:

1. JDBC加载数据库驱动建议使用类加载方式,这种方式不会导致驱动的重复加载,消耗性能;

2. 如果使用DriverManager进行驱动加载,则会出现驱动重复加载,导致mysql驱动包中静态代码块加载的驱动被覆盖,空耗性能;

2. Mybatis数据源驱动

mybatis自己做了数据源,分别是JNDI、POOLED、UNPOOLED,一般情况下,生产环境使用JNDI(请参看博文:《MyBatis Tomcat JNDI原理及源码分析》),开发环境使用POOLED,不使用数据源则使用UNPOOLED,三种方方式的细节不是本次讨论的重点因此略去。除了JNDI方式,最终获取数据库连接的都是通过org.apache.ibatis.datasource.unpooled.UnpooledDataSource类的doGetConnection方法,但是在获取连接之前都是需要加载驱动的,而mybatis则通过方法initializeDriver来进行驱动的加载,代码如下。

private synchronized void initializeDriver() throws SQLException {

if (!registeredDrivers.containsKey(driver)) {

Class> driverType;

try {

if (driverClassLoader != null) {

driverType = Class.forName(driver, true, driverClassLoader);

} else {

driverType = Resources.classForName(driver);

}

// DriverManager requires the driver to be loaded via the system ClassLoader.

// http://www.kfu.com/~nsayer/Java/dyn-jdbc.html

Driver driverInstance = (Driver)driverType.newInstance();

DriverManager.registerDriver(new DriverProxy(driverInstance));

registeredDrivers.put(driver, driverInstance);

} catch (Exception e) {

throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);

}

}

}        问题也就出现在这里,源码不好看,请看截图。

0818b9ca8b590ca3270a3433284dd417.png

mybatis是通过DriverManager进行驱动加载的,也就是说,该方式是会导致驱动重复加载空耗性能的。有人会质疑注册驱动的方法传入的是一个驱动的代理对象,但实际上这个方法只是对原始驱动包装了一下,就算采用代理,静态代码块也不会消失,因此重复加载驱动空耗性能的问题依旧存在。

补充:文章写的比较早,那时候还比较嫩,分析是有纰漏的。首先,mybatis是不存在驱动重复加载的,在initializeDriver()方法的第一行,就是通过mybatis-config.xml中数据源配置属性“driver"来进行判断在ConcurrentHashMap是否已经存在相同KEY的驱动,以mysql为例,"driver"="com.mysql.jdbc.Driver",两者都是字符串,而ConcurrentHashMap是线程安全的(分段锁),因此同一数据库类型是不会导致驱动重复加载的。

也就是说,如果mybatis同时使用了tomcat和oracle,那么只会存在两个驱动实例,一开始的if判断过不了。

至于java.sql.DriverManager.registerDriver(Driver driver)是否会导致驱动重复注册的问题,答案是肯定的,确实是会造成驱动重复注册。DriverManager中通过CopyOnWriteArrayList线程安全的数据结构来存放驱动,只要传入的driver不为null,就会被放到ArrayList中。更严重的是,从DriverManager中获取驱动使用的时候,是通过遍历CopyOnWriteArrayList,只要拿到匹配数据库的第一个驱动就返回,以mysql为例,因此如果注册多个mysql驱动,那么每次都是第一个驱动被使用,而后面的驱动则一直闲着。不同数据库的驱动都存放在该结构中,但是无论哪一种数据库,都只有第一个驱动会被使用,其余的都会闲着浪费资源。

java.sql.DriverManager源码分析如下。

0818b9ca8b590ca3270a3433284dd417.png

再回顾一下DriverManager.getConnection方法,遍历ArrayList,匹配立即返回,因此同一数据库注册多个驱动只会有第一个被使用,其余空闲,千万注意,传入的username, password, url是在连接数据库的时候才使用,而连接数据库则是通过Driver对象(实际上是driver.connec方法)。

0818b9ca8b590ca3270a3433284dd417.png

三、真的是犯错吗

上图中,画红线的下一句,将驱动代理和原始驱动放入到map中,也就意味着mabatis是允许多个驱动存在,实际上在mybatis-config.xml中可以配置多个environment(注意:environments标签是需要配置default属性的),不同的环境可能使用不同数据库的数据源,因此mybatis是需要存放每一个环境的数据库库驱动对象,为此,只有付出重复加载驱动空耗性能的代价换取设计上的实现,事实上,几乎只有在mysql环境下才会产生空耗问题。所以,这是一种折中选择吧,并不是真正的犯错!

至此,讨论结束。

附注:

本文如有错漏,烦请不吝指正,谢谢!

本文主要指出mybatis在msyql环境下会导致驱动重复加载问题,如果有错漏的地方,热烈欢迎讨论!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当涉及到并发下数据更新和插入时,最好使用数据库中提供的事务机制来保证数据的一致性。以下是示例代码: 首先,在 `application.properties` 文件中配置数据源和事务: ``` spring.datasource.url=jdbc:mysql://localhost:3306/your_database spring.datasource.username=your_username spring.datasource.password=your_password spring.datasource.driverClassName=com.mysql.jdbc.Driver # 开启事务 spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=50 spring.datasource.hikari.idle-timeout=60000 spring.datasource.hikari.pool-name=DatebookHikariCP spring.datasource.hikari.auto-commit=false spring.datasource.hikari.connection-test-query=SELECT 1 # MyBatis 配置 mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.use-generated-keys=true mybatis.configuration.default-fetch-size=100 mybatis.configuration.default-statement-timeout=30 mybatis.configuration.jdbc-type-for-null=NULL ``` 然后,在你的 `Mapper` 接口中定义数据库相关操作: ``` public interface UserMapper { // 插入用户 @Insert("insert into user(name, age) values(#{name}, #{age})") int insertUser(@Param("name") String name, @Param("age") int age); // 更新用户 @Update("update user set age = #{age} where id = #{id}") int updateUser(@Param("id") int id, @Param("age") int age); } ``` 最后,在你的业务代码中调用这些方法: ``` @Service public class UserService { @Autowired private UserMapper userMapper; public void insertAndUpdateUser() { // 开始事务 TransactionStatus transactionStatus = TransactionAspectSupport.currentTransactionStatus(); // 插入一条数据 int insertResult = userMapper.insertUser("Alice", 20); if (insertResult != 1) { // 插入失败,回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return; } // 更新刚刚插入的数据 int updateResult = userMapper.updateUser(1, 21); if (updateResult != 1) { // 更新失败,回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return; } // 提交事务 transactionManager.commit(transactionStatus); } } ``` 在上面的代码中,我们使用了 `TransactionManager` 来开启和提交事务,并使用了 `TransactionAspectSupport` 来回滚事务。这样,当有多个线程同时访问时,只有一个线程能够执行到更新或插入数据的代码块,从而保证了数据的一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值