我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Druid连接创建及销毁的方法是什么

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Druid连接创建及销毁的方法是什么

这篇文章主要介绍“Druid连接创建及销毁的方法是什么”,在日常操作中,相信很多人在Druid连接创建及销毁的方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Druid连接创建及销毁的方法是什么”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

前言

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

  • Druid数据库连接池中可用的连接存放在一个数组connections中;

  • Druid数据库连接池做并发控制,主要靠一把可重入锁以及和这把锁关联的两个Condition对象;

public DruidAbstractDataSource(boolean lockFair) {   lock = new ReentrantLock(lockFair);   notEmpty = lock.newCondition();   empty = lock.newCondition();}
  • 连接池没有可用连接时,应用线程会在notEmpty上等待,连接池已满时,生产连接的线程会在empty上等待;

  • 对连接保活,就是每间隔一定时间,对达到了保活间隔周期的连接进行有效性校验,可以将无效连接销毁,也可以防止连接长时间不与数据库服务端通信。

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数组清理,只保留安全的连接,这个过程示意图如下。

Druid连接创建及销毁的方法是什么

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

到此,关于“Druid连接创建及销毁的方法是什么”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Druid连接创建及销毁的方法是什么

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Druid连接创建及销毁的方法是什么

这篇文章主要介绍“Druid连接创建及销毁的方法是什么”,在日常操作中,相信很多人在Druid连接创建及销毁的方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Druid连接创建及销毁的方法是什么”的疑
2023-07-05

Singleton模式创建、多线程与销毁的方法是什么

这篇文章主要介绍“Singleton模式创建、多线程与销毁的方法是什么”,在日常操作中,相信很多人在Singleton模式创建、多线程与销毁的方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Singl
2023-06-18

linux创建软连接的方法是什么

在Linux中,创建软连接的方法是使用ln命令。具体语法如下:ln -s 源文件 目标文件其中,"-s"表示创建软连接。源文件是要创建软连接的文件或目录的路径,目标文件是软连接的路径和名称。例如:ln -s /home/user/file.
2023-09-27

php销毁变量的2种方法是什么

php销毁变量的2种方法:1、使用unset()函数,语法“unset($varname)”;2、给指定变量赋值为“NULL”,语法“$varname=null;”。
2017-09-22

Java Session获取或销毁的方法是什么

在Java中,可以使用HttpServletRequest对象来获取和销毁会话。要获取会话,可以使用以下方法之一:使用HttpServletRequest的getSession()方法来获取当前会话对象。如果会话不存在,则将创建新的会话。
2023-10-21

Spring中Bean初始化和销毁的方法是什么

今天小编给大家分享一下Spring中Bean初始化和销毁的方法是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。Sprin
2023-07-06

MySQL内连接、外连接及SQL JOINS的实现方法是什么

这篇文章主要讲解了“MySQL内连接、外连接及SQL JOINS的实现方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“MySQL内连接、外连接及SQL JOINS的实现方法是什么”吧
2023-07-05

SQL变量创建及使用的方法是什么

在 SQL 中,可以使用以下方法来创建和使用变量:创建变量:使用 DECLARE 语句来定义一个变量,并为其指定数据类型和初始值。例如:DECLARE @myVariable INT = 10;设置变量的值:使用 SET 语句来给变量赋值。
SQL变量创建及使用的方法是什么
2024-04-09

springboot整合druid及配置依赖的方法是什么

本篇内容主要讲解“springboot整合druid及配置依赖的方法是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“springboot整合druid及配置依赖的方法是什么”吧!Druid简
2023-06-22

连接redis的方法是什么

连接到Redis的方法取决于您使用的编程语言和Redis客户端库。以下是一些常见的连接Redis的方法:使用Redis官方提供的客户端库,如redis-py(Python)、jedis(Java)、phpredis(PHP)等。这些客户端库
连接redis的方法是什么
2024-05-11

vue-cli是什么及创建vue-cli项目的方法

vue-cli是vue官方提供的、快速生成vue工程化项目的工具,支持创建vue2和vue3的项目,本文给大家详细讲解vue-cli是什么及创建vue-cli项目的方法,感兴趣的朋友跟随小编一起看看吧
2023-05-16

weblogic创建域的方法是什么

WebLogic创建域的方法有两种:命令行方式和图形界面方式。1. 命令行方式:a. 打开命令行终端。b. 切换到WebLogic安装目录的“bin”文件夹。c. 运行以下命令创建域:```./config.cmd(Windows)./co
2023-09-01

clickhouse创建表的方法是什么

在ClickHouse中,可以使用SQL语句来创建表。以下是一个简单的示例:CREATE TABLE my_table (id UInt32,name String,age UInt8) ENGINE = MergeTree()ORD
clickhouse创建表的方法是什么
2024-03-01

pytorch创建tensor的方法是什么

在PyTorch中,可以通过以下几种方法来创建tensor:使用torch.Tensor()函数创建一个空的tensor:tensor = torch.Tensor()使用torch.tensor()函数根据给定的数据创建一个tensor
pytorch创建tensor的方法是什么
2024-04-08

springboot创建api的方法是什么

创建API的方法有很多种,以下是使用Spring Boot创建API的一种常见方法:1. 创建一个新的Spring Boot项目。2. 在项目中添加所需的依赖,通常包括Spring Web、Spring Data JPA、Spring Se
2023-10-21

sqlserver创建表的方法是什么

使用 CREATE TABLE 语句创建表,例如:CREATE TABLE 表名 (列1 数据类型,列2 数据类型,...);在 SQL Server Management Studio 中使用图形化界面创建表使用 SQL Server
sqlserver创建表的方法是什么
2024-04-09

oracle创建dblink的方法是什么

在Oracle数据库中,可以使用CREATE DATABASE LINK语句创建数据库链接(dblink)。创建数据库链接的语法如下:CREATE DATABASE LINK link_nameCONNECT TO remote_us
oracle创建dblink的方法是什么
2024-04-09

mybatis创建表的方法是什么

MyBatis是一个持久层框架,不提供直接创建表的方法。创建表通常是在数据库中执行SQL语句来完成的。在使用MyBatis时,你需要先创建好数据库表,然后定义对应的实体类和映射文件,将实体类与数据库表进行映射关系的配置。在映射文件中,你可以
2023-09-27

oracle directory创建的方法是什么

在 Oracle 数据库中,要创建一个目录(directory),可以使用以下的 SQL 语句:CREATE OR REPLACE DIRECTORY directory_name AS 'directory_path'
oracle directory创建的方法是什么
2024-04-09

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录