你认为的.NET数据库连接池,真的是全部吗?

Posted 有态度的马甲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你认为的.NET数据库连接池,真的是全部吗?相关的知识,希望对你有一定的参考价值。

一般我们的项目中会使用1到2个数据库连接配置,同程艺龙的数据库连接被收拢到配置中心,由DBA统一配置和维护,业务方通过某个字符串配置拿到的是开箱即用的Connection对象

DBA能在对业务方无侵入的情况下,让大规模微服务实例切换数据库连接,之后DBA要求对旧数据库的连接必须立即被清空, 那么问题来了: **dotnet程序怎么清空数据库连接?

如果有同学不知道DBA做这个要求的目的,那我啰嗦一下:
应用程序不再使用旧连接时,理论上你的连接池要被完全清空,因为单纯的释放连接,只会让连接池中的Connection处于Sleep状态,依旧维持了短时间的物理连接,这个短时间其实是不必要的占用,影响了旧连接数据库的吞吐量。

前置知识背景

回答这个问题之前, 我们还是先梳理一下:

  • dotnet进程, 数据库进程;
  • 两者都存在连接池,.NET数据库连接池, 数据库连接池。

.net能直接维护的是 .net数据库连接池, 本次我们通过清空.NET数据库连接池,清理应用进程与具体数据库的物理连接

1. 先掌握.NET数据库连接池

数据库连接是一个耗时的行为,大多数应用程序只使用1到2种数据库连接,为了最小化打开连接的成本,.net使用了一种称为连接池的优化技术。

.NET 数据库连接池程序维护了数据库物理连接,
通过为每个特定的连接配置保持一组活动的连接对象来管理连接。

每当应用程序尝试Open连接,池程序就会在池中找到可用的连接,如果有则返回给调用者;
应用程序Close连接对象时,池程序将连接对象返回到池中(Sleep), 这个连接可以在下一次Open调用中重用。

2. .NET数据库连接池是如何建立的?

在一个进程中,相同的连接配置才能被池化,.NET为不同连接配置维护了不同的连接池。

进程级别、  
连接字符串相同、  
连接字符串关键key顺序相同。  
(同一连接提供的关键字顺序不同将被分到不同的池)。

池初次建立的时候,创建了最小数量的connection对象,为满足后续的业务需求,池会渐增connection对象,直至达到Max Pool Size最大连接数(默认是100)。

后续请求如果面临.NET数据库连接池满的情况,将会进队列等待 ,超时15s抛出异常。

在一个应用程序中,有如下代码:

using (SqlConnection connection = new SqlConnection(  
  "Integrated Security=SSPI;Initial Catalog=Northwind"))  
      
        connection.Open();
        // Pool A is created.  
      
  
using (SqlConnection connection = new SqlConnection(  
  "Integrated Security=SSPI;Initial Catalog=pubs"))  
      
        connection.Open();
        // Pool B is created because the connection strings differ.  
      
  
using (SqlConnection connection = new SqlConnection(  
  "Integrated Security=SSPI;Initial Catalog=Northwind"))  
      
        connection.Open();
        // The connection string matches pool A.  
      

上面创建了三个Connection对象,但是只形成了两个数据库连接池

还是以上代码,如果有两个相同的应用程序,理论上就形成了四个数据库连接池。

3. 连接池中的连接什么时候被移除?

数据库连接对象,在被释放或者关闭的时候,会回收进连接池。

We strongly recommend that you always close the connection when you are finished using it so that the connection will be returned to the pool. You can do this using either the Close or Dispose methods of the Connection object, or by opening all connections inside a using statement in C#.

连接池中的连接空闲4-8 分钟,池程序会移除这个连接。

应用程序关闭,连接池直接被清空。


.NET 如何清空.NET连接池?

有了以上知识背景

我们再来回顾一下DBA的要求,切换原连接配置的时候,清空对原数据库的连接。

.NET这边只能操作.NET数据库连接池。

.NET提供ClearAllPools、ClearPool静态方法用于清空连接池, 目前看sqlserver,mysql客户端库均实现了这两个方法。

  • ClearAllPools: 清空与这个DBProvider相关的所有连接池

  • ClearPool(DBConnection conn) 清空与这个连接对象相关的连接池

ClearAllPools() 过于暴力,直接清空了此时与这个provider相关的连接池, 可能对于其他应用进程的数据库连接造成损流,

要使用细粒度的进程相关的 ClearPool(DBConnection conn) 方法, 本应用进程切换数据库连接之后,只清空与本次连接相关的.NET 数据库连接池。

光说不练不验证,不是我的风格。

天锤压测/queryapi 产生一个包含大量连接对象的连接池;
适当的时候,/clearpoolapi清空.NET连接池。

 using MySql.Data.MySqlClient;       // 引入MySql.Data.dll库文件  

 public class MySqlController : Controller
    
        // GET: MySql
        [Route("query")]
        public string Index()
        
            var s = "User ID=teinfra_neo_netreplay;Password=123456;DataBase=teinfra_neo_netreplay;Server=10.100.41.196;Port=3980;Min Pool Size=1;Max Pool Size=28;CharSet=utf8;";
            using (var conn = new MySqlConnection(s))
            
                var comm = conn.CreateCommand();
                comm.CommandText = "select count(*) from usertest;";
                conn.Open();
                var ret = comm.ExecuteScalar();

                comm.CommandText = "select count(*) from information_schema.PROCESSLIST WHERE HOST like  '10.22.12.245%';";
                var len = comm.ExecuteScalar();
                return $"查询结果:ret ,顺便查一下当前连接池的连接对象个数: len";
            ;
        

        [Route("clearpool")]
        public string Switch()
        
            var s = "User ID=teinfra_neo_netreplay;Password=123456;DataBase=teinfra_neo_netreplay;Server=10.100.41.196;Port=3980;Min Pool Size=1;Max Pool Size=28;CharSet=utf8;";
            using (var conn = new MySqlConnection(s))
            
                conn.Open();
                MySqlConnection.ClearPool(conn);
            ;

            using (var conn = new MySqlConnection(s))
            
                conn.Open();
                var comm = conn.CreateCommand();
                comm.CommandText = "select count(*) from information_schema.PROCESSLIST WHERE HOST like  '10.22.12.245%';";
                var len = comm.ExecuteScalar();
                return $"之前已经清空连接池, 此次查询连接池有 v1  个连接对象";
            

        
    

1. 经过压测工具

2. mysql数据库对比

mysql的连接数查询命令, (host是web服务器IP):
select * from information_schema.PROCESSLIST WHERE HOST like '10.22.12.245%';

3. 调用/clearpoolapi,清空.NET连接池, 进而直接影响 mysql数据库的物理连接。

bingo,清空.NET连接池===> 清理mysql数据库物理连接 的理论得到验证。

干货旁白

这是我在同程艺龙最近爬的比较深的坑位,
从本次实践中理解了.NET数据库连接池的定义方式、

清空进程级别.NET数据库连接池===> 清理该进程与数据库的物理连接。

对祖传代码的改造,.NET数据获取组件SDK 确实提高了原数据库的吞吐量。

希望本文设计考量、理论+论证的行文思路对于读者有所帮助, 再次感谢有心读者取关、再关注。

  • https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/connection-pooling

Java 线程池的实现原理,你真的理解吗?

点击关注公众号,实用技术文章及时了解

来源:guobinhit.blog.csdn.net/article/

details/105654919

1 线程状态

既然要说线程,我们就先来了解一下线程的几种状态:

public enum State 
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    

如上述代码所示,其来自于java.lang.Thread类,State为Thread类的内部公共枚举类,表示线程的 6 种状态。

  • NEW,新建状态。尚未启动的线程的状态。

  • RUNNABLE,可运行状态。处于RUNNABLE状态的线程正在 JVM 中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。

  • BLOCKED,阻塞状态。处于BLOCKED状态的线程正在等待监视器锁以便进入同步代码块或同步方法,或者在调用Object.wait()方法后准备重入同步代码块或同步方法。

  • WAITING,等待状态。处于WAITING状态的线程正在等待另一个线程执行特定的动作,例如需要另一个线程调用Object.notify()或者Object.notifyAll()进行唤醒。当调用以下无参方法时,线程会进入WAITING状态:

    • Object.wait()

    • Thread.join()

    • LockSupport.park()

  • TIMED_WAITING,具有指定等待时间的线程状态。当调用以下具有指定正等待时间的方法时,线程会进入TIMED_WAITING状态:

    • Thread.sleep(millis)

    • Object.wait(timeout)

    • Thread.join(millis)

    • LockSupport.parkNanos(blocker, nanos)

    • LockSupport.parkUntil(blocker, deadline)

  • TERMINATED,终止状态。当线程执行完成后,处于TERMINATED状态。

如上图所示,展示了线程在各种状态之间流转的详细图谱。

2 线程池

2.1 线程池的作用

我们知道,线程的创建是一项比较消耗资源的操作,如果我们频繁的创建线程,不仅会大量消耗内存资源,也会导致 CPU 的使用率飙高,因为线程的切换也会导致 CPU 的状态切换。那么,既然创建线程这么消耗资源,又该如何解决这个问题?

线程池就是为了解决这个问题而设计的,通过使用线程池,可以达到以下效果:

  • 降低资源消耗:通过重用线程来降低线程创建和销毁的资源消耗。

  • 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。

  • 提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

2.2 线程池的实现

在 Java 语言中,线程池是通过ThreadPoolExecutor实现的,其全参构造器为:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    

其中,各参数的含义分别为:

  • corePoolSize,核心线程数,表示要保留在线程池中的线程数,即使它们处于空闲状态,如果设置了allowCoreThreadTimeOut参数,则该参数的最小值可以为零。

  • maximumPoolSize,最大线程数,表示线程池中允许同时存在的最大线程数量。

  • keepAliveTime,存活时间,当线程池中的线程数量大于corePoolSize时,该参数表示多余的空闲线程在终止之前等待新任务的最长时间。也就是说,如果多余的空闲线程在等待时间超过keepAliveTime之后仍没有收到任务,则自动销毁。

  • unit,时间单位,表示keepAliveTime参数的时间单位。

  • workQueue,工作队列,在任务被执行前,该队列用于保存任务。该队列只能持有被execute方法提交的Runnable类型的任务。

  • threadFactory,线程工厂,执行器创建新线程时使用的工厂。

  • handler,拒绝策略,当达到线程数量边界和队列容量而阻止执行时使用的处理策略。

特别地,在ThreadPoolExecutor中,重载了很多构造器,用于满足我们不同的使用需求。其中,参数最少的重载构造器是:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) 
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    

由上述代码可知,在创建一个ThreadPoolExecutor的时候,我们至少需要指定corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueue这五个参数,而threadFactory和handler,则是可以使用默认值:

  • threadFactory,默认使用DefaultThreadFactory线程工厂;

  • handler,默认使用AbortPolicy拒绝策略。

对于线程工厂,在Executors类内部,还提供了一个PrivilegedThreadFactory工厂类,用于捕获访问控制上下文和类加载器。当然,我们也可以通过实现ThreadFactory接口或继承DefaultThreadFactory类来自定义线程池的工厂类。

对于拒绝策略,在ThreadPoolExecutor类内部,则是提供了四种拒绝策略的实现,分别是:

  • AbortPolicy,默认策略,直接抛出异常;

  • DiscardPolicy,直接丢弃任务;

  • DiscardOldestPolicy,丢弃阻塞队列中靠最前的任务,然后调用execute方法重试;

  • CallerRunsPolicy,使用调用者所在的线程执行任务。

同理,我们也可以自己实现RejectedExecutionHandler接口来自定义线程池的拒绝策略。说到这里,我们可能会有一个疑问,那就是:什么时候使用拒绝策略呢?当阻塞队列满了并且没有空闲的工作线程时,如果继续提交任务,就会使用拒绝策略来“拒绝”任务了。在这里,我们可以详细的梳理一遍ThreadPoolExecutor的执行过程:

  • 创建ThreadPoolExecutor线程池执行器,默认并不会立即创建执行线程。

  • 接收到一个新任务之后,检查当前执行线程数量是否小于corePoolSize数量,如果是,则是创建一个新的执行线程,来执行该任务;否则,将该任务放入工作队列。

  • 如果工作队列也满了,则判断当前执行线程数量是否小于maximumPoolSize数量,如果是,则创建一个新的执行线程,来执行该任务;否则,执行拒绝策略。

  • 如果当前执行线程的数量大于corePoolSize并且没有新的任务需要处理,则在等待keepAliveTime时间之后,自动销毁当前执行线程数量 - corePoolSize数量的线程,使执行线程的数量维持在corePoolSize的数量。

2.2.1 线程池内部状态

ThreadPoolExecutor中,使用以下常量值表示线程的状态:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    private static int runStateOf(int c)      return c & ~CAPACITY; 
    private static int workerCountOf(int c)   return c & CAPACITY; 
    private static int ctlOf(int rs, int wc)  return rs | wc; 

其中,AtomicInteger类型的变量ctl功能非常强大:使用低 29 位表示线程池中的线程数量,使用高 3 位表示线程池的运行状态:

  • RUNNING:-1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;

  • SHUTDOWN:0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;

  • STOP :1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;

  • TIDYING :2 << COUNT_BITS,即高3位为010;

  • TERMINATED:3 << COUNT_BITS,即高3位为011;

在ThreadPoolExecutor创建完成后,我们需要调用execute方法…

推荐

主流Java进阶技术(学习资料分享)

Java面试题宝典

加入Spring技术开发社区

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

以上是关于你认为的.NET数据库连接池,真的是全部吗?的主要内容,如果未能解决你的问题,请参考以下文章

你真的知道如何设置数据库连接池的大小吗

面试官常问的线程池,你真的了解吗?

你真的熟悉数据连接池吗?手写实现连接池

用了这么久的数据库连接池,你知道原理吗?

redis默认连接池的这个坑,你真的知道吗?

数据库的连接池,你真的懂了吗