原创

数据库连接池使用ThreadLocal的原因

温馨提示:
本文最后更新于 2021年08月06日,已超过 333 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

ThreadLocal 不知道大家日常有没有用到,反正我是没有用过~hhh,但是这个东西确实是挺重要的,我之前在多线程的专题里面写过一篇。

常见的就是现在的C3P0、Druid线程池也会用到。

复盘一下,如下。

1、为什么要引入数据库连接池?

数据库连接池大家可能不知道,但是线程池大家一定听过,其实数据库的连接池就是一个线程池,原生的JDBC,我们一般是这有操作数据库的:

Class.forName("com.mysql.jdbc.Driver");
java.sql.Connection conn = DriverManager.getConnection("jdbcUrl");

需要用到了就获取连接,不需要了就close()

C3P0、Druid 这种数据库工具都引入了线程池,把所有的数据库操作都交给线程池操作,这样的好处有:

1、 资源重用,假如一个请求频繁的操作数据库,每次都new 一个连接是十分消耗资源的。

2、更快的响应,建立连接是需要时间的,对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

3、统一管理, 可根据预先的连接占用超时设定,防止强制收回被占用连接,避免在操作期间出错。

数据库连接池就是典型的用空间换时间的思想

虽然创建了多个连接对象丢在池里,会占用一定的内存空间,但是可以省去接下来每一次的创建连接、关闭连接时间。

在数据库的连接上,也是一个TCP的三次握手,其中还需要验证账户、密码、权限什么的,可想而知多耗时。

好比你每次写代码都要开机、打开IDEA;如果不关电脑下次直接就可以用IDEA写代码了,虽然费点“电脑”,但是效率去高了。

2、数据库的连接池是如何管理的?

上面为什么要引入数据库连接池这个问题还是很容易回答的,但是线程池是如何管理的这个问题就破防,大意了,我并不是很了解,现在来总结一下。

我以Durid为例(其实不止durid),线程池是通过 ThreadLocal 来管理的。

先来说一下ThreadLocal的原理:

1、每个Thread维护着一个ThreadLocalMap的引用

2、ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

3、调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象

4、调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

5、ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

其为变量在每个线程中都创建了一个副本,每个线程都访问和修改本线程中变量的副本,但每个线程之间的变量是不能相互访问的,只能访问自己的。

如果你不用,那么可以维护一个全局变量(消耗资源),或者进行参数传递(得写多少无用代码,万一改了名字)

使用ThreadLocal的好处有:

1、代码调用DruidDataSource来管理conn连接,同时声明了ThreadLocal对象来保存每次线程请求所获取的连接,这样可以避免每个new一个JDBCUtils对象,将conn对象放在ThreadLocal对象中缓存起来,下次调用直接从ThreadLocal中获取来实现性能的提高。

2、保证事务的一致性。

每个线程保存的是该对象的一个副本,不同线程之间不会互相影响,假如一个DAO,你每次执行SQL操作的时候,是不是都要获取connection连接呢?如果有多个DAO操作,我们可以传参一个connection,但是这样写太麻烦了,而使用ThreadLocal,则在datasource.getConnection();的时候,直接就可以获取了,而且它可以保证当前线程中任何地方的Connection数据库连接都是相同的,也保证了事务的一致性。

来个例子:

//一个方法有两个dao操作
//方法一
DruidPooledConnection connection = datasource.getConnection();
PreparedStatement ps = connection.prepareStatement("select * from  user");
ResultSet resultSet = ps.executeQuery();

//方法二
DruidPooledConnection connection = datasource.getConnection();
PreparedStatement ps = connection.prepareStatement("update user set name = 'HaC' where id = 1");
Integer result = ps.executeUpdate();

这样的话Druid的getConnection()方法是从ThreadLocal获取的。

我们使用一些ORM框架,比如Mybatis、JPA等等无法感知这个过程,其实这些框架的底层也是如此。

当然Druid的功能不只是引入了ThreadLocal,还有线程池的大小控制、拒绝、超时管理等等很强大的功能。

可以尝试手写一个ThreadLocal来管理连接池:

public class C3P0Utils {

    private static DataSource source;//数据源
    static{
        source = new ComboPooledDataSource("mysql");//根据配置文件初始化数据源
    }

    /**
     * 获取数据库连接
     * @return
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException{
        return source.getConnection();
    }
}
public class TransactionThreadLocal {

    private static ThreadLocal<Connection> tc = new ThreadLocal<>();
    /**
     * 返回当前线程的数据库连接
     * @return
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException{
        Connection connection = tc.get();
        if(connection == null){
            connection = C3P0Utils.getConnection();
            tc.set(connection);
        }
        return connection;
    }

    /**
     * 开启事务
     * @throws SQLException
     */
    public static void startTransaction() throws SQLException{
        getConnection().setAutoCommit(false);
    }
    /**
     * 提交事务
     * @throws SQLException
     */
    public static void commit() throws SQLException{
        getConnection().commit();
    }

    /**
     * 事务回滚
     * @throws SQLException
     */
    public static void rollback() throws SQLException{
        getConnection().rollback();
    }

    /**
     * 关闭数据库
     * @throws SQLException
     */
    public static void close() throws SQLException{
        getConnection().close();//关闭数据库
        tc.remove();//将该线程的connection对象移除
    }
}
public class BookDaoImp implements BookDao{
    @Override
    public void edit(Book book) throws Exception {
        Connection conn = TransactionThreadLocal.getConnection();
        String sql = "update book set name=? where id=?";
        PreparedStatement pst = conn.prepareStatement(sql);
        pst.setString(1, "name");
        pst.setString(2, id);
        pst.execute();
    }
}

当然这个线程池的只放了一个连接,而Druid是可以自定义放很多个的,但是这个例子使用了ThreadLocal,可以避免每次都new 一个Connection,也不需要每次都把Connection作为参数传递。

正文到此结束
关注公众号 【HelloCoder】
免费领取Java学习资料
让技术,化繁为简
本文目录