超时设置应该被重视,因为超时设置的问题,已经引发了多起 P3 以上级别的事件。
通过配置合理的超时时间,可以防止依赖服务超时时间太长而响应慢,导致自己服务响应慢甚至崩溃。
从一个请求到最终响应,整个链路中有多处有独立的超时设置:
1、代理层超时与重试https://en.wikipedia.org/wiki/Failover:如 Proxy、Nginx,这些组件实现代理功能,如 Nginx 可以实现请求的负载均衡。需要设置代理与后端真实服务器之间的网络连接/读/写超时时间。
2、Web 容器超时:如 Tomcat、Jetty 等,提供 HTTP 服务运行环境的。需要设置客户端与容器之间的网络连接/读/写超时时间,和在此容器中默认 Socket 网络连接/读/写超时时间。
3、中间件客户端超时与重试:如 SOA、QMQ 等,需要设置客户的网络连接/读/写超时时间与失败重试机制。
4、数据库客户端超时:如 Mysql,需要分别设置 JDBC Connection、Statement 的网络连接/读/写超时时间。事务超时时间,获取连接池连接等待时间。
5、NoSQL 客户端超时:如 Redis,需要设置其网络连接/读/写超时时间,获取连接池连接等待时间。
6、业务超时。
核心原则:绝大多数场景下,应该依据超时设置时间来优化响应时间,而非以响应时间来决定超时时间。
● 合理超时时长应该从以下角度考虑:用户(300ms-2000ms 较佳)、技术(TCP、丢包重试等)、资源消耗(超时应该和 QPS 成反比)、业务场景。
● 超时时间不可过长,过⻓容易引起降级失效、系统崩溃、连接池爆满等问题。也不可设置过短,实际⽣产中容易因⽹络抖动⽽告警频繁,造成服务不稳定等⽤户体验问题。
● 服务之间尽量使用 non-blocking io,可以有效降低依赖服务对自身的影响。
● 调用阻塞方法时,使用带参数的方法代替无参方法。如:Future.get()。
● 设置调用方的超时时间之前,先了解清楚依赖服务的 TP99 响应时间是多少(如果依赖服务性能波动大,也可以看 TP95 或 90),调用方的超时时间可以在此基础上加 20~50%。超时设置应该使用配置,以灵活应对下游依赖响应时间的变化。
● 全局超时时间应该要略大于接口级别最长的耗时时间,每个接口的超时时间应该要略大于方法级别最长的耗时时间,每个方法的超时时间应该要略大于实际的方法执行时间。
● 服务端超时也需要设置,这样能避免客户端不设置的情况下配置是合理的,减少隐患。
● 如果调用方是高 QPS 服务,则必须考虑服务方超时情况下的降级和熔断策略。
● 服务降级时的快速失败原则可以做到服务级别的故障隔离,对于 QPS 较高的应用强烈建议使用。
● 大批量任务场景下,使用 RingBuffer(时间窗口)代替单 timer 可以更高效的触发超时。
● 导致超时出现的原因非常多,适当的资源隔离可能降低这种情况:线程、数据等。
● 超时是一种未知的状态:被调服务是否执行成功,这个状态是未知的。某些场景下可以根据业务需求反过来提升用户体验,但后续总应该有补偿或校验机制,避免产生更大的损失。
● 超时时间设置是系统的第⼀重保护,超时时间设置后,需要配合超时告警和响应时间优化,才能形成完整的系统⾃我修复的闭环。
● 超时的治理,核心还在于系统或者架构的优化。基于核心原则,合适的优化总是应该被考虑的。
fail-over&fail-back: https://en.wikipedia.org/wiki/Failover
fail-fast:https://en.wikipedia.org/wiki/Fail-fast
fail-safe:https://en.wikipedia.org/wiki/Fail-safe
SOA:服务的超时设置场景比较常见,也往往存在多重依赖,设置会比较复杂,单独列出,如下。 默认超时设置:
| 超时 | 默认值 | 说明 |
|---|---|---|
| ConnectionRequestTimeout | 200ms | 从 connection pool 获取一个 connection 最大等待时间 |
| ConnectTimeout | 1050ms | 与服务端建立连接的超时时间 |
| SocketTimeout | 5000ms | 每次在 Socket 上调用 write/read 的超时时间 |
1、可以通过代码或者 QConfig 配置。具体参考公司 Java 客户端配置。
2、客户端和服务端都应该设置超时时间,优先级客户端设置的超时>服务端的超时设置。
3、如果存在多级依赖关系,如 A 调用 B,B 调用 C,则超时设置应该是 A>B>C。如果依赖链路不长,可以考虑前端指定超时时长,后续依次减去自己的耗时后传递, 后续服务在确认已超时的情况下应及时中止。
注意:这里设置的超时并不是这次调用我们需要等待的最长时间,有可能真正的响应时间远远大于这个时间,但并没有触发超时异常。
失败重试永远要小心地为自己的服务和客户端添加重试逻辑,因为大量的重试可能会使事情更糟, 甚至阻止应用程序恢复。尤其是多重依赖的场景,重试可能触发多个其他请求或重试,并启动级联效应,导致请求快速膨胀,等于模拟了 DDos 攻击效果,结果可能是灾难性的。因此,务必设置合理的重试机制,并且应该和熔断、快速失败、降级机制配合。 是否需要进行重试,应该是由使用者经过场景分析后自行决定的,框架或者工具类,不应该介入到业务层面,帮使用者去做这件事。
重试逻辑固定循环次数:最常见的做法,这种方案往往没有考虑 backoff,对于下游来说会在失败发生时进一步遇到更多的请求压力,继而进一步恶化。
带固定 delay 的方式: 失败之后,进行固定间隔的 delay, delay 的方式按照是方法本身是异步还是同步的,可以通过定时器或则简单的 Thread.sleep 实现。 虽然这次带了固定间隔的 backoff,但是每次重试的间隔固定,此时对于下游资源的冲击将会变成间歇性的脉冲;特别是当集群都遇到类似的问题时,步调一致的脉冲,将会最终对资源造成很大的冲击,并陷入失败的循环中。
带随机 delay 的方式:实现类似上面的,但具体的 delay 时间,实在一个范围内浮动。这种方式的问题在于:虽然现在解决了 backoff 的时间集中的问题,对时间进行了随机打散,但是依然存在下面的问题:
● 如果依赖的底层服务持续地失败,该方法依然会进行固定次数的尝试,并不能起到很好的保护作用。
● 对结果是否符合预期,是否需要进行重试依赖于异常。
● 无法针对异常进行精细化的控制,如只针部分异常进行重试。
可进行颗粒度控制的重试,可以根据异常类型等进行定制化的重试, 样板代码:
public staticT retry(final Callable callable, final int times, final int minBackoff, final int maxBackoff, final double randomFactor, final Predicate condition) { for (int i = 0; i < times; i++) { try { return callable.call(); } catch (Exception ex) { if (!condition.test(ex)) { break; } doSleep(random(minBackoff, maxBackoff, randomFactor)); } } return null; }
相对来说,已经比较复杂了,可以使用 spring-retry 实现, 见后。这种方案仍然没有解决下游的持续性失败问题
1、避免过多无效的重试
■ 使用背压机制:背压说明 背压策略
■ 核心依赖服务挂掉或出错率很高时,可以考虑暂缓调用,改为采样重试,可以避免给下游服务带来更大的压力,也能及时发现下游服务是否恢复(成功率、响应时间等)。
2、结合熔断和服务降级
■ 熔断设置参见: 客户端熔断配置说明, 服务端熔断配置说明(Java)
■ 服务降级:保留核心请求,快速拒绝其它请求。有助于服务的自我恢复,也可以避免大量重试带来的雪崩。
■ 使用背压机制避免过多无效的重试。
■ 针对不同的异常类型,如超时、业务异常(Error Code)、失败,应该根据实际场景定制自己的容错机制(failover、failfast、failsafe 等)。
在整个系统中可能存在一些看不见的重试机制,如 SOA 框架、CDubbo、HttpClient。
SOA: 这里的重试对用户透明,并且只支持同步调用
| 配置项 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|
| soa.client.exception.retry.enable | false | true, false | HTTP Apache Client 提供的重试机制,重试时不换 IP 地址 |
| soa.client.exception.retry.times | 2 | [1, 100] | - |
| soa.client.framework.retry.enabled | true | true, false | SOA 框架提供的重试机制,重试时会按照负载均衡策略重新选取 IP 地址 |
| soa.client.framework.retry.times | 2 | [1, 100] | - |
支持用户设置自定义的异常重试:
// client = YourClient.getInstance(); // 新增希望HttpClient进行重试的异常class client.getIOExceptionRetryHelper().addHttpClientRetryExceptionClasses(Class>... httpClientRetryExceptionClasses); // 新增希望框架进行换机器重试的异常class client.getIOExceptionRetryHelper().addframeworkRetryExceptionClasses(Class>... frameworkRetryExceptionClasses);
CDubbo: CDubbo 默认关闭了重试(因为 Dubbo 默认采用了 failover),设置方法参见:CDubbo 扩展配置
Dubbo: Dubbo 有多种容错模式,参见文档
由于种种的显性或隐性的重试机制,我们在做服务设计时应该尽量考虑幂等性,避免重复调用导致的数据污染。幂等性是指一次操作与多次操作产生相同的结果,并不会因为多次操作产生不一致性。常见的方案有取消重试、幂等表、状态机、一致性 ID 等等,有很多成熟的方案可以参考,这里不再赘述。对于写服务,实现了幂等性,重试机制才是允许的。
重试工具 Spring RetrySpring Retry是Spring框架提供的一套高效且实用的重试工具,既可通过编码方式开箱使用也可直接使用注解。它提供了重试兜底、设置策略、熔断等功能,加以组合可以实现让系统安全地自动化重试。
org.springframework.retry spring-retry
主要包括以下几个注解:
@EnableRetry: 表示开启重试机制,可设置代理模式,默认使用JDK动态代理
@Retryable: 表示这个方法需要重试,它有很丰富的参数,可以设定策略来满足对重试的需求
@Backoff: 表示重试中的退避策略
@Recover: 兜底方法,即多次重试后还是失败就会执行这个方法
@Configuration
@EnableRetry
public class Application {
@Bean
public Service service() {
return new Service();
}
}
@Service
class Service {
@Retryable(value = RuntimeException.class)
public void service() {
// ... do something
}
@Recover
public void recover(RemoteAccessException e) {
// ... do recover
}
}
Spring Retry提供了丰富的策略供系统使用,包括重试策略和退避策略:
【1】重试策略:
■ NeverRetryPolicy: 只允许调用 RetryCallback 一次,不允许重试
■ AlwaysRetryPolicy: 允许无限重试,直到成功,此方式逻辑不当会导致死循环
■ SimpleRetryPolicy: 固定次数重试策略,默认重试最大次数为 3 次,RetryTemplate 默认使用策略
■ TimeoutRetryPolicy: 超时时间重试策略,默认超时时间为 1 秒,在指定的超时时间内允许重试
■ CircuitBreakerRetryPolicy: 有熔断功能的重试策略,需设置 3 个参数 openTimeout、 resetTimeout 和 delegate
■ CompositeRetryPolicy: 组合重试策略。有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即不可以。但不管哪种组合方式,组合中的每一个策略都会执行
退避策略
■ NoBackOffPolicy: 无退避算法策略,即当重试时是立即重试
■ FixedBackOffPolicy: 固定时间的退避策略,需设置参数 sleeper 等待策略(默认Thread.sleep)和 backOffPeriod 休眠时间(默认 1 秒)
■ UniformRandomBackOffPolicy: 随机时间退避策略,需设置 sleeper,minBackOffPeriod 最小退避间隔(默认 500 毫秒)和 maxBackOffPeriod 最大退避间隔(默认 1500 毫秒),该策略在 [ minBackOffPeriod , maxBackOffPeriod ] 之间取一个随机休眠时间
■ ExponentialBackOffPolicy: 指数退避策略,需设置参数 sleeper,initialInterval 初始休眠时间(默认 100 毫秒), maxInterval 最大休眠时间(默认 30 秒) 和 multiplier 指定乘数,即下一次休眠时间为当前休眠时间 * multiplier
■ ExponentialRandomBackOffPolicy: 随机指数退避策略,引入随机乘数,固定乘数可能会引起很多服务同时重试导致DDos
源码地址前往:Spring-Retry
【2】Guava Retry: Guava Retry是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。
com.github.rholder guava-retrying
其中 Retryer是最核心的类,用于执行重试策略。通过RetryerBuilder类进行构造,将设置好的重试策略添加到 Retryer中,最终通过执行 Retryer的核心方法 call来执行重试策略。
Callablecallable = new Callable () { public Boolean call() throws Exception { return true; // do something } }; Retryer retryer = RetryerBuilder. newBuilder() .retryIfResult(Predicates. isNull()) .retryIfExceptionOfType(IOException.class) .retryIfRuntimeException() .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .build(); try { retryer.call(callable); } catch (RetryException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }
源码地址前往: Guava-Retry
Java异常异常处理类异常:程序在运行时,发生了不可预测的事件,干扰了正常的指令流程,它阻止了程序按照预期正常执行。
Java 把异常当做是破坏正常流程的一个事件,当事件发生后,就会触发处理机制。Java 有一套独立的异常处理机制,在遇到异常时,方法并不返回任何值(返回值属于正常流程),而是抛出一个封装了错误信息的对象。
Throwable所有的异常对象都派生于 Throwable 类的一个实例。在一个 Throwable 里面可以获取如下信息:
■ 获取堆栈跟踪信息(源代码中哪个类,哪个方法,第几行出现了问题……从当前代码到最底层的代码调用链都可以查出来)
■ 获取引发当前 Throwable 的 Throwable,追踪获取底层的异常信息
■ 获取被压抑了,没抛出来的其他 Throwable。一次只能抛出一个异常,如果发生了多个异常,其他异常就不会被抛出,这时可以通过加入 suppressed 异常列表来解决(JDK7 以后才有)
■ 获取基本的详细描述信息
Throwable 类只有两个直接继承者:Error 和 Exception。然后 Exception 又分为 RuntimeException 和 CheckedException。
在 Java 中, 由系统环境问题引起的异常,一般都继承于 Error 类。对于 Error 类:
■ 一般开发者不要自定义 Error 子类,因为它代表系统级别的错误。与一般的程序无关。
■ 在 Java 异常处理机制中,Error 不强制捕获或声明,也就是不强制处理。因为程序本身对此类错误无能为力。一般情况下我们只要把堆栈跟踪信息记录下来就行。
在 Java 中,除了系统环境问题引起的异常,一般都继承于 Exception 类。Exception 分为 RuntimeException 和 CheckedException。CheckedException 必须要捕获或声明。而 RuntimeException 不强制。
CheckedException在 Java 中,直接或间接因为“资源”问题引起的异常,一般属于检查异常(CheckedException) 。检查异常继承于 Exception,而不继承于 RuntimeException。对于检查异常:
■ 必须捕获或声明
■ 交给关心这个异常的方法处理
■ 异常处理器应该引导用户接下来怎么办,至少做到安全退出
在 Java 中,由于接口方法使用不当造成的异常,一般属于 RuntimeException,也就是运行时异常。对于 RuntimeException:
■ 如果你调用服务方法的方式不正确,你应该马上修改代码,避免发生 RuntimeException
■ 如果是用户方法调用你的方法的方式不正确,你应该立刻抛出 RuntimeException,强制让使用者修正代码或改变使用方式,防止问题蔓延
■ 一般情况下,不要捕获或声明 RuntimeException。因为问题在于你的程序本身有问题,如果你用异常流程处理了,反而让正常流程问题一直存在
Error 和 RuntimeException 统称为非检查异常。两者的共同点就是都不被强制捕获或声明。实际上两者描述问题的范围完全没有交集。
总结所有的功能都在 Throwable 类里面实现了,子类只需要直接继承或间接继承它,并且加上需要的构造方法就行(一般而言,第一第二个构造方法是必须的,也可以全部加上),而且构造方法通常只需要一行代码:super(…),也就是说只要调用父类的构造方法就行了。Java 把异常分为三类(Error,Checked Exception,RuntimeException),只是在语法层面上有不同的标记而已。它们自身拥有的功能一样,运行时系统处理它们的方式也是一样的(你也可以捕获或声明非检查异常),不同的是编译器对它们的区别对待(检查异常必须要在代码里处理,非检查异常就不需要),以及程序员对它们的区别对待(这需要程序员遵循良好的实践原则)。
Java 异常处理机制在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
抛出异常: 当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
捕获异常: 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着 Java 程序的终止。
对于运行时异常、错误或可查异常,Java 技术所要求的异常处理方式有所不同。
■ 由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java 规定,运行时异常将由 Java 运行时系统自动抛出,允许应用程序忽略运行时异常。
■ 对于方法运行中可能出现的 Error,当运行方法不欲捕捉时,Java 允许该方法不做任何抛出声明。因为,大多数 Error 异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
■ 对于所有的可查异常,Java 规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
■ 能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者 Java 运行时系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是 Java 代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。
■ 任何 Java 代码都可以抛出异常,如:自己编写的代码、来自 Java 开发环境包中代码,或者 Java 运行时系统。无论是谁,都可以通过 Java 的 throw 语句抛出异常。
■ 从方法中抛出的任何异常都必须使用 throws 子句。捕捉异常通过 try-catch 语句或者 try-catch-finally 语句实现。
捕获异常 try-catch 语句总体来说,Java 规定:对于可查异常必须捕捉、或者声明抛出。允许忽略不可查的 RuntimeException 和 Error。
在 Java 中,异常通过 try-catch 语句结束。
关键词 try 后的一对大括号将一块可能发生异常的代码包起来,称为监控区域。Java 方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由 Java 运行时系统试图寻找匹配的 catch 子句以捕获异常。若有匹配的 catch 子句,则运行其异常处理代码,try-catch 语句结束。
匹配的原则是: 如果抛出的异常对象属于 catch 子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与 catch 块捕获的异常类型相匹配。
需要注意的是,一旦某个 catch 捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个 try-catch 语句结束。其他的 catch 子句不再有匹配和捕获异常类型的机会。
对于有多个 catch 子句的异常程序而言,应该尽量将捕获底层异常类的 catch 子句放在前面,同时尽量将捕获相对高层的异常类的 catch 子句放在后面。否则,捕获底层异常类的 catch 子句将可能会被屏蔽。
try-catch 语句还可以包括第三部分,就是 finally 子句。它表示无论是否出现异常,都应当执行的内容。
无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。在以下 4 种特殊情况下,finally 块不会被执行:
■ 在 finally 语句块中发生了异常
■ 在前面的代码中用了 exit()退出程序
■ 程序所在的线程死亡
■ 关闭 CPU
除了在各个方法层面捕获异常外,还有一种是全局层面的异常捕获机制,此机制各个框架都有自己的特定实现,总结全局异常处理主要的好处有如下几点:
【1】统一接口返回格式。比如说请求方法错误,本来是 get 但是用成了 post,这种错误都不会经过自己写的代码,无法控制返回的数据格式。
【2】方便记录日志。跟上面一样的道理,有的错误框架直接返回了,可能都没有日志记录。
【3】减少冗余代码。统一处理总比每一个接口自己处理要好吧。
【4】让业务处理代码更加优雅,结构更加简洁,可阅读性强。 举例:在一个请求的处理过程中,如果中途发现存在不可恢复的问题(比如数据检查发现异常了)导致的执行流程需要中断,返回错误信息;如果用正常的处理手法,那么我们需要一层层将异常信息回馈回去,然后在最外层(Controller)进行处理,输出 Message 。 但是如果有全局处理机制,那么我们可以直接在需要中断的地方,Throw 一个自定义的 RuntimeException,这样代码可阅读性会大大增强。
既然全局异常处理那么重要,那么如何做呢? 我们以 SpringBoot 为例,有如下几个:
【1】@ControllerAdvice: 相当于 controller 的切面,主要用于@ExceptionHandler, @InitBinder 和@ModelAttribute,使注解标注的方法对每一个 controller 都起作用。默认对所有 controller 都起作用,当然也可以通过@ControllerAdvice 注解中的一些属性选定符合条件的 controller
【2】@ExceptionHandler: 用于异常处理的注解,可以通过 value 指定处理哪种类型的异常还可以与@ResponseStatus 搭配使用,处理特定的 http 错误。标记的方法入参与返回值都有很大的灵活性,具体可以看注释也可以后边的深度探究。
【3】自定义AOP切面来处理: 这个也是当前正在使用的,通过自己定义切面,我们定制了处理过程中的共同内容,如 异常处理、日志信息 等逻辑。
任何 Java 代码都可以抛出异常,如:自己编写的代码、来自 Java 开发环境包中代码,或者 Java 运行时系统。无论是谁,都可以通过 Java 的 throw 语句抛出异常。从方法中抛出的任何异常都必须使用 throws 子句。
throws 抛出异常如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用 throws 子句来声明抛出异常。
throws 语句用在方法定义时声明该方法要抛出的异常类型,如果抛出的是 Exception 异常类型,则该方法被声明为抛出所有的异常。多个异常可使用逗号分割。当方法抛出异常列表的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由他去处理。
使用 throws 关键字将异常抛给调用者后,如果调用者不想处理该异常,可以继续向上抛出,但最终要有能够处理该异常的调用者。
Throws 抛出异常的规则:
【1】如果是不可查异常(unchecked exception),即 Error、RuntimeException 或它们的子类,那么可以不使用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
【2】必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误
【3】仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
【4】调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
判断一个方法可能会出现异常的依据如下:
【1】方法中有 throw 语句
【2】调用了其他方法,其他方法用 throws 子句声明抛出某种异常
throw 总是出现在函数体中,用来抛出一个 Throwable 类型的异常。程序会在 throw 语句后立即终止,它后面的语句执行不到,然后在包含它的所有 try 块中(可能在上层调用函数中)从里向外寻找含有与其匹配的 catch 子句的 try 块。
我们知道,异常是异常类的实例对象,我们可以创建异常类的实例对象通过 throw 语句抛出。该语句的语法格式为:
throw new exceptionname;
要注意的是,throw 抛出的只能够是可抛出类 Throwable 或者其子类的实例对象。
如果抛出了检查异常,则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。
如果所有方法都层层上抛获取的异常,最终 JVM 会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是 Error 或 RuntimeException,则该方法的调用者可选择处理该异常。
Java 异常处理实践原则【1】只针对不正常的情况才使用异常,对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常,避免不必要的使用被检查的异常,消除运行时异常
【2】尽量使用标准的异常,尽量处理最具体的异常
【3】抛出的异常要适合于相应的抽象,给出准确的异常处理信息
【4】只将非稳定代码放在 try-catch 块中,在 finally 中清理资源,不要在 finally 中写 return 语句,影响 try中的返回值。
【5】不要在 catch 中使用 Throwable。catch (Throwable)
如果你在 catch 子句中使用 Throwable,它将不仅捕获所有的异常(Exception),还会捕获所有错误(Error)。JVM 会抛出错误,这是应用程序不打算处理的严重问题。典型的例子是 OutOfMemoryError 或 StackOverflowError。这两种情况都是由应用程序控制之外的情况引起的,无法处理。所以,最好不要在 catch 中使用 Throwable,除非你完全确定自己处于一个特殊的情况下,并且你需要处理一个错误。
【6】正确处理异常
【7】不要在 catch 块中忽略捕获的异常;
【8】不要在 catch 中记录异常然后又抛出这个异常;
【9】将异常抛给函数调用者,如果一个方法可能会出现异常,但其自身却没有能力处理这种异常,可以在方法声明处用 throws 子句来声明抛出异常,由上层的方法调用者处理。
配置容错必须牢记的底线!千万不要只打印了一个日志,就把异常拦在了某一层。终止异常向上传递前,务必三思当前的处理是否到位!
纵观历史,15%以上RCA(根本原因分析 Root Cause Analysis)是因为修改配置导致,其中很大一部分是修改配置的时候输入了错误格式,程序无法解析,进而导致程序崩溃。
故障案例某日11点23分 xxx处理中量(5s)智能检测发现异常下降60%
事后分析得知其根因是:修改模板配置时,多配了个占位符 %S 导致字符串格式化的方法出错,程序未进行异常捕获,输入页加载查询接口失败,前端页面进行降级,关闭了入口,影响用户操作。
研发团队给出的解决方案:针对读取配置文案异常需要进行捕获降级,不能影响业务主流程
案例分析针对这类错误,以往的解决方案通常是打补丁式的修正,即:针对已发生异常的配置项进行容错处理(对解析函数添加try…catch,如上方)。
然而,每一次异常几乎都是发生在不同的配置上,也就是说我们的补丁对于生产事件的产生几乎没有任何抑制作用。这就好比马路上布满了存在设计缺陷的井盖,当某个井盖发生故障,我们就把它换成一个新型井盖,其他存在缺陷的井盖继续放任不管。
补丁式修正只能治标而不能治本,我们需要一种能治本的方案。下面就让我来给大家介绍这么一种方案(如果您有更好的方案,望不吝赐教):
最佳实践程序初始化时将所有配置读取并解析后保存为本地变量,应用每次直接读取本地Config变量。
监听QConfig,当配置发生变化时调用步骤1的方法重新解析配置文件。如果解析时发生异常,记录错误日志/报警,并使用旧的配置。
示例代码:
public class ConfigStatic {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigStatic.class);
private static ConfigStatic instance = new ConfigStatic();
private static final String TITLE = "ConfigStatic";
private final boolean initializeSuccess;
static {
// 将ConfigStatic.refresh注册至ConfigurationFunc,当配置发生变化的时候ConfigStatic.refresh将被调用
ConfigurationFunc.registerListener(TITLE, ConfigStatic::refresh);
}
private ConfigStatic() {
initializeSuccess = initialize();
}
public static ConfigStatic getInstance() {
return instance;
}
private static void refresh() {
// 创建新配置(构造函数会调用initialize进行初始化)
ConfigStatic newConfig = new ConfigStatic();
if (newConfig.initializeSuccess) {
// 如果新实例初始化成功,将其替换为instance
instance = newConfig;
}
}
private Set hsFcAddjustPriceAgencyId;
// 此处省略其他配置...
private boolean initialize() {
final String title = "ConfigStatic.initialize";
try {
this.directCompareNormalAirlines = ConfigurationFunc.getHashSet("yPlusXProductConfig", ",");
// 此处省略其他配置...
return true;
} catch (Exception ex) {
LogManager.build(title, ex).error();
return false;
}
}
}



