浅析设计高可用数据库连接池(多线程)的核心要点与技术原理以及处理线程的安全问题

前不久有俩个盆友和我探讨这些问题,我做了个简单的总结分享给打架,明天就是国庆了祝大家玩的开心,主要分享设计数据库连接池原理以及要处理关键点,本文只挑选某一种实现方式来简单阐述,暂不涉及事务相关。关于事务传播行为和跨库事务(包括2PC和TCC),过段时间再做分享!

那我们先引出问题,就从没有数据库连接池的时候说起吧(图就不画了,网上截一个)!!!

浅析设计高可用数据库连接池(多线程)的核心要点与技术原理以及处理线程的安全问题_第1张图片

缺点:首先,每一次web请求都要建立一次数据库连接。建立连接通常需要消耗相对较大的资源(包括分配内存资源)。这个时间对于一次或几次数据库操作,或许感觉不出系统有多大的开销。对于如今的web应用,尤其是大型电商网站,同时有几百人甚至几千人在线访问。这种情况下频繁的进行数据库连接操作势必占用很多的系统资源,进而影响网站的响应速度,严重的话甚至会造成服务器的瘫痪。还有对于每一次数据库连接,使用完后都得断开。如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终结果必定是重启数据库。还有,这种开发被创建的连接对象数不能控制,系统资源被毫无顾及的分配,连接数量过多,也可能会导致内存泄漏,服务器崩溃。

数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池就是针对这个问题提出来的.数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。如下图所示:

浅析设计高可用数据库连接池(多线程)的核心要点与技术原理以及处理线程的安全问题_第2张图片

我们做一个对比:

对于传统的通过传入jdbcUrl用Drivermanager.getConnection(jdbcUrl)来连接数据库:

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

注意:一次Drivermanager.getConnection(jdbcurl)获得只是一个connection,并不能满足高并发情况。因为connection不是线程安全的,一个connection对应的是一个事物。简单举个例子:线程A刚刚获得这个connection开启了一个事务,还没来得及做些什么,线程B直接提交了这个事务。他们使用的都是同一个Connection,这个就是致命的了,,算上延迟啥的天知道数据会成什么样。

数据库连接池是多次Drivermanager.getConnection(jdbcurl),获取多个connection放入map中。每次从map.getConnection(),都是先从threadlocal里面拿的,如果threadlocal里面有,则用,如果新线程,则将新的connection放在threadlocal里,再get给到线程。实现一个线程里进行多次DAO操作,用的是同一个connection,以保证这个事务完整。

        至于为什么要用ThreadLocal呢?这个和连接池无关,我认为更多的是和程序本身相关,那么它又是怎么保证线程安全呢?

要编写一个多线程安全的程序是困难的,为了让线程共享资源,必须小心翼翼的对共享资源进行同步,同步带来一定的效能延迟,而另一方面,在处理同步的时候,又要注意对象的锁定与释放,避免死锁,种种因素都使得编写多线程程序变得困难。那么我们尝试换个角度来思考多线程下资源共享的问题:既然共享资源如此困难,干脆就不要共享了,我们为每个线程创造一个资源的副本。将每一个线程存取数据的行为加以隔离,实现的方法就是给予每个线程一个特定空间来保管该线程所独享的资源。

这时候就用到了ThreadLocal,实质上他就是一个线程局部变量(local variable)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。使用场景:1.To keep state with a thread (user-id, transaction-id, logging-id);2.To cache objects which you need frequently.

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。它主要由四个方法组成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()或者set(Object)时才执行,并且仅执行1次。ThreadLocal中的确实实现直接返回一个null。我们来看一段源码代码,hibernate是如何使用ThreadLocal管理多线程访问的。

public static final ThreadLocal session = new ThreadLocal();
public static Session currentSession() {
  Session s = (Session)session.get();
    //open a new session,if this session has none
 if(s == null){
    s = sessionFactory.openSession();
     session.set(s);
  }
   return s;
}

做个简单的分析:
1.初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。如果不初始化initialvalue,则initialvalue返回null。
2. session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接).多线程情况下共享数据库连接是不安全的。ThreadLocal保证了每个线程都有自己的s(数据库连接)。
3.如果是该线程初次访问,自然s(数据库连接)会是null,接着创建一个Session,具体就是这行代码:s = sessionFactory.openSession()。
4.创建一个数据库连接实例 s
5.保存该数据库连接s到ThreadLocal中。
6.如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。

你可能感兴趣的:(细节深究)