JAVA和C#中数据库连接池原理与应用

JAVA和C#中数据库连接池原理

在现在的互联网发展中,高并发成为了主流,而最关键的部分就是对数据库操作和访问,在现在的互联网发展中,ORM框架曾出不穷, 比如:.Net-Core的EFCore、SqlSugar、Dapper。JAVA的Spring-DataJpa(EntityManager),Mybatis,MybatisPlus等等

但是说到ORM其实本质都是操作最底层的数据库访问组件:Ado.net,Jdbc

今天我就来聊一聊这两个数据库访问的连接池原理

在说到Ado.net和jdbc的数据连接池之前,首先我们需要了解数据库连接池是什么

连接到数据库服务器通常由几个需要很长时间的步骤组成。 必须建立 物理通道(例如套接字或命名管道),必须与服务器进行初次握手, 必须分析连接字符串信息,必须由服务器对连接进行身份验证,必 须运行检查以便在当前事务中登记,等等。

实际上,大多数应用程序仅使用一个或几个不同的连接配置。 这意味着在执行应用程序期间,许多相同的连接将反复地打开和关闭。这很耗费Cpu的性能。为了将打开连接的成本降至最低,ADO.NET使用称为连接池的优化技术。而Java则是jdbc连接池的优化技术。

一般来说,Java应用程序访问数据库的过程是:

  1. 装载数据库驱动程序;
  2. 通过jdbc建立数据库连接;
  3. 访问数据库,执行sql语句;
  4. 断开数据库连接。

JAVA和C#中数据库连接池原理与应用_第1张图片

 

JAVA和C#中数据库连接池原理与应用_第2张图片

 

这是常用的Tomcat的数据库连接导图和Jdbc进行数据库连接的步骤

 

而.Net Framwork/.Net Core应用程序访问数据库的过程是由 .NET数据提供程序的四个核心对象:

1.Connection:连接数据库 2.Command:执行数据库命令 3.DataReader:负责从数据源中读取数据 4.DataAdapter:负责数据集和数据库的联系

JAVA和C#中数据库连接池原理与应用_第3张图片

这是Ado.net数据库连接的导图

Ado.net:

Ado.net连接数据库的步骤:

1.新建一个数据库连接字符串
string conStr = “Data Source=.;Initial Catalog=MySchoolDB;Integrated Security=True”;
2.引入命名空间:
using System.Data.SqlClient;
3.创建SqlConnection对象
SqlConnection conn = new SqlConnection(conStr);
4.打开连接:
conn.Open();
5.关闭连接:
conn.Close();
五、使用Command对象的步骤:
1.创建数据库连接
SqlConnection conn = new SqlConnection(conStr);
2.定义sql语句
string sql = “insert into Admin values(‘值’)”;
3.创建SqlCommand对象
SqlCommand cmd = new SqlCommand(conn,sql);
4.执行命令
cmd.ExecuteScalar();

 

我们已经知道了在连接时,如果在一瞬间的访问量突然激增的情况下,那么线程就会开辟越多的数据库访问连接,这时候基本的连接已经不足以应对高并发高QPS的访问了

这个时候Mircosoft创造了由Data Provider提供的一种数据库连接池 --Ado.net连接池:它使得应用程序使用的连接保存在连接池里而避免每次都要完成建立/关闭连接的完整过程。

Data Provider在收到连接请求时建立连接的完整过程是:

  1. 先连接池里建立新的连接(即“逻辑连接”),然后建立该“逻辑连接”对应的“物理连接”。建立“逻辑连接”一定伴随着建立“物理连接”。
  2. Data Provider关闭一个连接的完整过程是先关闭“逻辑连接”对应的“物理连接”然后销毁“逻辑连接”。
  3. 销毁“逻辑连接”一定伴随着关闭“物理连接”,SqlConnection.Open()是向Data Provider请求一个连接Data Provider不一定需要完成建立连接的完整过程,可能只需要从连接池里取出一个可用的连接就可以;
  4. SqlConnection.Close()是请求关闭一个连接,Data Provider不一定需要完成关闭连接的完整过程,可能只需要把连接释放回连接池就可以。

现在我写一段测试代码测试不使用连接池的数据库连接效果: 同时我用windows的性能计数器侦测了Cpu的消耗

class Program
{
    static void Main(string[] args)
    {
        SqlConnection con = new SqlConnection("server=.\\sqlexpress;database=zsw;pooling=true;trusted_connection=true;uid=sa;pwd=zsw158991626ZSW;");
        for (int i = 0; i < 10; i++)
        {
            try
            {
                con.Open();
                Console.WriteLine("开始连接数据库" + System.Threading.Thread.CurrentThread.Name);
                System.Threading.Thread.Sleep(1000);
            }
            catch (Exception e) { Console.WriteLine(e.Message); }
            finally
            {
                con.Close();
                System.Threading.Thread.Sleep(1000);
            }
        }
        Console.Read();
    }
}

 

这个时候我的代码是开启了数据库池连接,而我的连接数只有1,但是当我们去掉Console.Readkey的时候设置pooling=false的时候此时我的数据连接占用了10个,由于我的电脑sqlserver性能检测打不开,但是大家可以去网上百度后试试查看连接数

但是! .Net Core连接了数据库好像是默认打开数据连接池,这个我找了半天的文档也没有结果。

那么这个pooling是什么呢?

每当程序需要读写数据库的时候。Connection.Open()会使用ConnectionString连接到数据库,数据库会为程序建立 一个连接,并且保持打开状态,此后程序就可以使用T-SQL语句来查询/更新数据库。当执行到Connection.Close()后,数据库就会关闭当 前的连接。很好,一切看上去都是如此有条不紊。

但是如果我的程序需要不定时的打开和关闭连接,(比如说 ASP.Net 或是 Web Service ),例如当Http Request发送到服务器的时候、,我们需要打开Connection 然后使用Select* from Table 返回一个DataTable/DataSet给客户端/浏览器,然后关闭当前的Connection。那每次都Open/Close Connection 如此的频繁操作对于整个系统无疑就成了一种浪费。

ADO.Net Team就给出了一个比较好地解决方法。将先前的Connection保存起来,当下一次需要打开连接的时候就将先前的Connection 交给下一个连接。这就是Connection Pool。

那么这个pooling是如何工作的呢?

首先当一个程序执行Connection.open()时候,ADO.net就需要判断,此连接是否支持Connection Pool (Pooling 默认为True),如果指定为False, ADO.net就与数据库之间创建一个连接(为了避免混淆,所有数据库中的连接,都使用”连接”描述),然后返回给程序。 如果指定为 True,ADO.net就会根据ConnectString创建一个Connection Pool,然后向Connection Pool中填充Connection(所有.net程序中的连接,都使用”Connection”描述)。填充多少个Connection由Min Pool Size (默认为0)属性来决定。例如如果指定为5,则ADO.net会一次与SQL数据库之间打开5个连接,然后将4个Connection,保存在 Connection Pool中,1个Connection返回给程序。

当程序执行到Connection.close() 的时候。如果Pooling 为True,ADO.net 就把当前的Connection放到Connection Pool并且保持与数据库之间的连接。 同时还会判断Connection Lifetime(默认为0)属性,0代表无限大,如果Connection存在的时间超过了Connection LifeTime,ADO.net就会关闭的Connection同时断开与数据库的连接,而不是重新保存到Connection Pool中。

(这个设置主要用于群集的SQL 数据库中,达到负载平衡的目的)。如果Pooling指定为False,则直接断开与数据库之间的连接。

然后当下一次Connection.Open() 执行的时候,ADO.Net就会判断新的ConnectionString与之前保存在Connection Pool中的Connection的connectionString是否一致。 (ADO.Net会将ConnectionString转成二进制流,所 以也就是说,新的ConnectionString与保存在Connection Pool中的Connection的ConnectionString必须完全一致,即使多加了一个空格,或是修改了Connection String中某些属性的次序都会让ADO.Net认为这是一个新的连接,而从新创建一个新的连接。所以如果您使用的UserID,Password的认 证方式,修改了Password也会导致一个Connection,如果使用的是SQL的集成认证,就需要保存两个连接使用的是同一个)。

然后 ADO.net需要判断当前的Connection Pool中是否有可以使用的Connection(没有被其他程序所占用),如果没有的话,ADO.net就需要判断ConnectionString设 置的Max Pool Size (默认为100),如果Connection Pool中的所有Connection没有达到Max Pool Size,ADO.net则会再次连接数据库,创建一个连接,然后将Connection返回给程序。

如果已经达到了 MaxPoolSize,ADO.net就不会再次创建任何新的连接,而是等待Connection Pool中被其他程序所占用的Connection释放,这个等待时间受SqlConnection.ConnectionTimeout(默认是15 秒)限制,也就是说如果时间超过了15秒,SqlConnection就会抛出超时错误(所以有时候如果SqlConnection.open()方法抛 出超时错误,一个可能的原因就是没有及时将之前的Connnection关闭,同时Connection Pool数量达到了MaxPoolSize。)

如果有可用的Connection,从Connection Pool 取出的Connection也不是直接就返回给程序,ADO.net还需要检查ConnectionString的ConnectionReset属性 (默认为True)是否需要对Connection 最一次reset。这是由于,之前从程序中返回的Connection可能已经被修改过,比如说使用 SqlConnection.ChangeDatabase method 修改当前的连接,此时返回的Connection可能就已经不是连接当前的Connection String指定的Initial Catalog数据库了。所以需要reset一次当前的连接。但是由于所有的额外检查都会增大ADO.net Connection Pool 对系统的开销。

连接池是为每个唯一的连接字符串创建的。 当创建一个池后,将创建多个连接对象并将其添加到该池中,以满足最小池大小的需求。 连接根据需要添加到池中,但是不能超过指定的最大池大小(默认值为 100)。 连接在关闭或断开时释放回池中。

总结

在请求 SqlConnection 对象时,如果存在可用的连接,将从池中获取该对象。 连接要可用,必须未使用,具有匹配的事务上下文或未与任何事务上下文关联,并且具有与服务器的有效链接。

连接池进程通过在连接释放回池中时重新分配连接,来满足这些连接请求。 如果已达到最大池大小且不存在可用的连接,则该请求将会排队。 然后,池进程尝试重新建立任何连接,直至到达超时时间(默认值为 15 秒)。 如果池进程在连接超时之前无法满足请求,将引发异常。

用好连接池将会大大提高应用程序的性能。相反,如果使用不当的话,则百害而无一益。一般来说,应当遵循以下原则:

  1. 在最晚的时刻申请连接,在最早的时候释放连接。
  2. 关闭连接时先关闭相关用户定义的事务。
  3. 确保并维持连接池中至少有一个打开的连接。
  4. 尽力避免池碎片的产生。主要包括集成安全性产生的池碎片以及使用许多数据库产生的池碎片。

JDBC:

JDBC默认的数据库连接池

JDBC的API中没有提供连接池的方法。一些大型的WEB应用服务器如BEA的WebLogic和IBM的WebSphere等提供了连接池的机制,但是必须有其第三方的专用类方法支持连接池的用法。

JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:

  ①DBCP 数据库连接池

  ②C3P0 数据库连接池

  DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource称为连接池

  数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。

  当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但上面的代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。

JDBC的数据库连接池的工作机制:

数据库连接池负责分配、管理和释放数据库连接的。数据库连接池在初始化时,会创建一定数量的连接放入连接池中,这些数据库连接的数量是由最小数据库连接数量来设定的。无论这些数据库连接有没有被使用,连接池一直都将保持有至少有这么多数量的连接。连接池的最大数据库连接数量限制了这个连接池占有的最大连接数,当应用程序向连接池请求的连接数大于这个限制时,这些请求将会被加入到等待队列中。 数据库的最小连接数和最大连接数的设置要考虑一下几个因素:

1) 最小连接数是数据库连接池会一直保持的数据库连接,如果当应用程序对数据库连接的使用不是特别大时,将会有大量的数据库连接资源被浪费;

2) 最大连接数是指数据库能申请的最大连接数,如果数据库连接请求超过这个数时,后面的数据库连接请求就会被加入到等待队列,这样会影响后面的数据库操作;

3) 如果最小连接数和最大连接数相差太大的话,那么最先的连接请求会获利,之后超过最小连接数量的连接就等价于重新创建了一个新的数据库连接.不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。

现在我们试试用DBCP的方式连接数据库

1、首先建立一个maven项目,然后在resources文件下新建一个db.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?&useSSL=false&serverTimezone=UTC
jdbc.username=root //用户名
jdbc.password=123456 //密码
initSize=10 //初始化连接数
maxTotal=200 //最大连接数
maxIdle=60 //最大空闲数,数据库连接的最大空闲时间。超过空闲时间,数据库连接将被标记为不可用,然后被释放。设为0表示无限制。

 

2、接着导入maven的包依赖


    
        mysql
        mysql-connector-java
        8.0.19
    
    
        org.apache.commons
        commons-dbcp2
        2.7.0
    
    
        org.apache.commons
        commons-pool2
        2.7.0
    

    
        commons-logging
        commons-logging
        1.2
    

 

直接复制粘贴即可,但是请注意你的jdk默认版本必须再8以上!

3、再新建一个JdbcUtil类

package com.jdbc.util;    
import org.apache.commons.dbcp2.BasicDataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

/**
 * DBCP的方式链接数据库
 */
public class JdbcUtil {
    private static String driver;
    private static String url;
    private static String username;
    private static String password;
    private static int initSize;
    private static int maxTotal;
    private static  int maxIdle;
    private static BasicDataSource ds;
    static {
        ds = new BasicDataSource();
        Properties cfg=new Properties();
        try { //读取db.properties文件
            InputStream in = JdbcUtil.class
                    .getClassLoader()
                    .getResourceAsStream("db.properties");
            cfg.load(in);
            //初始化参数
            driver=cfg.getProperty("jdbc.driver");
            url=cfg.getProperty("jdbc.url");
            username=cfg.getProperty("jdbc.username");
            password=cfg.getProperty("jdbc.password");
            initSize=Integer.parseInt(cfg.getProperty("initSize"));
            maxTotal=Integer.parseInt(cfg.getProperty("maxTotal"));
            maxIdle=Integer.parseInt(cfg.getProperty("maxIdle"));
            in.close();
            //初始化连接池
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            ds.setInitialSize(initSize);
            ds.setMaxTotal(maxTotal);
            ds.setMaxIdle(maxIdle);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public static Connection getConnection() {//连接数据库封装类
        try {
            /*
             * getConnection()从连接池中获取的重用
             * 连接,如果连接池满了,则等待。
             * 如果有归还的连接线,则获取重用的连接
             */
            Connection conn = ds.getConnection();
            return conn;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public static void close(Connection conn) {//关闭数据库的连接方法,封装复杂的关闭过程;
        if(conn!=null) {
            try {
                //将用过的连接归还到连接池
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

 

4、我们编写一个测试类进行验证

package com.jdbc.service;
import com.jdbc.util.JdbcUtil;

import java.sql.Connection;
import java.sql.SQLException;

public class JdbcTest {
    public static void main(String[] args) {
        try {
            for (int i=0;i<1000;i++){
                Thread a= new Thread(new TestThread(),"线程:"+(i+1));
                a.start();
                System.out.println(a.getName()+"已启动");
            }
        }
        catch (Exception ex){
            ex.printStackTrace();
        }
    }
    private static class TestThread implements Runnable{

        private Connection con= JdbcUtil.getConnection();
        @Override
        public void run() {
            try {
                if (con.isClosed()){
                    System.out.println("连接已经关闭");
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            finally {
                //JdbcUtil.close(con);
                //System.out.println("\t"+Thread.currentThread().getName()+"已关闭");
            }
        }
    }
}

 

现在运行测试,发现输出

JAVA和C#中数据库连接池原理与应用_第4张图片

端口占据了200个,线程池开启了工作,只不过我没有释放连接端口,但是我修改一下db.properties的最大连接数为300,现在我们来看看效果

可以看到我们的数据库连接已经报错了,这是为什么呢?因为我本地的MySQL连接只有200端口,当超过200个端口连接时就会崩溃。这也是常见的数据库连接性能瓶颈

现在我们关闭连接的代码取消注释,可以看到即使有1000个连接也会快速执行,而且不会占用多余的端口

JAVA和C#中数据库连接池原理与应用_第5张图片

DBCP的方式也是服务器Tomcat的所使用的方式,所以在tomcat使用数据连接池还是很有必要的,至少能扛得住一般的并发!该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。

总结

JAVA的JDBC和微软Ado.net其实本质上的差别并不大,因为都是对于数据库的操作,其根本数据库的性能最大瓶颈真的就是链接问题吗?

那么数据库的索引,现在的数据库分库分表,读写分离技术的存在是因为什么呢?所以,数据库连接池也是性能优化之一的,未来还有更多的数据库优化操作等待着人们去探索

比如现在的阿里巴巴的Druid,就是最求精益求精的结果,微软的ORM也纷纷早都开启了数据库连接池的优化,这标志着未来的互联网性能瓶颈已经不在局势与传统的关系型数据库了

未来Nosql的流行介入让高并发更能承担起互联网大项目的重任!

其实对于Ado.net和jdbc我并没有花时间去进行性能比较,我喜欢C#也喜欢Java,优秀的语言本就是互相借鉴,就和我们写代码、学算法一样,如果你开始就懂得了如何写出优秀的代码我相信,你也不会在乎语言的性能优势了。

本文引用:

https://blog.csdn.net/huwei2003/article/details/71459198

https://blog.csdn.net/hliq5399/article/details/73292023

https://blog.csdn.net/weixin_40751299/article/details/81609332

https://www.cnblogs.com/justdoitba/p/8087984.html

https://www.cnblogs.com/albertrui/p/8421791.html

https://blog.csdn.net/L_it123/article/details/88205528

感谢以上的大佬们的文章,让我得以节约时间写出这篇文章。

你可能感兴趣的:(JAVA和C#中数据库连接池原理与应用)