搞懂Druid之连接创建和销毁

news2024/11/15 12:56:23

前言

Druid是阿里开源的数据库连接池,是阿里监控系统Dragoon的副产品,提供了强大的可监控性和基于Filter-Chain的可扩展性。

本篇文章将对Druid数据库连接池的连接创建销毁进行分析。分析Druid数据库连接池的源码前,需要明确几个概念。

  1. Druid数据库连接池中可用的连接存放在一个数组connections中;
  2. Druid数据库连接池做并发控制,主要靠一把可重入锁以及和这把锁关联的两个Condition对象;
public DruidAbstractDataSource(boolean lockFair) {
   lock = new ReentrantLock(lockFair);

   notEmpty = lock.newCondition();
   empty = lock.newCondition();
}
  1. 连接池没有可用连接时,应用线程会在notEmpty上等待,连接池已满时,生产连接的线程会在empty上等待;
  2. 对连接保活,就是每间隔一定时间,对达到了保活间隔周期的连接进行有效性校验,可以将无效连接销毁,也可以防止连接长时间不与数据库服务端通信。

Druid版本:1.2.11

正文

一. DruidDataSource连接创建

DruidDataSource连接的创建由CreateConnectionThread线程完成,其run() 方法如下所示。

public void run() {
    initedLatch.countDown();

    long lastDiscardCount = 0;
    int errorCount = 0;
    for (; ; ) {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e2) {
            break;
        }

        long discardCount = DruidDataSource.this.discardCount;
        boolean discardChanged = discardCount - lastDiscardCount > 0;
        lastDiscardCount = discardCount;

        try {
            // emptyWait为true表示生产连接线程需要等待,无需生产连接
            boolean emptyWait = true;

            // 发生了创建错误,且池中已无连接,且丢弃连接的统计没有改变
            // 此时生产连接线程需要生产连接
            if (createError != null
                    && poolingCount == 0
                    && !discardChanged) {
                emptyWait = false;
            }

            if (emptyWait
                    && asyncInit && createCount < initialSize) {
                emptyWait = false;
            }

            if (emptyWait) {
                // 池中已有连接数大于等于正在等待连接的应用线程数
                // 且当前是非keepAlive场景
                // 且当前是非连续失败
                // 此时生产连接的线程在empty上等待
                // keepAlive && activeCount + poolingCount < minIdle时会在shrink()方法中触发emptySingal()来添加连接
                // isFailContinuous()返回true表示连续失败,即多次(默认2次)创建物理连接失败
                if (poolingCount >= notEmptyWaitThreadCount
                        && (!(keepAlive && activeCount + poolingCount < minIdle))
                        && !isFailContinuous()
                ) {
                    empty.await();
                }

                // 防止创建超过maxActive数量的连接
                if (activeCount + poolingCount >= maxActive) {
                    empty.await();
                    continue;
                }
            }

        } catch (InterruptedException e) {
            // 省略
        } finally {
            lock.unlock();
        }

        PhysicalConnectionInfo connection = null;

        try {
            connection = createPhysicalConnection();
        } catch (SQLException e) {
            LOG.error("create connection SQLException, url: " + jdbcUrl
                    + ", errorCode " + e.getErrorCode()
                    + ", state " + e.getSQLState(), e);

            errorCount++;
            if (errorCount > connectionErrorRetryAttempts
                    && timeBetweenConnectErrorMillis > 0) {
                // 多次创建失败
                setFailContinuous(true);
                // 如果配置了快速失败,就唤醒所有在notEmpty上等待的应用线程
                if (failFast) {
                    lock.lock();
                    try {
                        notEmpty.signalAll();
                    } finally {
                        lock.unlock();
                    }
                }

                if (breakAfterAcquireFailure) {
                    break;
                }

                try {
                    Thread.sleep(timeBetweenConnectErrorMillis);
                } catch (InterruptedException interruptEx) {
                    break;
                }
            }
        } catch (RuntimeException e) {
            LOG.error("create connection RuntimeException", e);
            setFailContinuous(true);
            continue;
        } catch (Error e) {
            LOG.error("create connection Error", e);
            setFailContinuous(true);
            break;
        }

        if (connection == null) {
            continue;
        }

        // 把连接添加到连接池
        boolean result = put(connection);
        if (!result) {
            JdbcUtils.close(connection.getPhysicalConnection());
            LOG.info("put physical connection to pool failed.");
        }

        errorCount = 0;

        if (closing || closed) {
            break;
        }
    }
}

CreateConnectionThreadrun() 方法整体就是在一个死循环中不断的等待,被唤醒,然后创建线程。当一个物理连接被创建出来后,会调用DruidDataSource#put方法将其放到连接池connections中,put() 方法源码如下所示。

protected boolean put(PhysicalConnectionInfo physicalConnectionInfo) {
    DruidConnectionHolder holder = null;
    try {
        holder = new DruidConnectionHolder(DruidDataSource.this, physicalConnectionInfo);
    } catch (SQLException ex) {
        // 省略
        return false;
    }

    return put(holder, physicalConnectionInfo.createTaskId, false);
}

private boolean put(DruidConnectionHolder holder,
                    long createTaskId, boolean checkExists) {
    // 涉及到连接池中连接数量改变的操作,都需要加锁
    lock.lock();
    try {
        if (this.closing || this.closed) {
            return false;
        }

        // 池中已有连接数已经大于等于最大连接数,则不再把连接加到连接池并直接返回false
        if (poolingCount >= maxActive) {
            if (createScheduler != null) {
                clearCreateTask(createTaskId);
            }
            return false;
        }

        // 检查重复添加
        if (checkExists) {
            for (int i = 0; i < poolingCount; i++) {
                if (connections[i] == holder) {
                    return false;
                }
            }
        }

        // 连接放入连接池
        connections[poolingCount] = holder;
        // poolingCount++
        incrementPoolingCount();

        if (poolingCount > poolingPeak) {
            poolingPeak = poolingCount;
            poolingPeakTime = System.currentTimeMillis();
        }

        // 唤醒在notEmpty上等待连接的应用线程
        notEmpty.signal();
        notEmptySignalCount++;

        if (createScheduler != null) {
            clearCreateTask(createTaskId);

            if (poolingCount + createTaskCount < notEmptyWaitThreadCount
                    && activeCount + poolingCount + createTaskCount < maxActive) {
                emptySignal();
            }
        }
    } finally {
        lock.unlock();
    }
    return true;
}

put() 方法会先将物理连接从PhysicalConnectionInfo中获取出来并封装成一个DruidConnectionHolderDruidConnectionHolder就是Druid连接池中的连接。新添加的连接会存放在连接池数组connectionspoolingCount位置,然后poolingCount会加1,也就是poolingCount代表着连接池中可以获取的连接的数量。

二. DruidDataSource连接销毁

DruidDataSource连接的销毁由DestroyConnectionThread线程完成,其run() 方法如下所示。

public void run() {
    // run()方法只要执行了,就调用initedLatch#countDown
    initedLatch.countDown();

    for (; ; ) {
        // 每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask的run()方法
        try {
            if (closed || closing) {
                break;
            }

            if (timeBetweenEvictionRunsMillis > 0) {
                Thread.sleep(timeBetweenEvictionRunsMillis);
            } else {
                Thread.sleep(1000);
            }

            if (Thread.interrupted()) {
                break;
            }

            // 执行DestroyTask的run()方法来销毁需要销毁的连接
            destroyTask.run();
        } catch (InterruptedException e) {
            break;
        }
    }
}

DestroyConnectionThreadrun() 方法就是在一个死循环中每间隔timeBetweenEvictionRunsMillis的时间就执行一次DestroyTaskrun() 方法。DestroyTask#run方法实现如下所示。

public void run() {
    // 根据一系列条件判断并销毁连接
    shrink(true, keepAlive);

    // RemoveAbandoned机制
    if (isRemoveAbandoned()) {
        removeAbandoned();
    }
}

DestroyTask#run方法中会调用DruidDataSource#shrink方法来根据设定的条件来判断出需要销毁和保活的连接。DruidDataSource#shrink方法如下所示。

// checkTime参数表示在将一个连接进行销毁前,是否需要判断一下空闲时间
public void shrink(boolean checkTime, boolean keepAlive) {
    // 加锁
    try {
        lock.lockInterruptibly();
    } catch (InterruptedException e) {
        return;
    }

    // needFill = keepAlive && poolingCount + activeCount < minIdle
    // needFill为true时,会调用empty.signal()唤醒生产连接的线程来生产连接
    boolean needFill = false;
    // evictCount记录需要销毁的连接数
    // keepAliveCount记录需要保活的连接数
    int evictCount = 0;
    int keepAliveCount = 0;
    int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
    fatalErrorCountLastShrink = fatalErrorCount;

    try {
        if (!inited) {
            return;
        }

        // checkCount = 池中已有连接数 - 最小空闲连接数
        // 正常情况下,最多能够将前checkCount个连接进行销毁
        final int checkCount = poolingCount - minIdle;
        final long currentTimeMillis = System.currentTimeMillis();
        // 正常情况下,需要遍历池中所有连接
        // 从前往后遍历,i为数组索引
        for (int i = 0; i < poolingCount; ++i) {
            DruidConnectionHolder connection = connections[i];

            // 如果发生了致命错误(onFatalError == true)且致命错误发生时间(lastFatalErrorTimeMillis)在连接建立时间之后
            // 把连接加入到保活连接数组中
            if ((onFatalError || fatalErrorIncrement > 0)
                    && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
                keepAliveConnections[keepAliveCount++] = connection;
                continue;
            }

            if (checkTime) {
                // phyTimeoutMillis表示连接的物理存活超时时间,默认值是-1
                if (phyTimeoutMillis > 0) {
                    // phyConnectTimeMillis表示连接的物理存活时间
                    long phyConnectTimeMillis = currentTimeMillis
                            - connection.connectTimeMillis;
                    // 连接的物理存活时间大于phyTimeoutMillis,则将这个连接放入evictConnections数组
                    if (phyConnectTimeMillis > phyTimeoutMillis) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                // idleMillis表示连接的空闲时间
                long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                // minEvictableIdleTimeMillis表示连接允许的最小空闲时间,默认是30分钟
                // keepAliveBetweenTimeMillis表示保活间隔时间,默认是2分钟
                // 如果连接的空闲时间小于minEvictableIdleTimeMillis且还小于keepAliveBetweenTimeMillis
                // 则connections数组中当前连接之后的连接都会满足空闲时间小于minEvictableIdleTimeMillis且还小于keepAliveBetweenTimeMillis
                // 此时跳出遍历,不再检查其余的连接
                if (idleMillis < minEvictableIdleTimeMillis
                        && idleMillis < keepAliveBetweenTimeMillis
                ) {
                    break;
                }

                // 连接的空闲时间大于等于允许的最小空闲时间
                if (idleMillis >= minEvictableIdleTimeMillis) {
                    if (checkTime && i < checkCount) {
                        // i < checkCount这个条件的理解如下:
                        // 每次shrink()方法执行时,connections数组中只有索引0到checkCount-1的连接才允许被销毁
                        // 这样才能保证销毁完连接后,connections数组中至少还有minIdle个连接
                        evictConnections[evictCount++] = connection;
                        continue;
                    } else if (idleMillis > maxEvictableIdleTimeMillis) {
                        // 如果空闲时间过久,已经大于了允许的最大空闲时间(默认7小时)
                        // 那么无论如何都要销毁这个连接
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                // 如果开启了保活机制,且连接空闲时间大于等于了保活间隔时间
                // 此时将连接加入到保活连接数组中
                if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
                    keepAliveConnections[keepAliveCount++] = connection;
                }
            } else {
                // checkTime为false,那么前checkCount个连接直接进行销毁,不再判断这些连接的空闲时间是否超过阈值
                if (i < checkCount) {
                    evictConnections[evictCount++] = connection;
                } else {
                    break;
                }
            }
        }

        // removeCount = 销毁连接数 + 保活连接数
        // removeCount表示本次从connections数组中拿掉的连接数
        // 注:一定是从前往后拿,正常情况下最后minIdle个连接是安全的
        int removeCount = evictCount + keepAliveCount;
        if (removeCount > 0) {
            // [0, 1, 2, 3, 4, null, null, null] -> [3, 4, 2, 3, 4, null, null, null]
            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
            // [3, 4, 2, 3, 4, null, null, null] -> [3, 4, null, null, null, null, null, null, null]
            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
            // 更新池中连接数
            poolingCount -= removeCount;
        }
        keepAliveCheckCount += keepAliveCount;

        // 如果池中连接数加上活跃连接数(借出去的连接)小于最小空闲连接数
        // 则将needFill设为true,后续需要唤醒生产连接的线程来生产连接
        if (keepAlive && poolingCount + activeCount < minIdle) {
            needFill = true;
        }
    } finally {
        lock.unlock();
    }

    if (evictCount > 0) {
        // 遍历evictConnections数组,销毁其中的连接
        for (int i = 0; i < evictCount; ++i) {
            DruidConnectionHolder item = evictConnections[i];
            Connection connection = item.getConnection();
            JdbcUtils.close(connection);
            destroyCountUpdater.incrementAndGet(this);
        }
        Arrays.fill(evictConnections, null);
    }

    if (keepAliveCount > 0) {
        // 遍历keepAliveConnections数组,对其中的连接做可用性校验
        // 校验通过连接就放入connections数组,没通过连接就销毁
        for (int i = keepAliveCount - 1; i >= 0; --i) {
            DruidConnectionHolder holer = keepAliveConnections[i];
            Connection connection = holer.getConnection();
            holer.incrementKeepAliveCheckCount();

            boolean validate = false;
            try {
                this.validateConnection(connection);
                validate = true;
            } catch (Throwable error) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("keepAliveErr", error);
                }
            }

            boolean discard = !validate;
            if (validate) {
                holer.lastKeepTimeMillis = System.currentTimeMillis();
                boolean putOk = put(holer, 0L, true);
                if (!putOk) {
                    discard = true;
                }
            }

            if (discard) {
                try {
                    connection.close();
                } catch (Exception e) {

                }

                lock.lock();
                try {
                    discardCount++;

                    if (activeCount + poolingCount <= minIdle) {
                        emptySignal();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
        this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
        Arrays.fill(keepAliveConnections, null);
    }

    // 如果needFill为true则唤醒生产连接的线程来生产连接
    if (needFill) {
        lock.lock();
        try {
            // 计算需要生产连接的个数
            int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
            for (int i = 0; i < fillCount; ++i) {
                emptySignal();
            }
        } finally {
            lock.unlock();
        }
    } else if (onFatalError || fatalErrorIncrement > 0) {
        lock.lock();
        try {
            emptySignal();
        } finally {
            lock.unlock();
        }
    }
}

DruidDataSource#shrink方法中,核心逻辑是遍历connections数组中的连接,并判断这些连接是需要销毁还是需要保活。通常情况下,connections数组中的前checkCount(checkCount = poolingCount - minIdle) 个连接是危险的,因为这些连接只要满足了:空闲时间 >= minEvictableIdleTimeMillis(允许的最小空闲时间),那么就需要被销毁,而connections数组中的最后minIdle个连接是相对安全的,因为这些连接只有在满足:空闲时间 > maxEvictableIdleTimeMillis(允许的最大空闲时间) 时,才会被销毁。这么判断的原因,主要就是需要让连接池里能够保证至少有minIdle个空闲连接可以让应用线程获取。

当确定好了需要销毁和需要保活的连接后,此时会先将connections数组清理,只保留安全的连接,这个过程示意图如下。

最后,会遍历evictConnections数组,销毁数组中的连接,遍历keepAliveConnections数组,对其中的每个连接做可用性校验,如果校验可用,那么就重新放回connections数组,否则销毁。

总结

连接的创建由一个叫做CreateConnectionThread的线程完成,整体流程就是在一个死循环中不断的等待,被唤醒,然后创建连接。每一个被创建出来的物理连接java.sql.Connection会被封装为一个DruidConnectionHolder,然后存放到connections数组中。

连接的销毁由一个叫做DestroyConnectionThread的线程完成,核心逻辑是周期性的遍历connections数组中的连接,并判断这些连接是需要销毁还是需要保活,需要销毁的连接最后会被物理销毁,需要保活的连接最后会进行一次可用性校验,如果校验不通过,则进行物理销毁。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/359440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【大数据离线开发】7.2 搭建HBase环境

7.2 搭建HBase的环境 准备工作&#xff1a; 解压Hbase安装包 [rootbigdata111 tools]# tar -zxvf hbase-1.3.1-bin.tar.gz -C ~/training/设置Hadoop的环境变量 vi ~/.bash_profile HBASE_HOME/root/training/hbase-1.3.1 export HBASE_HOMEPATH$HBASE_HOME/bin:$PATH export…

37、基于51单片机乒乓球比赛系统设计

摘要 乒乓球游戏电路是一个对输入信号、输入时机正确与否的8个LED表示乒乓球球台和乒乓球&#xff0c;用数码管模拟显示器&#xff0c;显示比赛局数比分和每局玩家得分的电路。电路并不复杂&#xff0c;整体分为两个模块&#xff1a;一&#xff0c;游戏主模块&#xff1b;二&a…

excel图表技巧:如何制作自动刷新的动态喜报

临近年关&#xff0c;各企业进入节日营销大战&#xff0c;每天都需要对销售数据进行统计分析&#xff0c;同时为了鼓励及带动新品的销售气氛&#xff0c;还会制作鼓励销售人员士气的喜报。今天就来分享使用Excel如何快速制作可以自动更新数据的喜报&#xff0c;非常方便哦&…

c语言编程规范 第四部分

5、禁止头文件循环依赖头文件循环依赖&#xff0c;指a.h包含b.h&#xff0c;b.h包含c.h&#xff0c;c.h包含a.h之类导致任何一个头文件修改&#xff0c;都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖&#xff0c;如a.h包含b.h&#xff0c;b.h包含c.h&a…

单例模式设计(面试题)

1、static修饰变量规则static修饰的静态成员属于 类而不是对象&#xff0c;所有的对象共享一份静态成员数据&#xff0c;所以不占用类的空间static修饰的成员&#xff0c;定义类的时候&#xff0c;必须分配空间static修饰的静态成员数据 必须类中定义 类外初始化静态成员变量可…

CentOS7安装MariaDB步骤

文章目录1.配置MariaDB yum源2.安装MariaDBMariaDB数据库管理系统是MySQL的一个分支&#xff0c;主要由开源社区在维护&#xff0c;采用GPL授权许可。 MariaDB的目的是完全兼容MySQL&#xff0c;包括API和命令行&#xff0c;使之能轻松成为MySQL的代替品。 CentOS 6 或早期的版…

0基础成功转行Python自动化测试工程师,年薪30W+,经验总结都在这(建议收藏)

两年前的决定我觉得还是非常正确的&#xff0c;就是自学了python&#xff0c;然后学习了自动化测试、性能测试、框架、持续集成&#xff0c;同时也把前面的软件测试基础知识全部补全了。目前的收入还比较满意&#xff0c;月入2W&#xff08;仅代表个人收入&#xff09;,13薪&am…

技术团队管理要求 网文节选要点,内部培训用

业务线开发级别分布 技术开发 高级技术开发 技术专家 p6p7 团队专家 p7 单团队 10人小团队 领域专家 p8 多团队 2-5 10人小团队&#xff0c;技术顶峰&#xff0c;业务和管理能力都不能弱。 商业或者业务leader p8 p9&#xff0c;商业模式设计和商业成功。业务能力和管…

用神经辐射场在大场景中漫游

目录 前言 介绍 背景 改进 NeRF 以编码大型场景 在训练数据中获得足够的观点 动态对象移除 应用 结论 参考 前言 最近一直在做NeRF相关工作&#xff0c;偶然看到台湾智慧实验室一篇文章&#xff08;Hovering Around a Large Scene with Neural Radiance Field Taiwan …

系列三、docker相关指令

一、docker指令 1.1、查看docker详细信息 docker info 1.2、查看docker版本 docker version 1.3、帮助命令 docker --help 二、images指令 2.1、查看本地仓库中有哪些镜像 docker images 2.2、下载新的镜像 # 语法 docker pull 镜像名:版本号# 案例 docker pull mysql…

详解CanNM配置-CanNmPnEnabled参数有什么用?

总目录链接==>> AutoSAR入门和实战系列总目录 @学前问答: CanNmPnEnabled是什么含义? CanNmPnEnabled会对接收NM PDU 有什么影响 CanNmAllNmMessagesKeepAwake是干嘛的? 文章目录 1 CanNmPnEnable配置解析2 答疑解析1 CanNmPnEnable配置解析 CanNmPnEnable配置的参…

camera 硬件基本知识

参考博客&#xff1a;1.【Camera专题】Qcom-你应该掌握的Camera调试技巧2_c枫_撸码的日子的博客-CSDN博客_outputpixelclock 2.浩瀚之水_csdn的博客_CSDN博客-深度学习,嵌入式Linux相关知识汇总,Caffe框架领域博主 3.一个早起的程序员的博客_CSDN博客-FPGA,PCIe应用实战,PCI-E…

windows 系统 同时安装启动 多个版本的 MySQL

目录一 安装MySQL8.01.0 下载MySQL8.0版本1.1 配置配置文件1.2 注册服务1.3 修改密码二 安装MySQL5.72.0 下载MySQL5.7版本2.1 配置配置文件2.2 注册服务2.3 启动服务并修改密码在同一台 windows 上安装不同版本的MySQL, 这里表示环境干净未安装MySQL的教程.如安装过请先百度搜…

jvm监控进程内存分布

线上经常内存爆满&#xff0c;导致设备掉线&#xff0c;进行排查14894 进程ip Pidjstat -gcutil 14894 1000 -1 jvm 内存分布%jmap -histo:live 14894 | head -50 jvm 存活的实例对象 前50个jmap -histo:live 14894 >> heap.txt jvm 存活的实例对象 输出成文件jmap -dump…

操作系统线程

进程那一章&#xff0c;我们留下了一个问题 第一个cpu调用进程&#xff0c;进程调用i/o设备&#xff0c;主动进入ready 队列 第二个cpu将程序执行时间平均分时&#xff0c;进程执行时间到 第三个fork函数&#xff0c;我们上一章的lab有实践&#xff0c;可以看出是父进程主动条用…

数字化转型下的园区运营如何才能智慧起来?推荐快鲸智慧园区系统

在数字化转型深入推动的背景下&#xff0c;“大数据”、“互联网”等技术不断推动着传统产业&#xff0c;园区运营所产生的业务数据与日俱增。数据作为数字化转型的核心要素&#xff0c;如果不对其进行分析整理&#xff0c;从中提取有价值的信息&#xff0c;数据的价值便无法得…

CS144-Lab1

实验架构 TCP实施中模块和数据流的排列 : [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmRfy6Va-1676857163260)(null)] 字节流是Lab0。TCP的工作是通过不可靠的数据报网络传输两个字节流(每个方向一个)&#xff0c;以便写入连接一侧套接字的字…

在superset中快速制作报表或仪表盘

在中小型企业&#xff0c;当下需要快速迭代、快速了解运营效果的业务&#xff0c;急需一款开源、好用、能快速迭代生产的报表系统。 老板很关心&#xff0c;BI工程师很关心&#xff0c;同时系统开发人员也同样关心&#xff0c;一个好的技术选型往往能够帮助公司减少很多成本&a…

软件持续测试的未来题】

测试是软件开发生命周期(SDLC)的重要组成部分。SDLC 的每个阶段都应包含测试&#xff0c;以获得更快的反馈并提高产品质量。如果以有效的方式实施和使用测试自动化&#xff0c;那么它可以为您带来出色的结果&#xff0c;而持续测试是正确的方法.。预计在2018-2023 年的预测期内…

CCNP350-401学习笔记(251-300题)

251、 Which IPv6 OSPF network type is applied to interface Fa0/0 of R2 by default? A. multipointB. broadcast C. Ethernet D. point-to-point 252、Which EIGRP feature allows the use of leak maps? A. neighborB. Stub C. offset-list D. address-family 253、W…