高级开发面试题及答案整理

1、什么是面向对象

  • 面向对象是模型化的,只需抽象出一个类,把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象,创建了对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为

2、封装、继承、多态特性

  • 封装
    • 隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性
  • 继承
    • 继承就是父子类的关系,Java中只允许单继承,子类可以调用父类的非私有属性和方法,可以提高代码复用性
  • 多态
    • 父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性

3、向上转型与向下转型

  • 向上转型
    • 通俗地讲即是将子类对象转为父类对象。父类对象可以是接口
    • 向上转型不要强制转型。向上转型后父类的引用所指向的属性是父类的属性,如果子类重写了父类的方法,那么父类引用指向的或者调用的方法是子类的方法,这个叫动态绑定。
    • 向上转型后父类引用不能调用子类自己的方法,就是父类没有但是子类的方法
  • 向下转型
    • 与向上转型相反,即是把父类对象转为子类对象
    • 向下转型需要考虑安全性,如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现java.lang.ClassCastException错误。
  • 向上转型的作用,减少重复代码,父类为参数,调有时用子类作为参数,就是利用了向上转型。这样使代码变得简洁。体现了JAVA的抽象编程思想

4、Java基本类型有哪些

  • 各个类型占用字节
    • byte 1
    • short 2
    • int 4
    • long 8
    • float 4
    • double 8
    • char 2
    • boolean 本该是1位, 但存储空间基本单位是字节,所以至少1字节
  • 每个字节是多少位
    • 1字节等于8位

5、int和long的区别

  • 区别1

    • 16位系统:long是4字节,int是2字节

    • 32位系统:long是4字节,int是4字节

    • 64位系统:long是8字节,int是4字节

  • 区别2

    • long和int的区别就是他们的占位长度不同 其中long是64位、而int是32位

6、int类型如果超了会怎么样,为什么

  • 超过最大之后会变成int范围内最小的值
  • 超出之后二进制高位的首位变成1,代表负数

7、怎么实现线程安全的List

  • CopyOnWriteArrayList
    • CopyOnWrite 写入时复制,它是一个List同步的替代品,通常情况下提供了更好的并发性,并且避免了再迭代时候对容器的加锁和复制。通常更适合用于迭代,在多插入的情况下由于多次的复制性能会一定的下降
    • CopyOnWriteArrayList内部是使用ReentrantLock进行加锁解锁完成单线程访问
  • Collections.synchronizedList
    • 主要是利用了装饰者模式对传入的集合进行调用, Collections中有内部类SynchronizedRandomAccessList
    • 父类有个mutex就是锁的对象, 在构建时候可以也可以指定锁的对象, 主要使用synchronize代码块实现线程安全

8、volatile是怎么解决可见性问题的

  • 涉及到Java内存模型, 每个线程都有自己的工作内存, 这块内存是线程私有的, 其他线程看不到, volatile的作用就是让工作内存的数据刷到主内存, 并且失效其它线程缓存行(MESI), 从而让其他线程去主内存重新读取数据

9、synchronized能不能解决可见性问题

  • synchronized会保证对进入同一个监视器的线程保证可见性
  • 比如线程th1修改了变量,退出监视器之前,会把修改变量值v1刷新的主内存当中;当线程t2进入这个监视器时,如果有某个处理器缓存了变量v1,首先缓存失效,然后必须重主内存重新加载变量值v1(这点和volatile很像)
  • 这里语义的解读只是说了对于同一个监视器,变量的可见性有一定的方式可寻,非同一个监视器就不保证了。

10、ReentrantLock能不能解决可见性问题,怎么解决的,如果不能,又是因为什么

  • AQS中的state是volatile的,volatile为了保证可见性,会在机器指令中加入lock指令,lock强制把(工作内存)写回(主内存),并失效其它线程的缓存行(MESI),这里要注意的是,lock并不仅仅只把被volatile修饰的变量写回主内存, 而是把工作内存中的变量都写入主内存
  • volatile修饰的state保证了lock和unlock的Happens-Before的偏序关系,根据volatile规则:volatile变量的写入必须在读取之前执行,保证了可重入锁ReentrantLock的unlock方法必须在lock方法返回前执行。

11、什么是AQS

  • AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架, JUC包下的工具类都是基于AQS来实现的线程安全

12、ReenTrantLock与CountdownLatch使用AQS的区别

  • AQS定义两种资源共享方式:Exclusive(独占、只有一个线程执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore / CountDownLatch)
  • 独占式
    • 以RenentrantLock为例, 它有一个state共享变量初始值为0,表示未锁定状态。当线程A访问的时候,state+1,就代表该线程锁定了共享资源,其他线程将无法访问,而当线程A访问完共享资源以后,state-1,直到state等于0,就将释放对共享变量的锁定,其他线程将可以抢占式或者公平式争夺。当然,它支持可重入,那什么是可重入呢?同一线程可以重复锁定共享资源,每锁定一次state+1,也就是锁定多次。说明:锁定多少次就要释放多少次。
  • 共享式
    • 以CountDownLatch为例, 共享资源可以被N个线程访问,也就是初始化的时候,state就被指定为N(N与线程个数相等),线程countDown()一次,state会CAS减1,直到所有线程执行完(state=0),那些await()的线程将被唤醒去执行执行剩余动作。

13、CAS为什么能保证线程安全

  • CAS操作是一个原子操作,所谓原子操作就是指在执行期间不会被其他线程打断,要么执行完毕,要么不执行
  • CAS操作有三个操作数V(内存地址),A(旧的预期值),B(准备设置的新值)。指令执行时先看V指向的内存中存储的值是否和A相同,如果相同才会更新为B,否则什么也不做。
  • 自旋是指当对象或同步块已经被其他线程锁定时,竞争线程空转等待占用线程执行完毕的情形。注意此时竞争线程并没有阻塞,而是原地空转,执行一个无限循环判断对象是否已解锁,所以不存在用户态到核心态的转换,因而同步效率较高(但会占用CPU时间)
  • 在多个线程同时CAS的情况下是不会发生多个线程CAS成功的情况的,因为计算机底层实现保证了V指向内存的互斥性和立即可见性,可以理解为CAS操作是底层保证的线程安全
  • 一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效。

14、synchronized实现原理及锁升级过程

  • synchronized用的锁存在Java对象头里,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和 锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
  • 锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级。为了提高获得锁和释放锁的效率,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。
  • 偏向锁
    • 当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需测试Mark Word里线程ID是否为当前线程。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要判断偏向锁的标识。如果标识被设置为0(表示当前是无锁状态),则使用CAS竞争锁;如果标识设置成1(表示当前是偏向锁状态),则尝试使用CAS将对象头的偏向锁指向当前线程,触发偏向锁的撤销。偏向锁只有在竞争出现才会释放锁。当其他线程尝试竞争偏向锁时,程序到达全局安全点后(没有正在执行的代码),它会查看Java对象头中记录的线程是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
  • 轻量级锁
    • 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,即Displaced Mark Word。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,锁就会升级为重量级锁。
  • 重量级锁
    • 通常说的就是synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
    • 重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

15、BIO、NIO、AIO区别

  • BIO

    • 同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • NIO

    • 同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。可以监听来自多个客户端的IO事件
    • 多路复用器,可以监听来自多个客户端的IO事件:
      • 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
      • 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
      • 监听多个客户端的连接请求和接收数据请求同时还能监听自己时候有数据要发送。
  • AIO

    • AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

16、使用BIO时,线程都在阻塞,并不影响性能,那为什么不好

  • 如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力
  • 采用BIO通信模型的服务端,接收客户端的请求之后为每一个客户端创建一个新的线程进行处理,处理完成后,通过输出流返回,线程销毁. 最大的问题在于,当用户并发访问量增加后,服务端的线程个数与客户端的访问数成1:1正相关. 线程是JVM非常宝贵的资源,当线程膨胀后,系统性能急剧下降,随着并发访问量继续增大,系统会发生线程堆栈溢出,创建新线程失败的问题,并最终导致服务器"挂"掉

17、什么是Epoll

18、什么是零拷贝

19、String类是怎么实现的

  • 首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。
  • 虽然final代表了不可变,但仅仅是引用地址不可变, final也可以将数组本身改变的,这个时候,起作用的还有private,正是因为两者保证了String的不可变性。

20、为什么String类是final的

  • final修饰的第一个好处是安全;第二个好处是高效
  • 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  • 只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率

21、String、StringBuffer、StringBuilder区别

  • 可变与不可变

    • String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的。

      • private final char value[];
    • StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。

      • char[] value;
  • 是否多线程安全

    • String中的对象是不可变的,也就可以理解为常量,显然线程安全
    • StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
    • StringBuilder并没有对方法进行加同步锁,所以是非线程安全的
  • 执行效率

    • String:适用于少量的字符串操作的情况
      • Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
    • StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
    • StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
    • StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。

22、Netty线程模型

  • Reactor线程模型
  • 常用的 Reactor 线程模型有三种
    • Reactor 单线程模型
      • 指的是所有的 IO 操作都在同一个 NIO 线程上面完成,NIO 线程的职责如下:
      • 作为 NIO 服务端,接收客户端的 TCP 连接;
      • 作为 NIO 客户端,向服务端发起 TCP 连接;
      • 读取通信对端的请求或者应答消息;
      • 向通信对端发送消息请求或者应答消息。
    • Reactor 多线程模型
      • 有专门一个 NIO 线程-Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求;
      • 网络 IO 操作-读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送;
      • 1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,防止发生并发操作问题。
    • 主从 Reactor 多线程模型
      • 服务端用于接收客户端连接的不再是个 1 个单独的 NIO 线程,而是一个独立的 NIO
        线程池
      • Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责 SocketChannel 的读写和编解码工作
      • Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端 sub reactor 线程池的 IO线程上,由 IO 线程负责后续的 IO 操作

23、消息队列怎么保证高可用性的

  • RabbitMQ
    • 普通集群模式(无高可用性)
      • 普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。
      • 这种方式确实很麻烦,也不怎么好,没做到所谓的分布式,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈
      • 而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让 RabbitMQ 落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。
      • 这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。
    • 镜像集群模式(高可用性)
      • 跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
      • 那么如何开启这个镜像集群模式呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
      • 好处
        • 你任何一个机器宕机了,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据
      • 坏处
        • 性能开销太大,消息需要同步到所有机器上,导致网络带宽压力和消耗很重
        • 不是分布式的,就没有扩展性可言了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并没有办法线性扩展你的 queue。如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了
  • Kafka
    • Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。
    • Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。
    • Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。
    • 写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。
    • 这么搞,就有所谓的高可用性了,因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。
    • 写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)
    • 消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

24、如何保证消息不被重复消费(消息的幂等性)

  • Kafka消费端可能出现重复消费的问题
    • kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次服务器如果重启或崩溃恢复后,可以继续从上次消费到的 offset 来继续消费。
    • 但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次
  • 怎么保证幂等性
    • 如果出现消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样就只保留了一条数据,从而保证了数据的正确性。
    • 其实还是得结合业务来思考
      • 比如拿个数据要写库,先根据主键查一下,如果这数据都有了,就别插入了,update 一下
      • 比如写 Redis,那没问题了,反正每次都是 set,天然幂等性
      • 稍微复杂一点的情况, 你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可
      • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据

25、消息队列消息丢失的问题

  • 生产者发送消息丢失
    • 写消息过程中,消息在网络传输过程中丢失
    • 消息发到了消息队列,但消息队列内部出错,消息没保存下来
  • 消息队列内部问题导致丢失
    • 消息队列接收到消息后先暂存到内存,但消费者还没来得及消费,消息队列崩溃了,导致消息丢失
  • 消费者消费消息处理丢失
    • 消费者消费到了消息,但还没来得及处理,消费者崩溃了,但消息队列以为消费者已经处理完了,导致消息丢失
  • RabbitMQ
    • 生产者角度解决消息丢失
      • RabbitMQ有一个事务机制,如果生产者发送消息后出现异常,生产者可以捕捉到这个异常,根据业务决定如何处理,如果没有异常,可以手动进行事务提交
        • 缺点
          • 事务机制是同步的,生产者发送消息会同步阻塞,等待消息发送成功或失败的响应,会降低吞吐量
      • 将channel设置为confirm模式,需要事先提供一个回调接口,当RabbitMQ接收到消息之后,会回调生产者本地的接口,通知这条消息是接收成功还是接收失败。之后成功或失败的处理要根据业务来判断如何处理了。
        • 好处在于这种模式是异步的,不需要阻塞等待
    • RabbitMQ自身导致消息丢失的解决
      • 开启RabbitMQ的持久化机制。必须开启3个地方的持久化
        • 创建exchange交换机的时候将其设置为持久化,可以保证RabbitMQ可以持久化交换机的元数据
        • 创建queue的时候将其设置为持久化,这就可以保证RabbitMQ可以持久化queue的元数据,但不会持久化queue里的数据
        • 发送消息时将消息的 deliveryMode 设置为 2 ,就是将消息设置为持久化的,此时RabbitMQ就会将消息持久化到磁盘
      • 集群
        • 如果只有一个 RabbitMQ 的节点,即使交换机、队列、消息做了持久化,如果服务
          崩溃或者硬件发生故障,RabbitMQ 的服务一样是不可用的,所以为了提高 MQ 服务的
          可用性,保障消息的传输,需要有多个 RabbitMQ 的节点
    • 消费者消费消息失败导致消息丢失的解决
      • 这种情况再RabbitMQ中一般是消费者开启了 autoAck 机制,就是消费者接收到消息之后就会通知RabbitMQ已经消费到消息了。如果消费者还没处理完消息,服务挂掉了,就会导致这条消息丢失
      • RabbitMQ 提供了消费者的消息确认机制(message acknowledgement),消费
        者可以自动或者手动地发送 ACK 给服务端。
      • 如果没有收到 ACK 的消息,消费者断开连接后,RabbitMQ 会把这条消息发送给其他消
        费者。如果没有其他消费者,消费者重启后会重新消费这条消息,重复执行业务逻辑
  • Kafka
    • 生产者角度解决消息丢失
      • 两个参数配置有关,可以根据业务情况进行跳转
      • 在生产者端设置 acks=all 参数,这是要求每条数据必须写入所有 replica之后,才能认为是写成功了
      • 在生产者端设置 retries=3 ,可以设置的高一点,表示重试次数的意思。这里是要求一旦写入失败,就进行重试,重新发送消息
    • Kafka自身问题导致消息丢失的解决
      • 出现丢失的情况是生产者像Kafka发送消息后,leader副本的partition还没来得及同步消息到其他follower副本,broker宕机了,然后重新选举partition的leader,就导致了数据的丢失
      • 需要设置4个参数
        • 创建 Topic 时设置 replication.factor 参数,这个值必须大于 1,要求每个 partition必须有至少 2 个副本
        • 在服务端设置 min.insync.replicas 参数,这个值必须大于1,这是要求一个leader至少感知到有一个follower还跟自己保持通信,这样才能确保leader挂了之后还有一个follower存活
        • 在生产者端设置 acks=all 参数,这是要求每条数据必须写入所有 replica之后,才能认为是写成功了
        • 在生产者端设置 retries=3 ,可以设置的高一点,表示重试次数的意思。这里是要求一旦写入失败,就进行重试,重新发送消息
      • 以上参数配置之后,至少可以保证leader所在broker发生故障后,leader切换不会导致数据丢失
    • 消费者消费消息失败导致消息丢失的解决
      • 这种情况的解决方案各个消息中间件解决方法都差不多,Kafka消费端需要关闭自动提交 Offset 的功能,改成手动提交 Offset

26、如何保证消息的顺序性

  • 消息的顺序性指的是消费者消费消息的顺序跟生产者生产消息的顺序是一致的
  • RabbitMQ
    • 出现顺序错乱的情况
      • 比如一个 queue,多个 consumer。生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。
      • 有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这就造成了顺序错乱的问题
    • 解决方案
      • 在 RabbitMQ 中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是
        不一样的,顺序无法保证。
      • 只有一个队列仅有一个消费者的情况才能保证顺序消费(不同的业务消息发送到不同的专用的队列)
      • 除非负载的场景,否则不要用多个消费者消费消息
  • Kafka
    • 出现顺序错乱的情况
      • 比如说我们建了一个 topic,有三个 partition。生产者在写的时候,可以指定一个 key,比如指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。消费者从 partition 中取出来数据的时候,也一定是有顺序的
      • 到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞 多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。
      • 多个线程并发跑的话,顺序可能就乱掉了。
    • 解决方案
      • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

27、如果一个线程获取Redis分布式锁之后执行很慢,锁超时后导致多个线程获得锁,这要怎么解决

  • 加锁的同时启动一个守护线程,当任务没执行完而锁快要释放时,守护线程执行Lua脚本为当前线程持有的锁延时,如果延时成功,那么直到业务逻辑执行完毕,, 其他线程无法获取到锁

28、Lua为什么能保证原子性

  • 因为Redis是单线程的,所有操作都是串行执行,Lua脚本是一个不可分割的原子命令,所以Redis在执行Lua脚本时必须全部执行完才会执行其他命令

29、Redis主从同步的原理

  • 全量同步
    • Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份
    • master服务器会开启一个后台进程用于将redis中的数据生成一个rdb文件,与此同时,服务器会缓存所有接收到的来自客户端的写命令(包含增、删、改),当后台保存进程
      处理完毕后,会将该rdb文件传递给slave服务器,而slave服务器会将rdb文件保存在磁盘并通过读取该文件将数据加载到内存,在此之后master服务器会将在此期间缓存的
      命令通过redis传输协议发送给slave服务器,然后slave服务器将这些命令依次作用于自己本地的数据集上最终达到数据的一致性。
  • 增量同步
    • Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程
    • 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令
    • 从redis 2.8版本以前,并不支持部分同步,当主从服务器之间的连接断掉之后,master服务器和slave服务器之间都是进行全量数据同步,但是从redis 2.8开始,即使主从连接中途断掉,也不需要进行全量同步,因为从这个版本开始融入了部分同步的概念。
    • 部分同步的实现依赖于在master服务器内存中给每个slave服务器维护了一份同步日志和同步标识,每个slave服务器在跟master服务器进行同步时都会携带自己的同步标识和上次同步的最后位置。
    • 当主从连接断掉之后,slave服务器隔断时间(默认1s)主动尝试和master服务器进行连接,如果从服务器携带的偏移量标识还在master服务器上的同步备份日志中,那么就从slave发送的偏移量开始继续上次的同步操作,如果slave发送的偏移量已经不再master的同步备份日志中(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内master服务器接收到大量的写操作),则必须进行一次全量更新。
    • 在部分同步过程中,master会将本地记录的同步备份日志中记录的指令依次发送给slave服务器从而达到数据一致。
  • 要注意的问题
    • 在上面的全量同步过程中,master会将数据保存在rdb文件中然后发送给slave服务器,但是如果master上的磁盘空间有效怎么办呢?那么此时全部同步对于master来说将是一份十分有压力的操作了。
    • 此时可以通过无盘复制来达到目的,由master直接开启一个socket将rdb文件发送给slave服务器。(无盘复制一般应用在磁盘空间有限但是网络状态良好的情况下

30、在使用Docker过程中遇到那些问题

  • 问题描述
    • 因为使用的是Docker Swarm部署服务,在采用Replica模式进行复制部署策略时,另一台服务器的容器一直部署失败
  • 问题排查
    • 通过在网上查询资料发现有类似问题,因为当时服务器是用的阿里云的ECS,且有一台服务器购买时间相对较早,两台服务器的Linux内核版本不一致,内核不一致产生的问题不记得是什么了
  • 解决问题
    • 升级Linux内核,使两台服务器的内核版本一致,重新部署测试,问题解决

31、K8S中要限制一个服务的CPU和内存大小,这个值要怎么判断限制多少

你可能感兴趣的:(高级开发面试题及答案整理)