目录

前言

正文

统计数据/属性

成员方法

状态/指标信息使用场景举例

默认值不合理

代码示例

总结


前言

我们知道Ribbon它是一个客户端负载均衡器,因此它内部维护着一个服务器列表ServerList,当实例出现问题时候,需要将这部分异常的服务Server从负载均衡列表中T除掉,那么Ribbon是以什么作为参考,决定T除/不T除Server的呢???这就是本文将要讲述的服务器状态的管理:ServerStats

负载均衡LB需要依赖这些统计信息做为判断的策略,负载均衡器的统计类主要是LoadBalancerStats,其内部持有ServerStats对每个Server运行情况做了相关统计如:平均响应时间、累计失败数、熔断(时间)控制等。


正文

Stat中文释义:统计,Statistic单词的简写形式。另外,希望读者在阅读本文之前,已经了解了netflix-statistics的知识,你可以参考这篇文章:[享学Netflix] 四十四、netflix-statistics详解,手把手教你写个超简版监控系统

服务状态。在LoadBalancer中捕获每个服务器(节点)的各种状态,每个Server就对应着一个ServerStats实例。ServerStats表示一台Server的状态,各种纬度的统计数据才能使得你最终挑选出一个最适合的Server供以使用,以及计算其当前访问压力(并发数)、成功数、失败数、是否熔断、熔断了多久等等。


统计数据/属性

到底统计了哪些数据呢?对Server进行多维度的数据统计,均体现在它的成员属性上:

public class ServerStats {private final CachedDynamicIntProperty connectionFailureThreshold;private final CachedDynamicIntProperty circuitTrippedTimeoutFactor;private final CachedDynamicIntProperty maxCircuitTrippedTimeout;private static final DynamicIntProperty activeRequestsCountTimeout = DynamicPropertyFactory.getInstance().getIntProperty("niws.loadbalancer.serverStats.activeRequestsCount.effectiveWindowSeconds", 60 * 10);long failureCountSlidingWindowInterval = 1000; private MeasuredRate serverFailureCounts = new MeasuredRate(failureCountSlidingWindowInterval);private MeasuredRate requestCountInWindow = new MeasuredRate(300000L);Server server;AtomicLong totalRequests = new AtomicLong();AtomicInteger successiveConnectionFailureCount = new AtomicInteger(0);AtomicInteger activeRequestsCount = new AtomicInteger(0);AtomicInteger openConnectionsCount = new AtomicInteger(0);private volatile long lastConnectionFailedTimestamp;private volatile long lastActiveRequestsCountChangeTimestamp;private AtomicLong totalCircuitBreakerBlackOutPeriod = new AtomicLong(0);private volatile long lastAccessedTimestamp;private volatile long firstConnectionTimestamp = 0;
}

对这些统计数据/属性分别做如下解释说明:

  • connectionFailureThreshold:连接失败阈值,默认值3(超过就熔断)

    • 默认值配置:niws.loadbalancer.default.connectionFailureCountThreshold此key指定
    • 个性化配置:"niws.loadbalancer." + name + ".connectionFailureCountThreshold"
  • circuitTrippedTimeoutFactor:断路器超时因子,默认值10s。
    • 默认值配置: niws.loadbalancer.default.circuitTripTimeoutFactorSeconds
    • 个性化配置:"niws.loadbalancer." + name + ".circuitTripTimeoutFactorSeconds"
  • maxCircuitTrippedTimeout:断路器最大超时秒数(默认使用超时因子计算出来),默认值是30s。
    • 默认值配置:niws.loadbalancer.default.circuitTripMaxTimeoutSeconds
    • 个性化配置:"niws.loadbalancer." + name + ".circuitTripMaxTimeoutSeconds"
  • totalRequests:总请求数量。每次请求结束/错误时就会+1。
  • successiveConnectionFailureCount连续(successive)请求异常数量(这个连续发生在Retry重试期间)。
    • 在重试期间,但凡有一次成功了,就会把此参数置为0(失败的话此参数就一直加)
    • 说明:只有在异常类型是callErrorHandler.isCircuitTrippingException(e)的时候,才会算作失败,才会+1
      • 默认情况下只有SocketException/SocketTimeoutException这两种异常才算失败哦~
  • activeRequestsCount:活跃请求数量(正在请求的数量,它能反应该Server的负载、压力)。
    • 但凡只要开始执行Sever了,就+1
    • 但凡只要请求完成了/出错了,就-1
    • 注意:它有时间窗口的概念,后面讲具体逻辑
  • openConnectionsCount:暂无任何使用处,可忽略。
  • lastConnectionFailedTimestamp:最后一次失败的时间戳。至于什么叫失败,参考successiveConnectionFailureCount对失败的判断逻辑
  • lastActiveRequestsCountChangeTimestamp:简单的说就是activeRequestsCount的值最后变化的时间戳
  • totalCircuitBreakerBlackOutPeriod:断路器断电总时长(连续失败>=3次,增加20~30秒。具体增加多少秒,后面有计算逻辑)。
  • lastAccessedTimestamp:最后访问时间戳。和lastActiveRequestsCountChangeTimestamp的区别是,它增/减都update一下,而lastAccessedTimestamp只有在增的时候才会update一下。
  • firstConnectionTimestamp:首次连接时间戳,只会记录首次请求进来时的时间。
  • failureCountSlidingWindowInterval:失败次数统计时间窗。默认值1000ms
  • serverFailureCounts:上一秒失败次数(上一秒是因为failureCountSlidingWindowInterval默认自是1000ms)
    • successiveConnectionFailureCount增它就增,只不过它有时间窗口(1s)
  • requestCountInWindow:一个窗口期内的请求总数,窗口期默认为5分钟(300秒)
    • activeRequestsCount增它就增,只不过它有时间窗口(300s)

当然,它还有几个基于netflix-statistics数据统计的指标属性:

ServerStats:// 默认60s(1分钟)publish一次数据private static final int DEFAULT_PUBLISH_INTERVAL =  60 * 1000; // = 1 minute// 缓冲区大小。这个默认大小可谓非常大呀,就算你QPS是1000,也能抗1分钟private static final int DEFAULT_BUFFER_SIZE = 60 * 1000; // = 1000 requests/sec for 1 minuteint bufferSize = DEFAULT_BUFFER_SIZE;int publishInterval = DEFAULT_PUBLISH_INTERVAL;private static final double[] PERCENTS = makePercentValues();private DataDistribution dataDist = new DataDistribution(1, PERCENTS);private DataPublisher publisher = null;private final Distribution responseTimeDist = new Distribution();
  • PERCENTS:百分比,可参见枚举类Percent:[10,20…,90…,99.5]
  • dataDist:它是一个DataAccumulator,数据累加器。
  • publisher:定时publish发布数据,默认1分钟发布一次
  • responseTimeDist:它是个Distribution类型,因为它仅仅只需要持续累加数据,然后提供最大最小值、平均值的访问而已

dataDistresponseTimeDist统一通过noteResponseTime(double msecs)来记录每个请求的响应时间,dataDist按照时间窗口统计,responseTimeDist一直累加


成员方法

已经知道了每个字段的含义,再来看其提供的方法,就轻松很多了。

ServerStats:// 默认构造器:connectionFailureThreshold等参数均使用默认值 该构造器默认无人调用public ServerStats() { ... }// 参数值来自于lbStats,可以和ClientName挂上钩// 它在LoadBalancerStats#createServerStats()方法里被唯一调用public ServerStats(LoadBalancerStats lbStats) { ... }// 初始化对象,开始数据收集和报告。**请务必调用此方法** 它才是一个完整的实例public void initialize(Server server) {serverFailureCounts = new MeasuredRate(failureCountSlidingWindowInterval);requestCountInWindow = new MeasuredRate(300000L);if (publisher == null) {dataDist = new DataDistribution(getBufferSize(), PERCENTS);publisher = new DataPublisher(dataDist, getPublishIntervalMillis());// 启动任务:开始发布数据。1分钟发布一次publisher.start();}// 和Server关联this.server = server;}// 停止数据方法public void close() {if (publisher != null)publisher.stop();}// 收集每一次请求的响应时间public void noteResponseTime(double msecs){dataDist.noteValue(msecs);responseTimeDist.noteValue(msecs);}// 获得当前时间的活跃请求数(也就是Server的当前负载)public int  getActiveRequestsCount() {return getActiveRequestsCount(System.currentTimeMillis());}// 强调:如果当前时间currentTime距离上一次请求进来已经超过了时间窗口60s,那就返回0// 简单一句话:如果上次请求距今1分钟了,那就一个请求都不算(强制归零)public int getActiveRequestsCount(long currentTime) {int count = activeRequestsCount.get();if (count == 0) {return 0;} else if (currentTime - lastActiveRequestsCountChangeTimestamp > activeRequestsCountTimeout.get() * 1000 || count < 0) {activeRequestsCount.set(0);return 0;            } else {return count;}}

这些是ServerStats提供的基本方法,能访问到所有的成员属性。下面介绍分别介绍两个主题方法:


CircuitBreaker断路器的原理

本处的断路器解释:当有某个服务存在多个实例时,在请求的过程中,负载均衡器会统计每次请求的情况(请求响应时间,是否发生网络异常等),当出现了请求出现累计重试时,负载均衡器会标识当前服务实例,设置当前服务实例的断路的时间区间,在此区间内,当请求过来时,负载均衡器会将此服务实例从可用服务实例列表中暂时剔除(其实就是暂时忽略此Server),优先选择其他服务实例。

该断路器和Hystrix无任何关系,无任何关系,无任何关系。它是ServerStats内部维护的一套熔断机制,体现在如下方法上:

ServerStats:// 看看该断路器到哪个时间点戒指(关闭)的时刻时间戳// 比如断路器要从0点开30s,那么返回值就是00:00:30s这个时间戳呗private long getCircuitBreakerTimeout() {long blackOutPeriod = getCircuitBreakerBlackoutPeriod();if (blackOutPeriod <= 0) {return 0;}return lastConnectionFailedTimestamp + blackOutPeriod;}// 返回需要中断的持续时间(毫秒值)private long getCircuitBreakerBlackoutPeriod() {int failureCount = successiveConnectionFailureCount.get();int threshold = connectionFailureThreshold.get();if (failureCount < threshold) {return 0;}int diff = (failureCount - threshold) > 16 ? 16 : (failureCount - threshold);int blackOutSeconds = (1 << diff) * circuitTrippedTimeoutFactor.get();if (blackOutSeconds > maxCircuitTrippedTimeout.get()) {blackOutSeconds = maxCircuitTrippedTimeout.get();}return blackOutSeconds * 1000L;}

目前断路器统计失败是靠连续失败次数去判断断路逻辑的。此方法逻辑可总结如下:

  1. 连续失败次数还小于阈值(默认3次),那么就不用断路。否则打开断路,执行计算要断开多久的逻辑
  2. 计算失败基数,最大不能超过16(就算你连续失败100次,此基数也是16)
  3. 根据超时因子circuitTrippedTimeoutFactor(默认是10)计算出时间值blackOutSeconds,该值不能大于上限connectionFailureCircuitTimeout(默认30s)
    1. 也就是说保证了断路器最长不能打开超过30s

此方法不仅判断了断路器的打开与否,若打开顺便打开断路器应该打开多长时间(单位s)的方法,有了这个方法的理论做支撑,判断当前断路器是否开启就非常简单了:

ServerStats:public boolean isCircuitBreakerTripped() {return isCircuitBreakerTripped(System.currentTimeMillis());}public boolean isCircuitBreakerTripped(long currentTime) {long circuitBreakerTimeout = getCircuitBreakerTimeout();if (circuitBreakerTimeout <= 0) {return false;}return circuitBreakerTimeout > currentTime;}

当触发了熔断器(连续失败次数过多),断路器开启的时间范围是:

  • 最大值:1<<16 * 10 = 320s
  • 最小值:1<<1 * 10 =100s

当然这值是根据配置走的,并且还有最大时间30s的限制哦~

在Server被熔断期间,负载均衡器都将忽略此Server


断路器如何闭合?

倘若断路器打开了,它如何恢复呢?有如下3种情形它会恢复到正常状态:

  1. 不是连续失败了,也就是成功了一次,那么successiveConnectionFailureCount就会立马归0,所以熔断器就闭合了
  2. 即使请求失败了,但是并非是断路器类异常,即不是RetryHandler#isCircuitTrippingException这种类型的异常时(比如RuntimeException就不是这种类型的异常),那就也不算连续失败,所以也就闭合了
  3. 到时间了,断路器自然就自动闭合了

该断路器和Hystrix的断路器有何区别?

很明显,该断路器规则非常简单,开启与否完全由连续失败来决定,而是否算失败由RetryHandler#isCircuitTrippingException来决定,默认它只认为SocketException/SocketTimeoutException(或者其子类异常)属于该种类型的异常哦~

所以:你的程序在执行时的任何业务异常(如NPE)和此断路器没有半毛钱关系

当然它们最大最大的区别是断的对象不一样:

  • 本断路器断的是Server,也就是远程服务器
  • Hystrix断路器断的是Client,也就是客户端的调用

当然,关于Hystrix断路器的内容详解请参考:[享学Netflix] 二十七、Hystrix何为断路器的半开状态?HystrixCircuitBreaker详解


获取响应时间逻辑

一个Server服务器的响应是最重要的衡量指标,因此它提供了大量的获取响应时间的方法:

ServerStats:// 重要。获取累计的,累计的,平均响应时间// responseTimeDist里获得的均是所有请求累计的public double getResponseTimeAvg() {return responseTimeDist.getMean();}public double getResponseTimeMax() {return responseTimeDist.getMaximum();}...// 样本大小(每次获取的值可能不一样的哦,因为dataDist是时间窗口嘛)public int getResponseTimePercentileNumValues() {return dataDist.getSampleSize();}// 这段时间窗口内(1分钟)的平均响应时间public double getResponseTimeAvgRecent() {return dataDist.getMean();}// ========下面是各个分位数的值======public double getResponseTime10thPercentile() {return getResponseTimePercentile(Percent.TEN);}...public double getResponseTime99point5thPercentile() {return getResponseTimePercentile(Percent.NINETY_NINE_POINT_FIVE);}

状态/指标信息使用场景举例

统计信息都是非常有用的,这里先简单介绍,过个眼瘾即可。它的使用均在负载均衡策略上,举例:

  • WeightedResponseTimeRule:使用指标ServerStats.responseTimeDist,获取该Server的平均响应时间来决策
  • AvailabilityFilteringRule:它用到了两个指标信息
    • 通过ServerStats.isCircuitBreakerTripped()判断当前断路器是否打开作为该Server是否可用的判断
    • ServerStats.activeRequestsCount找个活跃请求数最小的Server
  • ZoneAvoidanceRule:使用到了ServerStats.upServerListZoneMapLoadBalancerStats.getZoneSnapshot

默认值不合理

private static final int DEFAULT_PUBLISH_INTERVAL =  60 * 1000;
private static final int DEFAULT_BUFFER_SIZE = 60 * 1000;

这两个默认值决定了样本量,以及样本时间窗口。按这么设置:每收集一次持续1分钟(问题不大),但是样本大小是60 * 1000这个太高了:单台机器QPS1000持续1分钟才能填满此窗口,我相信绝大部分情况下都是这么高的QPS的,所以此默认值并不合理

但是,但是,但是:ServerStats唯一创建地方是LoadBalancerStats里:

protected ServerStats createServerStats(Server server) {ServerStats ss = new ServerStats(this);//configure custom settingsss.setBufferSize(1000);ss.setPublishInterval(1000);                    ss.initialize(server);return ss;
}

两个值均为1000,说明:每秒钟收集一次(这个频率太高了吧),然后样本1000表示这1s内要有1000的请求打进来能打满(QPS1000,也特高了)。所以实际上的默认值真的也很不合理,它们均只适合高并发场景。。。

坑爹的是,这两个值并没有提供钩子or外部化配置让我们可以随意更改,唯一的钩子是它是个protected方法,你只能通过继承 + 复写才行,而实际上我们很小概率回去复写它(它在BaseLoadBalancer里创建)。

说明:若你想更好的监控,使得负载均衡效果更好点,那么作为架构师的你可以考虑定制定制哦~


代码示例

@Test
public void fun4() throws InterruptedException {ServerStats serverStats = new ServerStats();// 缓冲区大小最大1000。 若QPS是200,5s能装满它  这个QPS已经很高了serverStats.setBufferSize(1000);// 5秒收集一次数据serverStats.setPublishInterval(5000);// 请务必调用此初始化方法serverStats.initialize(new Server("YourBatman", 80));// 多个线程持续不断的发送请求request(serverStats);// 监控ServerStats状态monitor(serverStats);// hold主线程TimeUnit.SECONDS.sleep(10000);
}// 单独线程模拟刷页面,获取监控到的数据
private void monitor(ServerStats serverStats) {new Thread(() -> {ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);executorService.scheduleWithFixedDelay(() -> {System.out.println("=======时间:" + serverStats.getResponseTimePercentileTime() + ",统计值如下=======");System.out.println("请求总数(持续累计):" + serverStats.getTotalRequestsCount());System.out.println("平均响应时间:" + serverStats.getResponseTimeAvg());System.out.println("最小响应时间:" + serverStats.getResponseTimeMin());System.out.println("最大响应时间:" + serverStats.getResponseTimeMax());System.out.println("样本大小(取样本):" + serverStats.getResponseTimePercentileNumValues());System.out.println("样本下的平均响应时间:" + serverStats.getResponseTimeAvgRecent());System.out.println("样本下的响应时间中位数:" + serverStats.getResponseTime50thPercentile());System.out.println("样本下的响应时间90分位数:" + serverStats.getResponseTime90thPercentile());}, 5, 5, TimeUnit.SECONDS);}).start();
}// 模拟请求(开启5个线程,每个线程都持续不断的请求)
private void request(ServerStats serverStats) {for (int i = 0; i < 5; i++) {new Thread(() -> {while (true) {// 请求之前 记录活跃请求数serverStats.incrementActiveRequestsCount();serverStats.incrementNumRequests();long rt = doSomething();// 请求结束, 记录响应耗时serverStats.noteResponseTime(rt);serverStats.decrementActiveRequestsCount();}}).start();}
}// 模拟请求耗时,返回耗时时间
private long doSomething() {try {int rt = randomValue(10, 200);TimeUnit.MILLISECONDS.sleep(rt);return rt;} catch (InterruptedException e) {e.printStackTrace();return 0L;}
}// 本地使用随机数模拟数据收集
private int randomValue(int min, int max) {return min + (int) (Math.random() * ((max - min) + 1));
}

运行程序,控制台打印:

=======时间:Tue Mar 17 21:27:49 CST 2020,统计值如下=======
请求总数(持续累计):240
平均响应时间:103.43404255319149
最小响应时间:10.0
最大响应时间:199.0
样本大小(取样本):225
样本下的平均响应时间:102.38666666666667
样本下的响应时间中位数:105.0
样本下的响应时间90分位数:178.5
=======时间:Tue Mar 17 21:27:54 CST 2020,统计值如下=======
请求总数(持续累计):465
平均响应时间:106.75869565217391
最小响应时间:10.0
最大响应时间:199.0
样本大小(取样本):225
样本下的平均响应时间:110.59555555555555
样本下的响应时间中位数:115.5
样本下的响应时间90分位数:185.0
=======时间:Tue Mar 17 21:27:59 CST 2020,统计值如下=======
请求总数(持续累计):701
平均响应时间:106.35488505747126
最小响应时间:10.0
最大响应时间:200.0
样本大小(取样本):235
样本下的平均响应时间:105.39574468085107
样本下的响应时间中位数:105.0
样本下的响应时间90分位数:179.0
=======时间:Tue Mar 17 21:28:04 CST 2020,统计值如下=======
请求总数(持续累计):939
平均响应时间:105.98929336188436
最小响应时间:10.0
最大响应时间:200.0
样本大小(取样本):240
样本下的平均响应时间:104.45
样本下的响应时间中位数:104.0
样本下的响应时间90分位数:181.0
=======时间:Tue Mar 17 21:28:09 CST 2020,统计值如下=======
请求总数(持续累计):1187
平均响应时间:104.72673434856176
最小响应时间:10.0
最大响应时间:200.0
样本大小(取样本):246
样本下的平均响应时间:101.32926829268293
样本下的响应时间中位数:103.0
样本下的响应时间90分位数:177.0

稍微核对一下数据:

  • 平均rt大概100ms,所以1s钟可以收到10次请求,5s的窗口就是收到50次请求
  • 公开启5个线程,所以每个窗口内收到的请求是50 * 5 = 250个左右
  • 观察每次样本大小数:250左右

可以看到数值都是吻合的,证明我们的示例木有啥问题。从控制台看到Server的历史持续状态、抽样的状态值一览无余,这就是监控,这就是负载均衡的“粮食”。


总结

关于Ribbon对服务器状态的管理ServerStats的介绍就到这了。本文花大篇幅介绍了很少人关注的Server状态统计这块的知识点,是因为这对理解Ribbon的核心非常之重要,对Ribbon是如何负载均衡选择Server的策略研究更是非常关键。

建议小伙伴可以不仅局限于当个“配置工程师”,而是花时间花精力深入其内了解起来,内部才是星辰大海,才有财富宝石。

Ribbon服务器状态:ServerStats及其断路器原理相关推荐

  1. Hystrix断路器原理及实现(服务降级、熔断、限流)

    Hystrix断路器原理及实现(服务降级.熔断.限流) 分布式系统面临的问题 Hystrix重要概念(面试常考) Hystrix案例 Hystrix 服务提供者 Hystrix 服务消费者 原因与解决 ...

  2. SpringCloud组件:Ribbon负载均衡策略及执行原理!

    大家好,我是磊哥. 今天我们来看下微服务中非常重要的一个组件:Ribbon.它作为负载均衡器在分布式网络中扮演着非常重要的角色. 本篇主要内容如下: 在介绍 Ribbon 之前,不得不说下负载均衡这个 ...

  3. java断路器原理_Netflix Hystrix断路器原理分析

    断路器原理 断路器在HystrixCommand和HystrixObservableCommand执行过程中起到至关重要的作用.查看一下核心组件HystrixCircuitBreaker packag ...

  4. 【夯实Spring Cloud】Spring Cloud中使用Hystrix实现断路器原理详解(上)

    本文属于[夯实Spring Cloud]系列文章,该系列旨在用通俗易懂的语言,带大家了解和学习Spring Cloud技术,希望能给读者带来一些干货.系列目录如下: [夯实Spring Cloud]D ...

  5. linux服务器六个状态,六、Linux_SSH服务器状态

    一.保持Xshell连接Linux服务器状态 1.登录服务器后 cd /etc/ssh/ vim sshd_config 找到 ClientAliveInterval 0和ClientAliveCou ...

  6. mc服务器状态查询php,PHP下查询游戏《Minecraft》多人游戏 服务器的人数。

    1 <?php2 3 /**4 * Minecraft服务器状态查询5 * @作者 Julian Spravil Git地址:https://github.com/FunnyItsElmo6 * ...

  7. Java服务器热部署的实现原理

    [本文转载于Java服务器热部署的实现原理] 今天发现早年在大象笔记中写的一篇笔记,之前放在ijavaboy上的,现在它已经访问不了了.前几天又有同事在讨论这个问题.这里拿来分享一下. 在web应用开 ...

  8. PHP获取CentOS服务状态,简单linux下php获取服务器状态代码

    简单的linux下的php获取服务器状态的代码,不多说-直接上函数: function get_used_status(){ $fp = popen('top -b -n 2 | grep -E &q ...

  9. 【Android RTMP】RTMP 直播推流阶段总结 ( 服务器端搭建 | Android 手机端编码推流 | 电脑端观看直播 | 服务器状态查看 )

    文章目录 安卓直播推流专栏博客总结 一. 服务器搭建 二. 手机端推流 三. 电脑端观看直播 四. RTMP 服务器端状态 安卓直播推流专栏博客总结 Android RTMP 直播推流技术专栏 : 0 ...

最新文章

  1. 深度丨谈谈人工智能的潜力、实践意义和目前存在的障碍
  2. python 实现显著性检测_强!汽车车道视频检测:python+OpenCV为主实现
  3. centos一键安装redmine
  4. vue 跳转页面带对象_vue跳转页面的几种方法(推荐)
  5. 话里话外:谁才是流程的主人
  6. php和mysql防伪网站源码,2015年最新php+mysql防伪查询程序源码微信认证查询含7套模板...
  7. linux网络总线的作用,I2C总线是什么?基于I2C总线的Linux系统有哪些优点?
  8. multiprocessing模块
  9. jsp企业员工请假管理系统
  10. 大学校园无线智能调频广播系统
  11. GridView自动排序
  12. 5--残差网络(ResNet)
  13. EXCEL工作表保护密码破解
  14. 如何选择云主机或者VPS挂EA?
  15. csdn博客更换皮肤
  16. java解析加密excel,java poi 打开加密 excel?该怎么处理
  17. golang和经济学相关资料学习,还不错,果然B站是个学习的好地方。
  18. 编译安装nginx并实现反向代理负载均衡和缓存功能
  19. devops包括什么_名字叫什么? DevOps版。
  20. Unity中抛物线的实现

热门文章

  1. MySQL中SELECT语句简单使用
  2. python综合练习1-- 用户登录
  3. 学生管理系统——基于双向循环链表
  4. 【LoadRunner】安装LoadRunner时提示缺少vc2005_sp1_with_atl_fix_redist解决方案
  5. 解决SwipeRefreshLayout左右滑动事件冲突的问题
  6. 【百度地图API】暑假放假回老家——城市切换功能
  7. 淘宝商品库MySQL优化实践的学习
  8. SQL2005实现全文检索的步骤 停止数据库的用户连接
  9. Javascript实现浏览器菜单命令
  10. php集合与数组的区别,java集合与数组的区别