数据库连接池性能优化,连接数到底应该设置多大?


1. 数据库连接池与 ThreadLocal

 

数据库连接池是线程安全的,但数据库连接不是!

          数据库连接池就用用来保存数据库连接的一个池子。每当我们的业务代码需要和数据库进行交互时,就从这个池子里面取出一个数据库连接,然后在这个连接上进行查增删改操作。使用结束后,业务代码再将这个连接归还给这个池子,然后这个连接就可以被其他业务代码继续使用了。

          数据库连接池本身的设计,也避免了多个线程同时共享一个连接的情况,一个连接必须先向连接池申请才能获得,使用结束必须归还连接池才能给下一个线程使用。从这个过程中我们可以看到,数据库连接池是可以在多个线程中使用的,因此数据库连接池必定是线程安全的。

          然而数据库连接Connection肯定不是线程安全的,如JDBC的对象Connection, Statement / PreparedStatementResultSet 都不是线程安全的,也不是资源安全的。不能在多个线程中共享这些对象。假如所有请求servlet的连接,使用的都是一个Connection,这个就是很致命的了,多个人使用同一个连接,算上延迟啥的,天知道数据会成什么样!
 
数据库连接池为什么使用ThreadLocal?

Thread内部有一个ThreadLocalMap对象,类似一个map,keyThreadLocalvalue为设置的值。

           因此我们要保证Connection对每个线程都是唯一的,这个时候就可以用到ThreadLocal了,保证每个线程都有自己的连接,这样就互相不干扰了。

          另外,假如在一个事务方法中,涉及了多个DAO操作,如果不使用ThreadLocal,那么其中一个DAO操作执行完毕就会关闭连接,下一个DAO还会从池中获取数据库连接,两个 DAO 就用到了两个Connection,这样的话是没有办法完成一个事务的。如果是从 ThreadLocal 中获得 Connection 的话,那么这些 DAO操作 就会被纳入到同一个 Connection 之下,多个dao操作使用的都是一个数据库连接,这样就可以更好地控制事务!

ThreadLocal存在的问题:内存泄漏:

           当在线程池中使用ThreadLocal 时,可能会导致内存泄漏问题:因为线程池的任务一般不会被销毁,当一个线程执行完任务后,紧接着会执行下一个任务,那么在第一个任务中被设置的ThreadLocal 就永远不会使用了,但是线程没有销毁,该对象也不会被回收,这就发生了内存泄漏。当任务很多时,每个任务都会生成ThreadLocal对象,且都不会被销毁,这就会逐渐侵蚀剩余的内存空间,导致可用内存越来越少!

          解决方案:为当ThreadLocal对象使⽤完之后,应该要把设置的key,value,也就是Entry对象进⾏回收,需要显式的调用ThreadLocal.remove()方法,删除对应的数据,释放空间


2. 数据库连接数测试

        假如你有一个网站,压力有个1万上下的并发访问——也就是说差不多2万左右的TPS。那么这个网站的数据库连接池应该设置成多大呢?可能更正确的问法是:这个网站的数据库连接池应该设置成多小呢?下面请看一下这个测试视频http://www.dailymotion.com/video/x2s8uec,(视频是英文解说且没有字幕,简单概括一下如下)

主题:视频中对Oracle数据库进行压力测试,9600并发线程进行数据库操作,每两次访问数据库的操作之间sleep 550ms

  1. 一开始设置的线程池大小为2048。每个请求要在连接池队列里等待33ms,获得连接后执行SQL(吞吐量)需要77ms
    在这里插入图片描述
  2. 然后设置线程池大小为1024。能看到,中间件连接池从2048减半之后,连接池队列里等待事件没怎么变,但执行SQL耗时减少了一半。
    在这里插入图片描述
  3. 接下来,设置线程池大小为96,并发线程数仍然是9600不变。连接池队列里等待事件几乎没了,吞吐量被优化到了2ms。查询性能得到了巨大提升!
    在这里插入图片描述

没有调整任何其他东西,仅仅只是缩小了中间件层的数据库连接池,就把请求响应时间从77ms左右缩短到了2ms。 还有同样的案例:nginx只用4个线程发挥出的性能就大大超越了100个进程的Apache HTTPD。这是为什么呢?


3. 透过现象看原理

        从上面的测试,我们发现数据库连接数越小,性能越好。下面来探讨一下现象下的本质。

        即使是单核CPU的计算机也能“同时”运行数百个线程。但我们都知道这只不过是操作系统用时间分片玩的一个小把戏。一颗CPU核心同一时刻只能执行一个线程,然后操作系统切换上下文,核心开始执行另一个线程的代码,以此类推。给定一颗CPU核心,其顺序执行A和B永远比通过 时间分片“同时”执行A和B要快,因为顺序执行不需要上下文切换,上下文切换也是耗时的。这是一条计算机科学的基本法则。一旦线程的数量超过了CPU核心的数量,再增加线程数系统就只会更慢,而不是更快。

        上面的说法只能说是接近真理,但还并没有这么简单,有一些其他的因素需要加入。当我们寻找数据库的性能瓶颈时,总是可以将其归为三类:CPU、磁盘、网络。把内存加进来也没有错,但比起磁盘和网络,内存的带宽要高出好几个数量级,所以就先不加了。

        如果我们无视磁盘和网络,那么结论就非常简单。在一个8核的服务器上,设定连接/线程数为8能够提供最优的性能,再增加连接数就会因上下文切换的损耗导致性能下降。但数据库通常把数据存储在磁盘上,磁盘又通常是由一些旋转着的金属碟片和一个装在步进马达上的读写头组成的。它必须“寻址”到另外一个位置来执行另一次读写操作。所以就有了寻址的耗时,此外还有旋回耗时,读写头需要等待碟片上的目标数据“旋转到位”才能进行操作。在磁盘寻址这一时间段(即"I/O等待")内,线程是在“阻塞”着等待磁盘,此时操作系统可以将那个空闲的CPU核心用于服务其他线程。所以,由于线程总是在I/O上阻塞,我们可以让线程/连接数比CPU核心多一些,这样能够在同样的时间内完成更多的工作。

        那么让线程/连接数比CPU核心多多少呢?这要取决于磁盘。较新型的SSD不需要寻址,也没有旋转的碟片。可别想当然地认为“SSD速度更快,所以我们应该增加线程数”,恰恰相反,无需寻址和没有旋回耗时意味着更少的阻塞,所以更少的线程,更接近于CPU核心数会发挥出更高的性能。只有当阻塞创造了更多的执行机会时,更多的线程数才能发挥出更好的性能。


4. 如何合理设置数据库连接数

        明白了原理后,就可以合理的设置连接数来优化性能。

        计算公式:连接数 = (核心数 * 2) + 有效磁盘数

        按这个公式,你的4核i7数据库服务器的连接池大小应该为((4 * 2) + 1) = 9。取个整就算是是10吧。不要觉得太小,它能轻松搞定3000用户以6000TPS的速率并发执行简单查询的场景。如果连接池大小超过10,你会看到响应时长开始增加,TPS开始下降。因为随着连接数增多,cpu上下文切换频繁,增加请求耗时,而遵循这个公式,可以在当前线程进行IO阻塞时,适当的就行上下文切换,避免了切换频繁带来的耗时!*当然,这也不是一个万能公式,具体设置的值还需要进行实际压力测试才能决定!

        在上面Oracle的视频中,他们把连接数从2048降到了96,提升了大量想能。实际上96都太高了,除非服务器有16或32颗核心。

结论:尽量设置一个小的连接池,和一个充满了等待连接的线程的队列


5. Druid连接池关闭不活跃连接时抛 Connection timed out 异常!

生产环境下,时不时打印下面这个异常,但不影响主业务
在这里插入图片描述

解决方案

spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.keep-alive=true

原因如下:

Druid连接池关闭不活跃连接时抛 Connection timed out 异常原因!

池化技术探索

### 回答1: 在 Spring Boot 项目中使用 YAML 文件配置连接 Galera MySQL 集群的示例如下: ``` spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql:replication://host1,host2,host3/dbname username: user password: pass driver-class-name: com.mysql.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 test-on-borrow: true test-on-return: true test-while-idle: true time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 validation-query-timeout: 1 max-pool-prepared-statement-per-connection-size: 20 share-prepared-statements: true use-global-data-source-stat: true ``` 其中,url 的格式为 `jdbc:mysql:replication://host1,host2,host3/dbname`,host1、host2、host3 分别为集群中的三个节点的地址,dbname 为数据库名称。username 和 password 分别为数据库的用户名和密码。 另外,由于 Galera 集群需要使用特定的 JDBC 驱动,需要在 pom.xml 中引入以下依赖: ``` <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency> ``` 这里我使用了阿里巴巴的Druid数据库连接池,需要引入: ``` <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.22</version> </dependency> ``` 以上是一个简单的示例,实际配置还需根据具体情况进行调整。 ### 回答2: 下面是一个示例的Spring Boot YAML文件配置连接Galera MySQL集群的设置: ```yaml spring: datasource: url: jdbc:mysql:replication://node1:3306,node2:3306,node3:3306/mydatabase?useUnicode=true&characterEncoding=utf8&autoReconnect=true username: your_username password: your_password driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource hikari: maximum-pool-size: 10 jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: ddl-auto: update naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy ``` 上述配置中,`spring.datasource.url`是连接Galera MySQL集群的URL。在该URL中,我们指定了集群中的多个节点(以逗号分隔),每个节点使用相同的端口(3306)并共享相同的数据库名(mydatabase)。你需要根据自己的实际配置替换`node1`,`node2`和`node3`为你的集群节点的主机名或IP地址。 `spring.datasource.username`和`spring.datasource.password`是连接数据库所需的用户名和密码。 其他的配置项如`hikari.maximum-pool-size`是连接池中最大连接数设置,`jpa.database-platform`是定义使用的数据库方言,`hibernate.ddl-auto`是定义Hibernate在启动时如何处理数据库模式的设置,`hibernate.naming-strategy`是定义Hibernate采用的命名策略。 你可以根据你的具体需求对上述配置进行调整。 ### 回答3: Galera是一个基于MySQL的高可用性解决方案,它支持多主复制和同步复制。Spring Boot是一个用于构建Java应用程序的框架,提供了方便的配置和自动化的构建。 要使用Spring Boot配置连接Galera MySQL集群,可以按照以下步骤进行: 1. 首先,确保你已经在Spring Boot项目的依赖中添加了MySQL驱动程序的依赖。可以在pom.xml文件中添加以下依赖: ```xml <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> </dependencies> ``` 2. 在src/main/resources目录下创建一个名为application.yml的文件,用于配置数据库连接信息。 ```yaml spring: datasource: url: jdbc:mysql://<galera-cluster>:<port>/<database-name>?useSSL=false&allowPublicKeyRetrieval=true username: <username> password: <password> driver-class-name: com.mysql.cj.jdbc.Driver ``` 上述配置中,需要将`<galera-cluster>`替换为Galera集群的连接信息,`<port>`替换为Galera集群的端口号,`<database-name>`替换为要连接的数据库名称,`<username>`和`<password>`替换为数据库的用户名和密码。 3. 在应用程序的Java类中,通过使用Spring注解来访问数据库。例如,可以创建一个名为UserRepository的接口类,并使用`@Repository`注解将其声明为Repository。 ```java @Repository public interface UserRepository extends JpaRepository<User, Long> { // 在这里添加你的数据库操作方法 } ``` 这样,你就可以使用Spring Boot和Galera MySQL集群进行数据库连接和操作了。通过配置正确的Galera集群连接信息,可以在应用程序中使用数据库的各种功能,并实现高可用性和同步复制。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值