目录

JAVA面试题整理(P6-P7)

目录

基础


用最有效率的方法计算2乘以8 (P6)

1
int a = 2 << 3; //左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方

日常开发中,你见过哪些信息编码,并简述这些编码的常用场景。(P6)

  • unicode

\u开头,Unicode(又称统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

  • URL编码(urlencode)

%开头,BS架构中请求参数中会经常用到,适用于统一资源标识符(URI)的编码。

  • base64

Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据(比如图片)的方法。

  • MD5/sha1

一种被广泛使用的密码散列(hash)函数,常用于信息传输校验、文件校验。

简述,浏览器地址栏输入URL回车后,发生了什么。(P7)

@link: 在浏览器输入URL回车之后发生了什么?(超详细版)

举例说明同步和异步(P6)

如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

在软件工程中,请简述双重检查锁定(Double-checked locking)的作用 (P7)

在软件工程中,双重检查锁定(也称为“双重检查锁定优化”)是一种软件设计模式,用于在获得锁之前通过测试锁定标准(“锁定提示”)来减少获取锁的开销。只有在锁定条件检查表明需要锁定时,才会发生锁定。如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Foo {
    private Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

& 和 && 的区别? (P6)

&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。

算法


给定字符串字母若干+数字若干, 如"PS2020" "B2021"等,写一个函数判断第一个数字字符所在的位置。要求效率最高。(P6)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static int numStart(String str) {
    if (str == null) {
        return -1;
    }
    for (int i = 0; i < str.length(); i++) {
        if (str.charAt(i) >= '0' && str.charAt(i) <= '9')
            return i;
    }
    return -1;
}

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。(P6)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 示例1:
输入: 123
输出: 321

# 示例2:
输入: -123
输出: -321

# 示例3:
输入: 120
输出: 21

解答:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Solution {
    public int reverse(int x) {
        int ret = 0;
        while (x != 0) {
            if( ret * 10 / 10 != ret )
                return 0;
            ret = ret * 10 + x % 10;
            x = x / 10;
        }
        return ret;
    }
}

JAVA


JVM垃圾回收算法和垃圾回收器有哪些,最新的JDK采用什么算法。(P7)

  • 算法:

标记-清除(Mark-Sweep)算法 复制(Copying)算法 标记-整理算法 分代收集算法

  • 收集器:

串行收集器 并行收集器 Concurrent Mark and Sweep (CMS) G1 - Garbage First

  • 最新的JDK使用:G1

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?(P6)

是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和C#中可以通过传引用或传输出参数来改变传入的参数的值。在C#中可以编写如下所示的代码,但是在Java中却做不到。

重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?(P6)

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。

为什么不能根据返回类型来区分重载?(P7)

  • 原理上:

在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名;特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载。

  • 实践上:

因为方法在被调用的时候,可以不需要返回值,比如 直接调用 max();,因此无法明确调用的是哪个方法。

具有内存回收机制的JAVA中,会存在内存泄漏吗,请简单描述。(P6)

理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。

try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后? (P6)

会执行,在方法返回调用者前执行。

注意
在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,就会返回修改后的值。显然,在finally中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java中也可以通过提升编译器的语法检查级别来产生警告或错误,Eclipse中可以在如图所示的地方进行设置,强烈建议将此项设置为编译错误。

说出下面代码的运行结果。(此题的出处是《Java编程思想》一书)(P6)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Annoyance extends Exception {}
class Sneeze extends Annoyance {}

class Human {

    public static void main(String[] args) 
        throws Exception {
        try {
            try {
                throw new Sneeze();
            } 
            catch ( Annoyance a ) {
                System.out.println("Caught Annoyance");
                throw a;
            }
        } 
        catch ( Sneeze s ) {
            System.out.println("Caught Sneeze");
            return ;
        }
        finally {
            System.out.println("Hello World!");
        }
    }
}
1
2
3
4
javac ./Human.java && java Human
Caught Annoyance
Caught Sneeze
Hello World!

当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?(P6)

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,方法A和方法B的锁是同一个锁即对象锁,当进入方法A后获得了对象锁,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

JAVA中如何实现任务调度? (P6)

  • JDK原生定时工具:Timer

所有任务都是由同一个线程来调度,所有任务都是串行执行,意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。

  • JDK对定时任务调度的线程池:ScheduledExecutorService

由于Timer存在的问题,JDK5之后便提供了基于线程池的定时任务调度:ScheduledExecutorService。 设计理念:每一个被调度的任务都会被线程池中的一个线程去执行,因此任务可以并发执行,而且相互之间不受影响。

  • Quartz

虽然ScheduledExecutorService对Timer进行了线程池的改进,但是依然无法满足复杂的定时任务调度场景。 因此OpenSymphony提供了强大的开源任务调度框架:Quartz。 Quartz是纯Java实现,而且作为Spring的默认调度框架。

  • 分布式定时任务框架: Elastic-Job

基于Quartz和Zookepper开发并开源的一个Java分布式定时任务。Elastic job主要的功能有支持弹性扩容,通过Zookepper集中管理和监控job,支持失效转移等。

Statement和PreparedStatement有什么区别?哪个性能更好?(P7)

  1. PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性)

  2. PreparedStatement中的SQL语句是可以带参数的,避免了用字符串连接拼接SQL语句的麻烦和不安全

  3. 当批量处理SQL或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)

在进行数据库编程时,连接池有什么作用? (P6)

由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行TCP四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid、HikariCP等。

在JAVA中,能否获取一个对象的私有属性的值? (P6)

可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。

在JAVA中,能否修改一个第三方jar包里已经存在的函数?(P7)

可以使用java字节码生成框架ASM、Javassist和byteBuddy等,对已有的函数进行动态代理。

什么是双亲委派机制,双亲委派机制的作用是什么。 (P6)

概念:

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

作用:

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

  • 最基础:Bootstrap ClassLoader(加载JDK的/lib目录下的类)
  • 次基础:Extension ClassLoader(加载JDK的/lib/ext目录下的类)
  • 普通:Application ClassLoader(程序自己classpath下的类)

为什么一些场景需要破坏双亲委派模型?举几个例子。(P7)

@link Tomcat 类加载器之为何违背双亲委派模型

用Java写一个单例类 (P6)

  • 饿汉式单例
1
2
3
4
5
6
7
public class Singleton {
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}
  • 懒汉式单例
1
2
3
4
5
6
7
8
public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static synchronized Singleton getInstance(){
        if (instance == null) instance  new Singleton();
        return instance;
    }
}

String 是最基本的数据类型吗? (P6)

不是。Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5以后引入的枚举类型也算是一种比较特殊的引用类型。

switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上? (P6)

  • 在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。

  • 从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型。

  • 从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

在Java中,如何跳出当前的多重嵌套循环? (P6)

在最外层循环前加一个标记如A,然后用break A; 可以跳出多重循环。

如何实现对象克隆 (P6)

  1. 实现Cloneable接口并重写Object类中的clone()方法
  2. 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

Spring中Bean的作用域有哪些 (P6)

  • 在Spring的早期版本中,仅有两个作用域:singleton和prototype,前者表示Bean以单例的方式存在;后者表示每次从容器中调用Bean时,都会返回一个新的实例,prototype通常翻译为原型。

  • Spring 2.x中针对WebApplicationContext新增了3个作用域,分别是:request(每次HTTP请求都会创建一个新的Bean)、session(同一个HttpSession共享同一个Bean,不同的HttpSession使用不同的Bean)和globalSession(同一个全局Session共享一个Bean)。

JVM 发生 OOM 的几种原因、及解决办法 (P7)

  1. Java 堆空间
1
2
3
4
5
6
7
8
9
# 原因:
无法在 Java 堆中分配对象
吞吐量增加
应用程序无意中保存了对象引用,对象无法被 GC 回收
应用程序过度使用 finalizer。finalizer 对象不能被 GC 立刻回收。finalizer 由结束队列服务的守护线程调用,有时 finalizer 线程的处理能力无法跟上结束队列的增长

# 解决:
使用 -Xmx 增加堆大小
修复应用程序中的内存泄漏
  1. GC 开销超过限制
1
2
3
4
5
6
7
# 原因:
Java 进程98%的时间在进行垃圾回收,恢复了不到2%的堆空间,最后连续5个(编译时常量)垃圾回收一直如此。

# 解决:
使用 -Xmx 增加堆大小
使用 -XX:-UseGCOverheadLimit 取消 GC 开销限制
修复应用程序中的内存泄漏
  1. 请求的数组大小超过虚拟机限制
1
2
3
4
5
6
# 原因:
应用程序试图分配一个超过堆大小的数组

# 解决:
使用 -Xmx 增加堆大小
修复应用程序中分配巨大数组的 bug
  1. Perm gen 空间
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 原因:
Perm gen 空间包含:
类的名字、字段、方法
与类相关的对象数组和类型数组
JIT 编译器优化

当 Perm gen 空间用尽时,将抛出异常。

# 解决:
使用 -XX: MaxPermSize 增加 Permgen 大小
不重启应用部署应用程序可能会导致此问题。重启 JVM 解决
  1. Metaspace
1
2
3
4
5
6
7
8
9
# 原因:
从 Java 8 开始 Perm gen 改成了 Metaspace,在本机内存中分配 class 元数据(称为 metaspace)。如果 metaspace 耗尽,则抛出异常

# 解决:
通过命令行设置 -XX: MaxMetaSpaceSize 增加 metaspace 大小
取消 -XX: maxmetsspacedize
减小 Java 堆大小,为 MetaSpace 提供更多的可用空间
为服务器分配更多的内存
可能是应用程序 bug,修复 bug
  1. 无法新建本机线程
1
2
3
4
5
6
7
8
# 原因:
内存不足,无法创建新线程。由于线程在本机内存中创建,报告这个错误表明本机内存空间不足

# 解决:
为机器分配更多的内存
减少 Java 堆空间
修复应用程序中的线程泄漏
使用 -Xss 减小线程堆栈大小
  1. 杀死进程或子进程
1
2
3
4
5
6
7
# 原因:
内核任务:内存不足结束器,在可用内存极低的情况下会杀死进程

# 解决:
将进程迁移到不同的机器上
给机器增加更多内存
与其他 OOM 错误不同,这是由操作系统而非 JVM 触发的。
  1. 发生 stack_trace_with_native_method
1
2
3
4
5
6
# 原因:
本机方法(native method)分配失败
打印的堆栈跟踪信息,最顶层的帧是本机方法

# 解决:
使用操作系统本地工具进行诊断

开源组件


谈谈MySQL的SQL语句优化的经验(P6)

  • 查询语句无论使用哪种判断条件(等于 大于 小于),WHERE 左侧的条件查询字段不要使用表达式函数

  • 使用EXPLAIN命令分析SQL的效率

  • 为每一张表设置一个ID属性

  • 避免WHERE语句中对字段进行NULL判断

  • 避免WHERE语句中使用!=<>操作符

  • 根据业务使用场景,合理创建索引或联合索引

  • 使用LIKE %abc%不走索引,而LIKE abc%会走索引

  • 对于枚举类型的字段,建议使用ENUM INT而不是VARCHAR,如性别、类型等

  • 选择合适的字段类型,标准是尽可能小尽可能定长尽可能使用整数

  • 字段设计尽可能NOT NULL

  • SELECT查询语句只需要使用一条记录时,要使用LIMIT 1

  • 不直接使用SELECT *,而应该使用具体要查询的字段

不需要的列会增加数据传输时间和网络开销 对于无用的大字段,如 varchar、blob、text,会增加 io 操作 失去MySQL优化器“覆盖索引”策略优化的可能性

  • 进行水平分隔或垂直分隔

水平分隔:通过建立结构相同的几张表分别存储数据,比如按时间分隔 垂直分隔:将一些字段单独放在一张表中,分隔后用id或其他唯一业务字段进行关联

是否遇到过 Nginx的 HTTP 499 状态码? 请简述可能产生的原因。(P6)

499对应的是 “client has closed connection”。这很有可能是因为服务器端处理的时间过长,客户端“不耐烦”了。

即调用方设置的时间时间较短但服务方处理时间过长,而导致调用方主动断开。

是否遇到过 Nginx的 104: Connection reset by peer错误? 请简述可能产生的原因。(P7)

errno = 104错误表明nginx在对一个反向代理的对端socket已经关闭的的连接调用write或send方法,在这种情况下,调用write或send方法后,对端socket便会向本端socket发送一个RESET信号,在此之后如果继续执行write或send操作,就会得到errno为104,错误描述为connection reset by peer。

产生这种问题有可能是nginx维护的长连接对端socket异常关闭。

此时, HTTP状态码为502。

为什么使用消息队列 (P6)

  • 核心的有三个:解耦、异步、削峰

在使用MQ消息中间件时,要注意哪些问题?(P6)

  • 作为生产者时,需要做好消息生产投递(ACK)失败时的重试策略。
  • 作为生产者时,需要确定消息是否有序,如果有序需要确定是全局有序还是hash有序。
  • 作为消费者时,需要注意消费组名不能与已有的消费组重名。
  • 作为消费者时,消费的逻辑执行失败后,需要做好重试策略。
  • 作为消费者时,需要保证消息不能重复消费(即需要做好幂等)。
  • 作为消费者时,第一次启动时,需要设置好从哪里消费,一般情况是设定从尾部或指定时间消费。
  • 作为消费者时,要确定是否有序消费,以及有序消费时的ack阻塞造成的消息堆积对业务的影响。

限流、熔断、降级 (P7)

限流
  • 限流解决了什么问题

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。

  • 限流采用的算法-漏桶算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:

[图]漏桶(Leaky Bucket)
  • 限流采用的算法-令牌算法

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.

令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。

[图]令牌桶(Token Bucket)
  • 使用Guava的RateLimiter
1
2
3
4
5
//每秒只发出5个令牌
RateLimiter rateLimiter = RateLimiter.create(5.0);
if(!rateLimiter.tryAcquire()){
	// 如果没有获取到令牌 ...
}
  • NGNIX限流
  1. 按连接数限流(ngx_http_limit_conn_module)(令牌算法实现)
  2. 按请求速率限流(ngx_http_limit_req_module)(漏桶算法实现)
  • Tomcat 线程池限流
  1. maxThreads(最大线程数):每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,那么最大线程数决定了Web服务可以同时处理多少个请求,默认200.
  2. accepCount(最大等待数):当调用Web服务的HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中,这个acceptCount就是指能够接受的最大等待数,默认100.如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)。
  3. maxConnections(最大连接数):这个参数是指在同一时间,tomcat能够接受的最大连接数。一般这个值要大于maxThreads+acceptCount。
  • Redis限流

计数器算法:简陋的设计思路:假设一个用户(用IP判断)每分钟访问某一个服务接口的次数不能超过10次,那么我们可以在Redis中创建一个键,并此时我们就设置键的过期时间为60秒,每一个用户对此服务接口的访问就把键值加1,在60秒内当键值增加到10的时候,就禁止访问服务接口。在某种场景中添加访问时间间隔还是很有必要的。

令牌桶算法:基于 Redis 的 list接口可以实现令牌桶令牌补充和令牌消耗操作。

  • @SentinelResource限流
  1. sentinel-dashboard:与hystrix-dashboard类似,但是它更为强大一些。除了与hystrix-dashboard一样提供实时监控之外,还提供了流控规则、熔断规则的在线维护等功能。

  2. 客户端整合:每个微服务客户端都需要整合sentinel的客户端封装与配置,才能将监控信息上报给dashboard展示以及实时的更改限流或熔断规则等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

@Slf4j
@Service
public class TestService {
 
    @SentinelResource(value = "doSomeThing", blockHandler = "exceptionHandler")
    public void doSomeThing(String str) {
        log.info(str);
    }
 
    // 限流与阻塞处理
    public void exceptionHandler(String str, BlockException ex) {
        log.error( "blockHandler:" + str, ex);
    }   
}

主要做了两件事:

  • 通过@SentinelResource注解的blockHandler属性制定具体的处理函数
  • 实现处理函数,该函数的传参必须与资源点的传参一样,并且最后加上BlockException异常参数;同时,返回类型也必须一样。

如果熟悉Hystrix的读者应该会发现,这样的设计与HystrixCommand中定义fallback很相似,还是很容易理解的。

@link: SentinelResource详细用法

熔断

在介绍熔断机制之前,我们需要了解微服务的雪崩效应。在微服务架构中,微服务是完成一个单一的业务功能,这样做的好处是可以做到解耦,每个微服务可以独立演进。但是,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。

这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。

熔断机制是应对雪崩效应的一种微服务链路保护机制。

  • @SentinelResource熔断
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

@Slf4j
@Service
public class TestService {
 
    // 熔断与降级处理
    @SentinelResource(value = "doSomeThing2", fallback = "fallbackHandler")
    public void doSomeThing2(String str) {
        log.info(str);
        throw new RuntimeException("发生异常");
    }
 
    public void fallbackHandler(String str) {
        log.error("fallbackHandler:" + str);
    }
}

在Sentinel中定义熔断的降级处理方法非常简单,与Hystrix非常相似。只需要使用@SentinelResource注解的fallback属性来指定具体的方法名即可。

熔段解决如下几个问题:

  • 当所依赖的对象不稳定时,能够起到快速失败的目的
  • 快速失败后,能够根据一定的算法动态试探所依赖对象是否恢复
降级

降级是指自己的待遇下降了,从RPC调用环节来讲,就是去访问一个本地的伪装者而不是真实的服务。

当双11活动时,把无关交易的服务统统降级,如查看蚂蚁深林,查看历史订单,商品历史评论,只显示最后100条等等。