Mybatis引出的一系列问题-JDBC 的探究

1 引入对JDBC的理解-1

在这里插入图片描述
一般来说,Java应用程序访问数据库的过程是:

  1. 装载数据库驱动程序;
  2. 通过jdbc建立数据库连接;
  3. 访问数据库,执行sql语句;
  4. 断开数据库连接。
Public void FindAllUsers(){
    
    
    //1、装载sqlserver驱动对象
    DriverManager.registerDriver(new SQLServerDriver());             
    //2、通过JDBC建立数据库连接
    Connection con =DriverManager.getConnection("jdbc:sqlserver://192.168.2.6:1433;DatabaseName=customer", "sa", "123");            
    //3、创建状态会话
    Statement state =con.createStatement();           
    //4、查询数据库并返回结果
    ResultSet result =state.executeQuery("select * from users");           
    //5、输出查询结果
    while(result.next()){
    
    
        System.out.println(result.getString("email"));
    }            
    //6、断开数据库连接
    result.close();
    state.close();
    con.close();
}

程序开发过程中,存在很多问题: 首先,每一次web请求都要建立一次数据库连接。建立连接是一个费时的活动,每次都得花费0.05s~1s的时间,而且系统还要分配内存资源。 这个时间对于一次或几次web请求数据库操作,或许感觉不出系统有多大的开销。可是对于现在的web应用,尤其是大型电子商务网站,同时有几百人甚至几千人在线是很正常的事。

在这种情况下,频繁的进行数据库连接操作势必占用很多的系统资源,网站的响应速度必定下降,严重的甚至会造成服务器的崩溃。 不是危言耸听,这就是制约某些电子商务网站发展的技术瓶颈问题。其次,对于每一次数据库连接,使用完后都得断开。 但是,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将不得不重启数据库。 还有,这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

上述的用户查询案例,如果同时有1000人访问,就会不断的有数据库连接、断开操作:
在这里插入图片描述
通过上面的分析,我们可以看出来,“数据库连接”是一种稀缺的资源,为了保障网站的正常使用,应该对其进行妥善管理。其实,我们查询完数据库后,如果不关闭连接,而是暂时存放起来,当别人使用时,把这个连接给他们使用,就避免了一次建立数据库连接和断开的操作时间消耗。原理如下:
在这里插入图片描述
由上面的分析可以看出,问题的根源就在于对数据库连接资源的低效管理。我们知道,对于共享资源,有一个很著名的设计模式:资源池(resource pool)。该模式正是为了解决资源的频繁分配、释放所造成的问题。 为解决上述问题,可以采用数据库连接池技术。

数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。

我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。更为重要的是我们可以通过连接池的管理机制——监视数据库的连接的数量、使用情况,为系统开发、测试及性能调整提供依据。

我们自己尝试开发一个连接池,来为上面的查询业务提供数据库连接服务:

  1. 编写class 实现DataSource 接口
  2. 在class构造器一次性创建10个连接,将连接保存LinkedList中
  3. 实现getConnection 从 LinkedList中返回一个连接
  4. 提供将连接放回连接池中方法
public class MyDataSource implements DataSource {
    
    
              //链表 --- 实现栈结构
                privateLinkedList<Connection> dataSources = new LinkedList<Connection>();
              //初始化连接数量
              publicMyDataSource() {
    
    
                     //一次性创建10个连接
                     for(int i = 0; i < 10; i++) {
    
    
                            try {
    
    
                               //1、装载sqlserver驱动对象
                               DriverManager.registerDriver(new SQLServerDriver());
                               //2、通过JDBC建立数据库连接
                               Connection con =DriverManager.getConnection(
                                  "jdbc:sqlserver://192.168.2.6:1433;DatabaseName=customer", "sa", "123");
                               //3、将连接加入连接池中
                               dataSources.add(con);
                            } catch (Exception e) {
    
    
                               e.printStackTrace();
                            }
                     }
              }

              @Override
              publicConnection getConnection() throws SQLException {
    
    
                     //取出连接池中一个连接
                     finalConnection conn = dataSources.removeFirst(); // 删除第一个连接返回
                     return conn;
              }

              //将连接放回连接池
              publicvoid releaseConnection(Connection conn) {
    
    
                     dataSources.add(conn);
              }
       }


       //查询所有用户
       Public void FindAllUsers(){
    
    
              //1、使用连接池建立数据库连接
              MyDataSource dataSource = new MyDataSource();
              Connection conn =dataSource.getConnection();        
              //2、创建状态
              Statement state =con.createStatement();           
              //3、查询数据库并返回结果
              ResultSet result =state.executeQuery("select * from users");           
              //4、输出查询结果
              while(result.next()){
    
    
                     System.out.println(result.getString("email"));
              }            
              //5、断开数据库连接
              result.close();
              state.close();
              //6、归还数据库连接给连接池
              dataSource.releaseConnection(conn);
        }

这就是数据库连接池的原理,它大大提供了数据库连接的利用率,减小了内存吞吐的开销。我们在开发过程中,就不需要再关心数据库连接的问题,自然有数据库连接池帮助我们处理,这回放心了吧。但连接池需要考虑的问题不仅仅如此,下面我们就看看还有哪些问题需要考虑。

1、并发问题
为了使连接管理服务具有最大的通用性,必须考虑多线程环境,即并发问题。这个问题相对比较好解决,因为java语言自身提供了对并发管理的支持,使用synchronized关键字即可确保线程是同步的。使用方法为直接在类方法前面加上synchronized关键字,如:

public synchronized connection getconnection()

2、多数据库服务器和多用户
对于大型的企业级应用,常常需要同时连接不同的数据库(如连接oracle和sybase)。如何连接不同的数据库呢?我们采用的策略是:

设计一个符合单例模式的连接池管理类,在连接池管理类的唯一实例被创建时读取一个资源文件,其中资源文件中存放着多个数据库的url地址等信息。根据资源文件提供的信息,创建多个连接池类的实例,每一个实例都是一个特定数据库的连接池。连接池管理类实例为每个连接池实例取一个名字,通过不同的名字来管理不同的连接池。

对于同一个数据库有多个用户使用不同的名称和密码访问的情况,也可以通过资源文件处理,即在资源文件中设置多个具有相同url地址,但具有不同用户名和密码的数据库连接信息。

3、事务处理
我们知道,事务具有原子性,此时要求对数据库的操作符合“all-all-nothing”原则即对于一组sql语句要么全做,要么全不做。在java语言中,connection类本身提供了对事务的支持,可以通过设置connection的autocommit属性为false 然后显式的调用commit或rollback方法来实现。

但要高效的进行connection复用,就必须提供相应的事务支持机制。可采用每一个事务独占一个连接来实现,这种方法可以大大降低事务管理的复杂性。

4、连接池的分配与释放
连接池的分配与释放,对系统的性能有很大的影响。合理的分配与释放,可以提高连接的复用度,从而降低建立新连接的开销,同时还可以加快用户的访问速度。

对于连接的管理可使用空闲池。即把已经创建但尚未分配出去的连接按创建时间存放到一个空闲池中。

每当用户请求一个连接时,系统首先检查空闲池内有没有空闲连接。如果有就把建立时间最长(通过容器的顺序存放实现)的那个连接分配给他(实际是先做连接是否有效的判断,如果可用就分配给用户,如不可用就把这个连接从空闲池删掉,重新检测空闲池是否还有连接);

如果没有则检查当前所开连接池是否达到连接池所允许的最大连接数(maxconn)如果没有达到,就新建一个连接,如果已经达到,就等待一定的时间(timeout)。

如果在等待的时间内有连接被释放出来就可以把这个连接分配给等待的用户,如果等待时间超过预定时间timeout 则返回空值(null)。

系统对已经分配出去正在使用的连接只做计数,当使用完后再返还给空闲池。对于空闲连接的状态,可开辟专门的线程定时检测,这样会花费一定的系统开销,但可以保证较快的响应速度。也可采取不开辟专门线程,只是在分配前检测的方法。

5、连接池的配置与维护
连接池中到底应该放置多少连接,才能使系统的性能最佳?系统可采取设置最小连接数(minconn)和最大连接数(maxconn)来控制连接池中的连接。

最小连接数是系统启动时连接池所创建的连接数。如果创建过多,则系统启动就慢,但创建后系统的响应速度会很快;如果创建过少,则系统启动的很快,响应起来却慢。

最大连接数是连接池中允许连接的最大数目,具体设置多少,要看系统的访问量,可通过反复测试,找到最佳点。如何确保连接池中的最小连接数呢?有动态和静态两种策略。

动态即每隔一定时间就对连接池进行检测,如果发现连接数量小于最小连接数,则补充相应数量的新连接以保证连接池的正常运转。静态是发现空闲连接不够时再去检查。

实际开发中有成熟的开源连接池供我们使用:
理解了连接池的原理就可以了,没有必要什么都从头写一遍,那样会花费很多时间,并且性能及稳定性也不一定满足要求。事实上,已经存在很多流行的性能优良的第三方数据库连接池jar包供我们使用。如:

Apache commons-dbcp
c3p0
Druid
HikariCP

其中c3p0已经很久没有更新了,DBCP更新速度很慢,基本处于不活跃状态。而Druid和HikariCP处于活跃状态的更新中。

2 引入对JDBC的理解-2

数据库应用,在许多软件系统中经常用到,是开发中大型系统不可缺少的辅助。 但如果对数据库资源没有很好地管理(如:没有及时回收数据库的游标(ResultSet)、Statement、连接 (Connection)等资源),往往会直接导致系统的稳定。 这类不稳定因素,不单单由数据库或者系统本身一方引起,只有系统正式使用后,随着流量、用户的增加,才会逐步显露。

对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。 但是对于一个复杂的数据库应用,情况就完全不同了。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。

连接复用: 通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。 对于共享资源,有一个很著名的设计模式:资源池。该模式正是为了解决资源频繁分配、释放所造成的问题的。 把该模式应用到数据库连接管理领域,就是建立一个数据库连接池,提供一套高效的连接分配、使用策略,最终目标是实现连接的高效、安全的复用。

数据库连接池的基本原理: 在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。 如:外部使用者可通过 getConnection 方法获取连接,使用完毕后再通过 releaseConnection 方法将连接返回,注意此时连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。

数据库连接池技术带来的优势

1 资源重用
由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。

2 更快的系统响应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。 对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

3 新的资源分配手段
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术,如果设计中还没有考虑到连接池的应用, 那么…….快在设计文档中加上这部分的内容吧。某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。

4 统一的连接管理,避免数据库连接泄漏
在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄漏。

一个最小化的数据库连接池实现: 在基于Java开发的系统中,JDBC是程序员和数据库打交道的主要途径,提供了完备的数据库操作方法接口。 但考虑到规范的适用性,JDBC只提供了最直接的数据库操作规范,对数据库资源管理,如:对物理连接的管理及缓冲,期望第三方应用服务器(Application Server)的提供。

应用在获取数据库连接connection时,需要指定使用哪种类型的Driver,在获得特定的连接后,可按照固定的接口操作不同类型的数据库, 如: 分别获取Statement、执行SQL获得ResultSet等,如下面的例子 :

    Class.forName("com.mysql.jdbc.Driver");
    Connection conn = DriverManager.getConnection(url, username, password);
    Statement stmt = conn.createStatement();
    String sql1 = "update user set age = 100 where id = 1";
    int rows1 = stmt.executeUpdate(sql1);
    stmt.close();
    conn.close();

在完成数据操作后,一定要关闭所有涉及到的数据库资源。这虽然对应用程序的逻辑没有任何影响,但是关键的操作。 上面是个简单的例子,如果搀和众多的if-else、exception,资源的管理也难免百密一疏,Java系统也同样会面临崩溃的恶运。 所以数据库资源的管理依赖于应用系统本身,是不安全、不稳定的一种隐患。

JDBC连接池: 在标准JDBC对应用的接口中,并没有提供资源的管理方法。所以,缺省的资源管理由应用自己负责。虽然在JDBC规范中,多次提及资源的关闭/回收及其他的合理运用。 但最稳妥的方式,还是为应用提供有效的管理手段。 所以,JDBC为第三方应用服务器(Application Server)提供了一个由数据库厂家实现的管理标准接口:连接缓冲(connection pooling)。 引入了连接池( Connection Pool )的概念 ,也就是以缓冲池的机制管理数据库的资源。

JDBC最常用的资源有三类: Connection数据库连接。 Statement会话。 ResultSet结果集游标。
在这里插入图片描述
数据库连接池的实现及原理:
这是一种“爷—父—子”的关系,对Connection的管理,就是对数据库资源的管理。举个例子: 如果想确定某个数据库连接(Connection)是否超时,则需要确定其(所有的)子Statement是否超时,同样,需要确定所有相关的 ResultSet是否超时;在关闭Connection前,需要关闭所有相关的Statement和ResultSet。因此,连接池(Connection Pool)所起到的作用,不仅仅简单地管理Connection,还涉及到 Statement和ResultSet。

连接池(ConnectionPool)与资源管理:
ConnectionPool以缓冲池的机制,在一定数量上限范围内,控制管理Connection,Statement和ResultSet。任何数据库的资源是有限的,如果被耗尽,则无法获得更多的数据服务。

在大多数情况下,资源的耗尽不是由于应用的正常负载过高,而是程序的原因。在实际工作中,数据资源往往是瓶颈资源,不同的应用都会访问同一数据源。其中某个应用耗尽了数据库资源后,意味其他的应用也无法正常运行。

因此,ConnectionPool的第一个任务是限制:每个应用或系统可以拥有的最大资源,也就是确定连接池的大小(PoolSize)。

ConnectionPool的第二个任务:在连接池的大小(PoolSize)范围内,最大限度地使用资源,缩短数据库访问的使用周期。

许多数据库中,连接(Connection)并不是资源的最小单元,控制Statement资源比Connection更重要。以Oracle为例: 每申请一个数据库连接(Connection)会在物理网络(如 TCP/IP网络)上建立一个用于通讯的连接,在此连接上还可以申请一定数量的Statement。同一连接可提供的活跃Statement数量可以达到几百。 在节约网络资源的同时,缩短了每次会话周期(物理连接的建立是个费时的操作)。
在这里插入图片描述
但在一般的应用中,多数按照上例代码操作,这样有10个程序调用,则会产生10次物理连接,每个Statement单独占用一个物理连接,这是极大的资源浪费。

ConnectionPool可以解决这个问题,让几十、几百个Statement只占用同一个物理连接, 发挥数据库原有的优点。通过ConnectionPool对资源的有效管理,应用可以获得的Statement总数到达 : (并发物理连接数)×(每个连接可提供的Statement数量)

例如,某种数据库可同时建立的物理连接数为 200个,每个连接可同时提供250个Statement,那么ConnectionPool最终为应用提供的并发Statement总数为: 200 × 250 = 50,000个。这是个并发数字,很少有系统会突破这个量级。所以指出资源的耗尽与应用程序直接管理有关。

简单JDBC连接池的实现
在JDBC规范中,应用通过驱动接口(Driver Interface)直接获取数据库的资源。为了有效、合理地管理资源,在应用与JDBC Driver之间,增加了连接池: Snap-ConnectionPool。
并且通过面向对象的机制,使连接池的大部分操作是透明的。

参见下图,Snap-ConnectionPool的体系:
在这里插入图片描述
图中所示,通过实现JDBC的部分资源对象接口( Connection, Statement, ResultSet ),
在 Snap-ConnectionPool内部分别产生三种逻辑资源对象: PooledConnection, PooledStatement和 PooledResultSet。它们也是连接池主要的管理操作对象,并且继承了JDBC中相应的从属关系。这样的体系有以下几个特点:

透明性:
在不改变应用原有的使用JDBC驱动接口的前提下,提供资源管理的服务。
资源封装:
复杂的资源管理被封装在 Snap-ConnectionPool内部,不需要应用系统过多的干涉。管理操作的可靠性、安全性由连接池保证。应用的干涉(如:主动关闭资源),只起到优化系统性能的作用,遗漏操作不会带来负面影响。
资源合理应用:
按照JDBC中资源的从属关系,Snap-ConnectionPool不仅对Connection进行缓冲处理,对Statement也有相应的机制处理。合理运用Connection和Statement之间的关系,可以更大限度地使用资源。所以,Snap-ConnectionPool封装了Connection资源, 通过内部管理PooledConnection,为应用系统提供更多的Statement 资源。
资源连锁管理:
Snap-ConnectionPool包含的三种逻辑对象,继承了JDBC中相应对象之间的从属关系。在内部管理中,也依照从属关系进行连锁管理。例如:判断一个Connection是否超时,需要根据所包含的Statement是否活跃;判断Statement也要根据 ResultSet的活跃程度。

连接池集中管理ConnectionManager
ConnectionPool是Snap-ConnectionPool的连接池对象。在Snap-ConnectionPool内部,可以指定多个不同的连接池(ConnectionPool)为应用服务。ConnectionManager管理所有的连接池,每个连接池以不同的名称区别。通过配置文件适应不同的数据库种类。如下图所示:
在这里插入图片描述
通过ConnectionManager,可以同时管理多个不同的连接池,提供通一的管理界面。在应用系统中通过 ConnectionManager和相关的配置文件,可以将凌乱散落在各自应用程序中的数据库配置信息(包括:数据库名、用户、密码等信息),集中在一个文件中。便于系统的维护工作。

连接池使用范例:
对上栗代码的标准JDBC的使用范例,改为使用连接池,结果如下:

    import java.sql.*;
    import net.snapbug.util.dbtool.*;ConnectionPool dbConn = ConnectionManager .getConnectionPool("testOracle" );
    Statement st = dbConn.createStatement();
    ResultSet rs = st.executeQuery(“select * from demo_table” );
    …
    some data source operation in here
    rs.close();
    st.close();

在例子中,Snap-ConnectionPool封装了应用对Connection的管理。只要改变JDBC获取Connection的方法——获取连接池(ConnectionPool)(粗体部分),其他的数据操作都可以不做修改。

按照这样的方式,Snap-ConnectionPool可帮助应用有效地管理数据库资源。
如果应用忽视了最后资源的释放: rs.close() 和 st.close(),连接池会通过超时(time-out)机制,自动回收。

小结:
无论是Snap-ConnectionPool还是其他的数据库连接池,都应当具备一下基本功能:

  1. 对源数据库资源的保护
  2. 充分利用发挥数据库的有效资源
  3. 简化应用的数据库接口,封闭资源管理。
  4. 对应用遗留资源的自动回收和整理,提高资源的再次利用率。
  5. 在这个前提下,应用程序才能投入更多的精力于各自的业务逻辑中,数据库资源也不再成为系统的瓶颈。

3 技术经理:求求你,别再乱改数据库连接池的大小了

基本上来说,大部分项目都需要跟数据库做交互,那么,数据库连接池的大小设置成多大合适呢? 一些开发老鸟可能还会告诉你:没关系,尽量设置的大些,比如设置成 200,这样数据库性能会高些,吞吐量也会大些! 你也许会点头称是,真的是这样吗?看完这篇文章,也许会颠覆你的认知哦!

可以很直接的说,关于数据库连接池大小的设置,每个开发者都可能在一环节掉进坑里,事实上呢,大部分程序员可能都会依靠自己的直觉去设置它的大小,设置成 100 ? 思量许久后,自顾自想,应该差不多吧?

不妨意淫一下,你手里有个网站,并发压力虽然还没到 Facebook 那个级别,但是呢?也有个1万上下的并发量! 也就是说差不多2万左右的 TPS[TPS:Transactions Per Second(每秒传输的事物处理个数),TPS包括一条消息入和一条消息出,加上一次用户数据库访问,那么问题来了!这个网站的数据库连接池应该设置成多大合适呢? 其实这个问法本身就是有问题的,我们需要反过来问,正确问法应该是: “这个网站的数据库连接池应该设置成多小合适呢?”

Oracle 数据库进行了压力测试,模拟 9600 个并发线程来操作数据库,每两次数据库操作之间 sleep 550ms,注意,开始设置的线程池大小为 2048。

让我们来看看数据库连接池的大小为 2048 性能测试结果的鬼样子: 每个请求要在连接池队列里等待 33ms,获得连接之后,执行SQL需要耗时77ms, CPU 消耗维持在 95% 左右;

接下来,我们将连接池的大小改小点,设置成 1024,其他测试参数不变,结果咋样? "这里,获取连接等待时长基本不变,但是 SQL 的执行耗时降低了!"

哎呦,有长进哦!

接下来,我们再设置小些,连接池的大小降低到 96,并发数等其他参数不变,看看结果如何: 每个请求在连接池队列中的平均等待时间为 1ms, SQL 执行耗时为 2ms。

我去!什么鬼?

我们没调整任何东西,仅仅只是将数据库连接池的大小降低了,这样,就能把之前平均 100ms 响应时间缩短到了 3ms。吞吐量指数级上升啊! 你这也太溜了!

为啥有这种效果? 我们不妨想一下,为啥 Nginx 内部仅仅使用了 4 个线程,其性能就大大超越了 100 个进程的 Apache HTTPD 呢? 追究其原因的话,回想一下计算机科学的基础知识,答案其实非常明显。 要知道,即使是单核 CPU 的计算机也能“同时”运行着数百个线程。但我们其实都知道,这只不过是操作系统快速切换时间片,跟我们玩的一个小把戏罢了。 一核 CPU同一时刻只能执行一个线程,然后操作系统切换上下文,CPU 核心快速调度,执行另一个线程的代码,不停反复,给我们造成了所有进程同时运行假象。 其实,在一核 CPU 的机器上,顺序执行A和B永远比通过时间分片切换“同时”执行A和B要快,其中原因,学过操作系统这门课程的童鞋应该很清楚。 一旦线程的数量超过了 CPU 核心的数量,再增加线程数系统就只会更慢,而不是更快,因为这里涉及到上下文切换耗费的额外的性能。 说到这里,你应该恍然大悟了 ……

当我们在寻找数据库的性能瓶颈时,大致可归为三类: CPU、 磁盘 IO 、网络 IO ,也许你会说,还有内存这一因素?内存的确是需要考虑的,但是比起磁盘IO和网络IO,稍显微不足道,这里就不加了。

假设我们不考虑磁盘 IO 和网络 IO,就很好定论了,在一个 8 核的服务器上,数据库连接数/线程数设置为 8 能够提供最优的性能,如果再增加连接数,反而会因为上下文切换导致性能下降。

大家都知道,数据库通常把数据存储在磁盘上,而磁盘呢,通常是由一些旋转着的金属碟片和一个装在步进马达上的读写头组成的。 读/写头同一时刻只能出现在一个位置,当它需要再次执行读写操作时,它必须“寻址”到另外一个位置才能完成任务。 所以呢?这里就有了寻址耗时,此外还有旋转耗时,读写头需要等待磁盘碟片上的目标数据“旋转到位”才能进行读写操作。使用缓存当然是能够提升性能的,但上述原理仍然适用。

在这段(“I/O等待”)时间内,线程是处于“阻塞”等待状态,也就是说没干啥正事!此时操作系统可以将这个空闲的CPU 核心用于服务其他线程。 这里我们可以总结一下,当你的线程处理的是 I/O 密集型业务时,便可以让线程/连接数设置的比 CPU核心大一些,这样就能够在同样的时间内,完成更多的工作,提升吞吐量。

那么问题又来了? 大小设置成多少合适呢?

这要取决于磁盘,如果你使用的是 SSD 固态硬盘,它不需要寻址,也不需要旋转碟片。打住打住!!!你千万可别理所当然的认为:“既然SSD速度更快,我们把线程数的大小设置的大些吧!!” 结论正好相反!无需寻址和没有旋回耗时的确意味着更少的阻塞,所以更少的线程(更接近于CPU核心数)会发挥出更高的性能。只有当阻塞密集时,更多的线程数才能发挥出更好的性能。

上面我们已经说过了磁盘 IO, 接下来我们谈谈网络 IO!

网络 IO 其实也是非常相似的。通过以太网接口读写数据时也会造成阻塞,10G带宽会比1G带宽的阻塞耗时少一些,而 1G 带宽又会比 100M 带宽的阻塞少一些。 通常情况下,我们把网络 IO 放在第三顺位来考虑,然而有些人会在性能计算中忽略网络 IO 带来的影响。

在这里插入图片描述
上图是 PostgreSQL 的基准性能测试数据,从图中我们可以看到,TPS 在连接数达到 50 时开始变缓。 回过头来想下,Oracle 的性能测试中,测试人员们将连接数从 2048 降到了 96,实际上 96 还是太高了,除非你的服务器 CPU 核心数有 16 或 32。

下面公式由 PostgreSQL 提供,不过底层原理是不变的,它适用于市面上绝大部分数据库产品。 还有,你应该模拟预期的访问量,并通过下面的公式先设置一个偏合理的值,然后在实际的测试中,通过微调,来寻找最合适的连接数大小。

连接数 = ((核心数 * 2) + 有效磁盘数)

核心数不应包含超线程(hyper thread),即使打开了超线程也是如此,如果热点数据全被缓存了,那么有效磁盘数实际是0,随着缓存命中率的下降,有效磁盘数也逐渐趋近于实际的磁盘数。 另外需要注意,这一公式作用于SSD 的效果如何,尚未明了。

好了,按照这个公式,如果说你的服务器 CPU 是 4核 i7 的,连接池大小应该为 ((4*2)+1)=9。 取个整, 我们就设置为 10 吧。你这个行不行啊?10 也太小了吧!

你要是觉得不太行的话,可以跑个性能测试看看,我们可以保证,它能轻松支撑 3000 用户以 6000 TPS 的速率并发执行简单查询的场景。 你还可以将连接池大小超过 10,那时,你会看到响应时长开始增加,TPS 开始下降。

**结论:**你需要的是一个小连接池,和一个等待连接的线程队列 ,假设说你有 10000 个并发访问,而你设置了连接池大小为 10000,你怕是石乐志哦。 改成 1000,太高?改成 100?还是太多了。你仅仅需要一个大小为 10 数据库连接池,然后让剩下的业务线程都在队列里等待就可以了。

**连接池中的连接数量大小应该设置成:**数据库能够有效同时进行的查询任务数(通常情况下来说不会高于 2*CPU核心数)。 你应该经常会看到一些用户量不是很大的 web 应用中,为应付大约十来个的并发,却将数据库连接池设置成 100, 200 的情况。请不要过度配置您的数据库连接池的大小。

实际上,连接池的大小的设置还是要结合实际的业务场景来说事。 比如说,你的系统同时混合了长事务和短事务,这时,根据上面的公式来计算就很难办了。正确的做法应该是创建两个连接池,一个服务于长事务,一个服务于"实时"查询,也就是短事务。 还有一种情况,比方说一个系统执行一个任务队列,业务上要求同一时间内只允许执行一定数量的任务,这时,我们就应该让并发任务数去适配连接池连接数,而不是连接数大小去适配并发任务数。

4 数据库连接池druid参数详解

spring:
    datasource:
        druid:
            # 指定数据源类型为DruidDataSource
            type: com.alibaba.druid.pool.DruidDataSource
            # 数据库连接URL
            url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
            # 数据库用户名
            username: root
            # 数据库密码
            password: "xx123!@#"
            # 连接池初始化时创建的连接数
            initialSize: 5
            # 连接池中最大连接数
            maxActive: 50
            # 连接池中最小空闲连接数
            minIdle: 5
            # 连接池中最大空闲连接数
            maxIdle: 10
            # 获取连接时的最大等待时间(毫秒)
            maxWait: 60000
            # 间隔多久检测一次空闲连接(毫秒)
            timeBetweenEvictionRunsMillis: 60000
            # 连接池中连接最小空闲时间(毫秒)
            minEvictableIdleTimeMillis: 300000
            # 用于检测连接是否有效的SQL语句
            validationQuery: SELECT 1
            # 是否开启空闲连接的检测
            testWhileIdle: true
            # 是否开启连接的检测功能,在获取连接时检测连接是否有效
            testOnBorrow: false
            # 是否开启连接的检测功能,在归还连接时检测连接是否有效
            testOnReturn: false
            # 是否缓存PreparedStatement对象
            poolPreparedStatements: true
            # 缓存PreparedStatement对象的最大数量
            maxPoolPreparedStatementPerConnectionSize: 20
            # 配置监控统计用的filter,允许监控统计
            filters: stat
            # 配置扩展属性,用于监控统计分析SQL性能等
            connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

1、type: 数据源类型,这里使用了Druid连接池的类型。 注意事项:使用Druid连接池的时候需要添加Druid连接池的依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${
    
    druid.version}</version>
</dependency> 

2、url: 数据库连接的URL。 注意事项:需要根据实际情况修改URL。

3、username: 数据库用户名。

4、password: 数据库密码 ;包含一些特殊字符的时候需要加引号。

5、initialSize: 连接池初始化时创建的连接数。 需要根据实际情况设置连接数。连接池创建连接时,会创建initialSize个连接,以确保应用程序在启动时可以立即获取到数据库连接。

6、maxActive: 连接池中最大连接数。 需要根据实际情况设置最大连接数。如果连接池中连接数达到maxActive,则应用程序获取连接的请求将被阻塞,直到有连接被释放。

7、minIdle: 连接池中最小空闲连接数。 需要根据实际情况设置最小空闲连接数。如果连接池中空闲连接数少于minIdle,连接池会创建新的连接以达到minIdle。

8、maxIdle: 连接池中最大空闲连接数。 需要根据实际情况设置最大空闲连接数。如果连接池中空闲连接数超过maxIdle,连接池会关闭多余的连接以避免占用过多的资源。

9、maxWait: 获取连接时的最大等待时间。 需要根据实际情况设置最大等待时间。如果连接池中没有可用连接且已经达到最大连接数,则应用程序获取连接的请求将被阻塞,最多等待maxWait毫秒。

10、timeBetweenEvictionRunsMillis: 间隔多久检测一次空闲连接(毫秒)。 需要根据实际情况设置检测间隔时间。连接池会定期检查空闲连接的状态,如果空闲时间超过minEvictableIdleTimeMillis,则将连接关闭。

11、minEvictableIdleTimeMillis: 连接池中连接最小空闲时间(毫秒)。 需要根据实际情况设置最小空闲时间。连接池会定期检查空闲连接的状态,如果空闲时间超过minEvictableIdleTimeMillis,则将连接关闭。

12、validationQuery: 用于检测连接是否有效的SQL语句。 需要根据实际情况设置SQL语句。连接池会使用这个SQL语句来检测连接是否有效。

13、testWhileIdle: 是否开启空闲连接的检测。 需要根据实际情况设置是否开启空闲连接的检测。如果开启,连接池会定期检查空闲连接的状态。

14、testOnBorrow: 是否开启连接的检测功能, 在获取连接时检测连接是否有效。 需要根据实际情况设置是否开启连接的检测功能。如果开启,连接池在获取连接时会检查连接是否有效。

15、testOnReturn: 是否开启连接的检测功能,在归还连接时检测连接是否有效。 需要根据实际情况设置是否开启连接的检测功能。如果开启,连接池在归还连接时会检查连接是否有效。

16、poolPreparedStatements: 是否缓存PreparedStatement对象。 需要根据实际情况设置是否缓存PreparedStatement对象。如果开启,连接池会缓存PreparedStatement对象以提高性能。

17、maxPoolPreparedStatementPerConnectionSize: 缓存PreparedStatement对象的最大数量。 需要根据实际情况设置缓存的最大数量。如果开启了缓存PreparedStatement对象,连接池会限制每个连接缓存的最大数量。

maxPoolPreparedStatementPerConnectionSize是Druid连接池的一个配置项,用于配置连接池中缓存的PreparedStatement的最大数量。 PreparedStatement是预编译的SQL语句,可以提高SQL执行的效率和安全性,避免SQL注入等安全问题。 在使用Druid连接池时,当开启了缓存PreparedStatement功能时,每个连接都会缓存一定数量的PreparedStatement对象,以便在需要执行SQL语句时能够快速获取。

而maxPoolPreparedStatementPerConnectionSize就是用于配置每个连接中缓存的PreparedStatement的最大数量。 需要注意的是,缓存PreparedStatement虽然可以提高SQL执行的效率,但同时也会占用一定的内存资源。因此,需要根据实际情况进行合理配置。 默认情况下,该属性的值为10。

在上面的配置中,maxPoolPreparedStatementPerConnectionSize: 20 ;表示将每个连接中缓存的PreparedStatement的最大数量设置为20个。

18、filters: 配置监控统计用的filter,允许监控统计。 如果要使用Druid的监控功能,需要配置此项。stat表示使用Druid的监控功能。

19、connectionProperties: 配置扩展属性,用于监控统计分析SQL性能等。 druid.stat.mergeSql和druid.stat.slowSqlMillis是两个与SQL监控有关的属性。

druid.stat.mergeSql用于配置是否合并SQL。当该属性设置为true时,Druid会将相同的SQL语句合并为一条,以节省SQL统计的开销和提高统计精度。 默认情况下,该属性的值为false。

druid.stat.slowSqlMillis用于配置SQL执行的时间阈值,单位为毫秒。当一条SQL执行的时间超过该阈值时,Druid会将该SQL记录到慢SQL列表中,以便进行分析和优化。默认情况下,该属性的值为3000毫秒。 在上面的配置中,druid.stat.mergeSql=true表示启用SQL合并功能, druid.stat.slowSqlMillis=5000表示将SQL执行的时间阈值设置为5000毫秒。 这些属性的具体含义和配置方法,可以参考Druid的官方文档。

猜你喜欢

转载自blog.csdn.net/zs18753479279/article/details/132076267