一、背景

HTTP协议是无状态的协议,即每一次请求都是互相独立的。因此它的最初实现是,每一个http请求都会打开一个tcp socket连接,当交互完毕后会关闭这个连接。

HTTP协议是全双工的协议,所以建立连接与断开连接是要经过三次握手与四次挥手的。显然在这种设计中,每次发送Http请求都会消耗很多的额外资源,即连接的建立与销毁。

于是,HTTP协议的也进行了发展,通过持久连接的方法来进行socket连接复用。

从图中可以看到:

  1. 在串行连接中,每次交互都要打开关闭连接

  2. 在持久连接中,第一次交互会打开连接,交互结束后连接并不关闭,下次交互就省去了建立连接的过程。

持久连接的实现有两种:HTTP/1.0+的keep-alive与HTTP/1.1的持久连接。

二、HTTP/1.0+的Keep-Alive

从1996年开始,很多HTTP/1.0浏览器与服务器都对协议进行了扩展,那就是“keep-alive”扩展协议。

注意,这个扩展协议是作为1.0的补充的“实验型持久连接”出现的。keep-alive已经不再使用了,最新的HTTP/1.1规范中也没有对它进行说明,只是很多应用延续了下来。

使用HTTP/1.0的客户端在首部中加上”Connection:Keep-Alive”,请求服务端将一条连接保持在打开状态。服务端如果愿意将这条连接保持在打开状态,就会在响应中包含同样的首部。如果响应中没有包含”Connection:Keep-Alive”首部,则客户端会认为服务端不支持keep-alive,会在发送完响应报文之后关闭掉当前连接。

通过keep-alive补充协议,客户端与服务器之间完成了持久连接,然而仍然存在着一些问题:

在HTTP/1.0中keep-alive不是标准协议,客户端必须发送Connection:Keep-Alive来激活keep-alive连接。

代理服务器可能无法支持keep-alive,因为一些代理是”盲中继”,无法理解首部的含义,只是将首部逐跳转发。所以可能造成客户端与服务端都保持了连接,但是代理不接受该连接上的数据。

三、HTTP/1.1的持久连接

HTTP/1.1采取持久连接的方式替代了Keep-Alive。

HTTP/1.1的连接默认情况下都是持久连接。如果要显式关闭,需要在报文

中加上Connection:Close首部。即在HTTP/1.1中,所有的连接都进行了复用。

然而如同Keep-Alive一样,空闲的持久连接也可以随时被客户端与服务端关闭。不发送Connection:Close不意味着服务器承诺连接永远保持打开。

四、HttpClient如何生成持久连接

HttpClien中使用了连接池来管理持有连接,同一条TCP链路上,连接是可以复用的。HttpClient通过连接池的方式进行连接持久化。

其实“池”技术是一种通用的设计,其设计思想并不复杂:

  1. 当有连接第一次使用的时候建立连接

  2. 结束时对应连接不关闭,归还到池中

  3. 下次同个目的的连接可从池中获取一个可用连接

  4. 定期清理过期连接

所有的连接池都是这个思路,不过我们看HttpClient源码主要关注两点:

  • 连接池的具体设计方案,以供以后自定义连接池参考

  • 如何与HTTP协议对应上,即理论抽象转为代码的实现

4.1 HttpClient连接池的实现

HttpClient关于持久连接的处理在下面的代码中可以集中体现,下面从MainClientExec摘取了和连接池相关的部分,去掉了其他部分:

MainClientExec摘取了和连接池相关的部分,去掉了其他部分:

public class MainClientExec implements ClientExecChain {

@Override

public CloseableHttpResponse execute(

final HttpRoute route,

final HttpRequestWrapper request,

final HttpClientContext context,

final HttpExecutionAware execAware) throws IOException, HttpException {

     //从连接管理器HttpClientConnectionManager中获取一个连接请求ConnectionRequest

final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;

final int timeout = config.getConnectionRequestTimeout();

//从连接请求ConnectionRequest中获取一个被管理的连接HttpClientConnection

managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);

     //将连接管理器HttpClientConnectionManager与被管理的连接HttpClientConnection交给一个ConnectionHolder持有

final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);

try {

HttpResponse response;

if (!managedConn.isOpen()) {

          //如果当前被管理的连接不是出于打开状态,需要重新建立连接

establishRoute(proxyAuthState, managedConn, route, request, context);

}

       //通过连接HttpClientConnection发送请求

response = requestExecutor.execute(request, managedConn, context);

       //通过连接重用策略判断是否连接可重用

if (reuseStrategy.keepAlive(response, context)) {

//获得连接有效期

final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);

//设置连接有效期

connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);

          //将当前连接标记为可重用状态

connHolder.markReusable();

} else {

connHolder.markNonReusable();

}

}

final HttpEntity entity = response.getEntity();

if (entity == null || !entity.isStreaming()) {

//将当前连接释放到池中,供下次调用

connHolder.releaseConnection();

return new HttpResponseProxy(response, null);

} else {

return new HttpResponseProxy(response, connHolder);

}

}

这里看到了在Http请求过程中对连接的处理是和协议规范是一致的,这里要展开讲一下具体实现。

PoolingHttpClientConnectionManager是HttpClient默认的连接管理器,首先通过requestConnection()获得一个连接的请求,注意这里不是连接。

public ConnectionRequest requestConnection(

final HttpRoute route,

final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null);

return new ConnectionRequest() {

@Override

public boolean cancel() {

return future.cancel(true);

}

@Override

public HttpClientConnection get(

final long timeout,

final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {

final HttpClientConnection conn = leaseConnection(future, timeout, tunit);

if (conn.isOpen()) {

final HttpHost host;

if (route.getProxyHost() != null) {

host = route.getProxyHost();

} else {

host = route.getTargetHost();

}

final SocketConfig socketConfig = resolveSocketConfig(host);

conn.setSocketTimeout(socketConfig.getSoTimeout());

}

return conn;

}

};

}

可以看到返回的ConnectionRequest对象实际上是一个持有了Future<CPoolEntry>,CPoolEntry是被连接池管理的真正连接实例。

从上面的代码我们应该关注的是:

  • Future<CPoolEntry> future = this.pool.lease(route, state, null)

    如何从连接池CPool中获得一个异步的连接,Future<CPoolEntry>

  • HttpClientConnection conn = leaseConnection(future, timeout, tunit)

    如何通过异步连接Future<CPoolEntry>获得一个真正的连接HttpClientConnection

4.2 Future<CPoolEntry>

看一下CPool是如何释放一个Future<CPoolEntry>的,AbstractConnPool核心代码如下:

private E getPoolEntryBlocking(

final T route, final Object state,

final long timeout, final TimeUnit tunit,

final Future<E> future) throws IOException, InterruptedException, TimeoutException {

     //首先对当前连接池加锁,当前锁是可重入锁ReentrantLockthis.lock.lock();

try {

        //获得一个当前HttpRoute对应的连接池,对于HttpClient的连接池而言,总池有个大小,每个route对应的连接也是个池,所以是“池中池”

final RouteSpecificPool<T, C, E> pool = getPool(route);

E entry;

for (;;) {

Asserts.check(!this.isShutDown, "Connection pool shut down");

          //死循环获得连接

for (;;) {

            //从route对应的池中拿连接,可能是null,也可能是有效连接

entry = pool.getFree(state);

            //如果拿到null,就退出循环

if (entry == null) {

break;

}

            //如果拿到过期连接或者已关闭连接,就释放资源,继续循环获取

if (entry.isExpired(System.currentTimeMillis())) {

entry.close();

}

if (entry.isClosed()) {

this.available.remove(entry);

pool.free(entry, false);

} else {

              //如果拿到有效连接就退出循环

break;

}

}

          //拿到有效连接就退出

if (entry != null) {

this.available.remove(entry);

this.leased.add(entry);

onReuse(entry);

return entry;

}

          //到这里证明没有拿到有效连接,需要自己生成一个

final int maxPerRoute = getMax(route);

//每个route对应的连接最大数量是可配置的,如果超过了,就需要通过LRU清理掉一些连接

final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);

if (excess > 0) {

for (int i = 0; i < excess; i++) {

final E lastUsed = pool.getLastUsed();

if (lastUsed == null) {

break;

}

lastUsed.close();

this.available.remove(lastUsed);

pool.remove(lastUsed);

}

}

          //当前route池中的连接数,没有达到上线

if (pool.getAllocatedCount() < maxPerRoute) {

final int totalUsed = this.leased.size();

final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);

            //判断连接池是否超过上线,如果超过了,需要通过LRU清理掉一些连接

if (freeCapacity > 0) {

final int totalAvailable = this.available.size();

               //如果空闲连接数已经大于剩余可用空间,则需要清理下空闲连接

if (totalAvailable > freeCapacity - 1) {

if (!this.available.isEmpty()) {

final E lastUsed = this.available.removeLast();

lastUsed.close();

final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());

otherpool.remove(lastUsed);

}

}

              //根据route建立一个连接

final C conn = this.connFactory.create(route);

              //将这个连接放入route对应的“小池”中

entry = pool.add(conn);

              //将这个连接放入“大池”中

this.leased.add(entry);

return entry;

}

}

         //到这里证明没有从获得route池中获得有效连接,并且想要自己建立连接时当前route连接池已经到达最大值,即已经有连接在使用,但是对当前线程不可用

boolean success = false;

try {

if (future.isCancelled()) {

throw new InterruptedException("Operation interrupted");

}

            //将future放入route池中等待

pool.queue(future);

            //将future放入大连接池中等待

this.pending.add(future);

            //如果等待到了信号量的通知,success为true

if (deadline != null) {

success = this.condition.awaitUntil(deadline);

} else {

this.condition.await();

success = true;

}

if (future.isCancelled()) {

throw new InterruptedException("Operation interrupted");

}

} finally {

//从等待队列中移除

pool.unqueue(future);

this.pending.remove(future);

}

//如果没有等到信号量通知并且当前时间已经超时,则退出循环

if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {

break;

}

}

       //最终也没有等到信号量通知,没有拿到可用连接,则抛异常

throw new TimeoutException("Timeout waiting for connection");

} finally {

       //释放对大连接池的锁

this.lock.unlock();

}

}

上面的代码逻辑有几个重要点:

  • 连接池有个最大连接数,每个route对应一个小连接池,也有个最大连接数

  • 不论是大连接池还是小连接池,当超过数量的时候,都要通过LRU释放一些连接

  • 如果拿到了可用连接,则返回给上层使用

  • 如果没有拿到可用连接,HttpClient会判断当前route连接池是否已经超过了最大数量,没有到上限就会新建一个连接,并放入池中

  • 如果到达了上限,就排队等待,等到了信号量,就重新获得一次,等待不到就抛超时异常

  • 通过线程池获取连接要通过ReetrantLock加锁,保证线程安全

到这里为止,程序已经拿到了一个可用的CPoolEntry实例,或者抛异常终止了程序。

4.3 HttpClientConnection

protected HttpClientConnection leaseConnection(

final Future<CPoolEntry> future,

final long timeout,

final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {

final CPoolEntry entry;

try {

       //从异步操作Future<CPoolEntry>中获得CPoolEntry

entry = future.get(timeout, tunit);

if (entry == null || future.isCancelled()) {

throw new InterruptedException();

}

Asserts.check(entry.getConnection() != null, "Pool entry with no connection");

if (this.log.isDebugEnabled()) {

this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));

}

       //获得一个CPoolEntry的代理对象,对其操作都是使用同一个底层的HttpClientConnection

return CPoolProxy.newProxy(entry);

} catch (final TimeoutException ex) {

throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");

}

}

五、HttpClient如何复用持久连接?

在上一章中,我们看到了HttpClient通过连接池来获得连接,当需要使用连接的时候从池中获得。

对应着第三章的问题:

  1. 当有连接第一次使用的时候建立连接

  2. 结束时对应连接不关闭,归还到池中

  3. 下次同个目的的连接可从池中获取一个可用连接

  4. 定期清理过期连接

我们在第四章中看到了HttpClient是如何处理1、3的问题的,那么第2个问题是怎么处理的呢?

即HttpClient如何判断一个连接在使用完毕后是要关闭,还是要放入池中供他人复用?再看一下MainClientExec的代码

//发送Http连接

response = requestExecutor.execute(request, managedConn, context);

//根据重用策略判断当前连接是否要复用

if (reuseStrategy.keepAlive(response, context)) {

//需要复用的连接,获取连接超时时间,以response中的timeout为准

final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);

if (this.log.isDebugEnabled()) {

final String s;

               //timeout的是毫秒数,如果没有设置则为-1,即没有超时时间

if (duration > 0) {

s = "for " + duration + " " + TimeUnit.MILLISECONDS;

} else {

s = "indefinitely";

}

this.log.debug("Connection can be kept alive " + s);

}

            //设置超时时间,当请求结束时连接管理器会根据超时时间决定是关闭还是放回到池中

connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);

//将连接标记为可重用

            connHolder.markReusable();

} else {

            //将连接标记为不可重用

connHolder.markNonReusable();

}

可以看到,当使用连接发生过请求之后,有连接重试策略来决定该连接是否要重用,如果要重用就会在结束后交给HttpClientConnectionManager放入池中。

那么连接复用策略的逻辑是怎么样的呢?

public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy {

public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy();

@Override

public boolean keepAlive(final HttpResponse response, final HttpContext context) {

     //从上下文中拿到request

final HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);

if (request != null) {

       //获得Connection的Header

final Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION);

if (connHeaders.length != 0) {

final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null));

while (ti.hasNext()) {

final String token = ti.nextToken();

            //如果包含Connection:Close首部,则代表请求不打算保持连接,会忽略response的意愿,该头部这是HTTP/1.1的规范

if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {

return false;

}

}

}

}

     //使用父类的的复用策略

return super.keepAlive(response, context);

}

}

看一下父类的复用策略

if (canResponseHaveBody(request, response)) {

final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);

//如果reponse的Content-Length没有正确设置,则不复用连接

          //因为对于持久化连接,两次传输之间不需要重新建立连接,则需要根据Content-Length确认内容属于哪次请求,以正确处理“粘包”现象

//所以,没有正确设置Content-Length的response连接不能复用

if (clhs.length == 1) {

final Header clh = clhs[0];

try {

final int contentLen = Integer.parseInt(clh.getValue());

if (contentLen < 0) {

return false;

}

} catch (final NumberFormatException ex) {

return false;

}

} else {

return false;

}

}

if (headerIterator.hasNext()) {

try {

final TokenIterator ti = new BasicTokenIterator(headerIterator);

boolean keepalive = false;

while (ti.hasNext()) {

final String token = ti.nextToken();

            //如果response有Connection:Close首部,则明确表示要关闭,则不复用

if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {

return false;

            //如果response有Connection:Keep-Alive首部,则明确表示要持久化,则复用

} else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {

keepalive = true;

}

}

if (keepalive) {

return true;

}

} catch (final ParseException px) {

return false;

}

}

     //如果response中没有相关的Connection首部说明,则高于HTTP/1.0版本的都复用连接

return !ver.lessEquals(HttpVersion.HTTP_1_0);

总结一下:

  • 如果request首部中包含Connection:Close,不复用

  • 如果response中Content-Length长度设置不正确,不复用

  • 如果response首部包含Connection:Close,不复用

  • 如果reponse首部包含Connection:Keep-Alive,复用

  • 都没命中的情况下,如果HTTP版本高于1.0则复用

从代码中可以看到,其实现策略与我们第二、三章协议层的约束是一致的。

六、HttpClient如何清理过期连接

在HttpClient4.4版本之前,在从连接池中获取重用连接的时候会检查下是否过期,过期则清理。

之后的版本则不同,会有一个单独的线程来扫描连接池中的连接,发现有离最近一次使用超过设置的时间后,就会清理。默认的超时时间是2秒钟。

public CloseableHttpClient build() {

//如果指定了要清理过期连接与空闲连接,才会启动清理线程,默认是不启动的

if (evictExpiredConnections || evictIdleConnections) {

          //创造一个连接池的清理线程

final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,

maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,

maxIdleTime, maxIdleTimeUnit);

closeablesCopy.add(new Closeable() {

@Override

public void close() throws IOException {

connectionEvictor.shutdown();

try {

connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);

} catch (final InterruptedException interrupted) {

Thread.currentThread().interrupt();

}

}

});

          //执行该清理线程

connectionEvictor.start();

}

可以看到在HttpClientBuilder进行build的时候,如果指定了开启清理功能,会创建一个连接池清理线程并运行它。

public IdleConnectionEvictor(

final HttpClientConnectionManager connectionManager,

final ThreadFactory threadFactory,

final long sleepTime, final TimeUnit sleepTimeUnit,

final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {

this.connectionManager = Args.notNull(connectionManager, "Connection manager");

this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();

this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;

this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;

this.thread = this.threadFactory.newThread(new Runnable() {

@Override

public void run() {

try {

            //死循环,线程一直执行

while (!Thread.currentThread().isInterrupted()) {

              //休息若干秒后执行,默认10秒

Thread.sleep(sleepTimeMs);

               //清理过期连接

connectionManager.closeExpiredConnections();

               //如果指定了最大空闲时间,则清理空闲连接

if (maxIdleTimeMs > 0) {

connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);

}

}

} catch (final Exception ex) {

exception = ex;

}

}

});

}

总结一下:

    • 只有在HttpClientBuilder手动设置后,才会开启清理过期与空闲连接

    • 手动设置后,会启动一个线程死循环执行,每次执行sleep一定时间,调用HttpClientConnectionManager的清理方法清理过期与空闲连接。

七、本文总结

  • HTTP协议通过持久连接的方式,减轻了早期设计中的过多连接问题

  • 持久连接有两种方式:HTTP/1.0+的Keep-Avlive与HTTP/1.1的默认持久连接

  • HttpClient通过连接池来管理持久连接,连接池分为两个,一个是总连接池,一个是每个route对应的连接池

  • HttpClient通过异步的Future<CPoolEntry>来获取一个池化的连接

  • 默认连接重用策略与HTTP协议约束一致,根据response先判断Connection:Close则关闭,在判断Connection:Keep-Alive则开启,最后版本大于1.0则开启

  • 只有在HttpClientBuilder中手动开启了清理过期与空闲连接的开关后,才会清理连接池中的连接

  • HttpClient4.4之后的版本通过一个死循环线程清理过期与空闲连接,该线程每次执行都sleep一会,以达到定期执行的效果

Http 持久连接与 HttpClient 连接池的更多相关文章

  1. Http持久连接与HttpClient连接池

    一.背景 HTTP协议是无状态的协议,即每一次请求都是互相独立的.因此它的最初实现是,每一个http请求都会打开一个tcp socket连接,当交互完毕后会关闭这个连接. HTTP协议是全双工的协议, ...

  2. HttpClient连接池的连接保持、超时和失效机制

    HTTP是一种无连接的事务协议,底层使用的还是TCP,连接池复用的就是TCP连接,目的就是在一个TCP连接上进行多次的HTTP请求从而提高性能.每次HTTP请求结束的时候,HttpClient会判断连 ...

  3. HttpClient连接池

    HttpClient连接池,发现对于高并发的请求,效率提升很大.虽然知道是因为建立了长连接,导致请求效率提升,但是对于内部的原理还是不太清楚.后来在网上看到了HTTP协议的发展史,里面提到了一个属性C ...

  4. httpClient 连接池问题出现403.9

    困扰了半个月时间终于找到连接池的问题,由于调用第三方有异常导致连接不能及时释放 所以写了一个定时扫描释放连接 监控连接池释放连接: public static class IdleConnection ...

  5. HttpClient连接池的一些思考

    前言 使用apache的httpclient进行http的交互处理已经很长时间了,而httpclient实例则使用了http连接池,想必大家也没有关心过连接池的管理.事实上,通过分析httpclien ...

  6. 使用HttpClient连接池进行https单双向验证

    https单双向验证环境的搭建参见:http://www.cnblogs.com/YDDMAX/p/5368404.html 一.单向握手 示例程序: package com.ydd.study.he ...

  7. HttpClient连接池抛出大量ConnectionPoolTimeoutException: Timeout waiting for connection异常排查

    转自: http://blog.csdn.net/shootyou/article/details/6615051 今天解决了一个HttpClient的异常,汗啊,一个HttpClient使用稍有不慎 ...

  8. HttpClient实战三:Spring整合HttpClient连接池

    简介 在微服务架构或者REST API项目中,使用Spring管理Bean是很常见的,在项目中HttpClient使用的一种最常见方式就是:使用Spring容器XML配置方式代替Java编码方式进行H ...

  9. httpclient连接池在ES Restful API请求中的应用

    package com.wm.utils; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http ...

随机推荐

  1. 【洛谷P1507 NASA的食物计划】

    题目背景 NASA(美国航空航天局)因为航天飞机的隔热瓦等其他安全技术问题一直大伤脑筋,因此在各方压力下终止了航天飞机的历史,但是此类事情会不会在以后发生,谁也无法保证,在遇到这类航天问题时,解决方法 ...

  2. Go语言(IDEA下+Eclipse下)Hello World

    第一步,去下载Go环境 然后安装即可. IDEA 先安装GO插件: ..点击Browse... ..搜索GO ..点击安装,安装完之后重启 ..重启完之后,New~(IDEA已经自动识别出系统中安装的 ...

  3. windows下配置host不生效问题

    今天遇到了host配置之后不生效的问题,原因是文本编辑器用的是非windows格式,再notepad++右下角切换一下即可,将notepad++ 视图-显示符号-显示所有字符勾上之后可以看到windo ...

  4. 20165232 2017-2018-2《Java程序设计》课程总结

    20165232 2017-2018-2<Java程序设计>课程总结 每周作业链接汇总: 我期望的师生关系 学习基础和c语言基础调查 预备作业3 Linux安装及学习 第一周学习总结 第二 ...

  5. falsk简单项目示例

    目录结构: 源码及分析 https://github.com/freshman411/Flask_test/

  6. java类的编译、加载和执行

    一.java类的编译流程 这里主要讲的是从java文件到class文件 下图是java类编译的详细步骤: 1.词法分析:将java源代码的字符流转变为标记(Token)的集合,Token是编译过程中的 ...

  7. Ubuntu调节屏幕亮度

    下面的方式支持双屏,最多支持四个屏幕调节亮度. sudo add-apt-repository ppa:apandada1/brightness-controller sudo apt-get upd ...

  8. Redis 高可用分布式集群

    一,高可用 高可用(High Availability),是当一台服务器停止服务后,对于业务及用户毫无影响. 停止服务的原因可能由于网卡.路由器.机房.CPU负载过高.内存溢出.自然灾害等不可预期的原 ...

  9. 简:Spring中Bean的生命周期及代码示例

    (重要:spring bean的生命周期. spring的bean周期,装配.看过spring 源码吗?(把容器启动过程说了一遍,xml解析,bean装载,bean缓存等)) 完整的生命周期概述(牢记 ...

  10. PHP7 学习笔记(十六)Yaconf 一个高性能的配置管理扩展

    鸟哥博客原文:Yaconf – 一个高性能的配置管理扩展 什么是yaconf ? 它使用单独的一个配置目录(在yaconf.directory指定), 不和代码在一起.它在PHP启动的时候, 处理所有 ...