2019面试题总结

in with 1 comment

消息队列

  1. 在项目中如何用到的MQ

    • 解耦

      订单系统将消息写入消息队列,后台以及豆丁网等系统从消息队列中订阅,从而订单系统无需任何修改

    • 异步

      订单生成前调用环球网校接口的一些校验非必需逻辑,所以以异步方式运行,加快相应速度

    • 削峰

      并发量大时,将写请求放进消息队列,其他系统按照数据库能处理的并发量消费队列中的数据

  2. 为什么要用MQ

    解耦、异步、削峰

  3. MQ的技术选型

    这里只对比RabbitMQ和Kafka

    特性RabbitMQKafka
    开发语言erlangscala
    单机吞吐量万级十万级
    时效性微秒级毫秒级
    可用性主从架构分布式架构
    功能特性并发能力很强,性能极好,延时很低,有管理界面。erlang开发,自主研发比较困难仅支持主要的MQ功能,无消息查询、消息回溯等功能,应用于大数据领域
  4. MQ的优点和缺点

    优点:

    • 解耦

    • 异步

    • 削峰

    缺点:

    • 系统可用性降低

      如何保证MQ高可用

    • 系统复杂性增加

      1. 重复消费
      2. 消息丢失
      3. 顺序性
    • 一致性问题

  5. 如何保证消息队列高可用

    这里只针对RabbitMQ和Kafka进行说明。

    RabbitMQ: RabbitMQ基于主从架构实现高可用,其有两种模式:普通集群模式、镜像集群模式

    普通集群模式: 在集群环境下部署多个MQ,但创建的消息队列(queue)会集中置于一个MQ实例中,其他每个实例同步该消息队列的元数据(可以理解为配置文件,但不是消息本身)。当消费者需要拉取消息时,如果并没有连接到消息队列所在实例,那么该实例会到消息队列所在实例上拉取消息。

    • 优点:节省硬盘资源降低网络带宽压力,提高吞吐量

    • 缺点:

      1. 可能会在MQ集群内部产生大量数据传输,系统开销较大

      2. 可用性较低。一旦queue所在实例挂掉,消息就会丢失,消费者无法正常消费

    镜像集群模式: 在这种模式下,创建的消息队列无论元数据还是消息队列中的消息都会存在于多个实例上,然后每次当生产者发送消息给MQ实例时,都会自动把消息发送到多个实例的消息队列中进行消息同步。

    • 优点:高可用。

    • 缺点:

      1. 由于RabbitMQ不支持分布式集群,消息队列同步到所有MQ实例会增大网络带宽压力,系统性能开销也相应提高

      2. 扩展性不好。无法线性扩展消息队列,一旦在集群中增加机器,就要同步所有消息队列中的数据。

    Kafka: Kafka支持分布式,在0.8前没有高可用机制,0.8后提供了 replica 副本机制,很好地保证了消息队列的高可用性。

    HA机制-(replica副本机制) 每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 被称为 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,保证了高可用。

  6. 消息队列如何避免重复消费(保证幂等性)

    • 如果这条数据需要写库,先根据主键查一下,如果有了,那就update

    • 如果写入redis,redis的set天然幂等

    • 生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。

      消息队列中重复消费数据需要结合具体业务处理。

  7. 如何保证消息的可靠性传输(如何处理消息丢失的问题)

    rabbitMQ:

    • 生产者:

      1. 使用事务机制,如果消息发送失败,会抛出异常,这里可以再次重试发送这条消息。缺点,这是同步的,生产者发送消息会阻塞,等待这条消息发送成功或失败,导致生产者发送消息吞吐量降低。

      2. 使用channel的confirm模式,发送消息后生产者会回调本地接口,通知该消息是否发送成功,如果失败可以重新发送。

    • rabbitMQ:持久化到磁盘

    • 消费者:关闭autoAck,处理完逻辑后再发送ack给mq

    Kafka:

    • 生产者:如果设置了ack=all,一定不会丢,当leader接收到消息,所有的follower都同步到了消息之后,才认为本次写成功了。

    • Kafka:

      1. 在topic设置replication.factor参数:这个值必须大于1,要求每个partition必须有至少2个副本

      2. 在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower

      3. 在producer端设置acks=all:这个是要求每条数据,必须是写入所有replica之后,才能认为是写成功了

      4. 在producer端设置retries=MAX(无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了

    • 消费者:关闭自动提交offset,处理完逻辑后手动提交offset,同时消费者本身处理逻辑需要保证幂等性。

  8. 如何保证消息的顺序性

    rabbitMQ:

    • 一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

    Kafka:

    • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
  9. 如何解决消息队列的延时及过期失效问题,消息队列满了以后怎么处理,有几百万消息持续挤压几小时,怎么解决

  10. 如何设计消息队列架构

搜索引擎-elasticsearch

  1. lucene是什么?倒排索引是什么?全文索引是什么?

  2. es的分布式架构原理和设计

  3. es写入数据的工作原理是什么?es查数据的工作原理是什么?

  4. es在数据量很大的情况下,如何提高查询性能

  5. es生产集群的部署架构是什么,每个索引的数据量有多少,每个索引大概有多少分片

缓存

  1. redis和memcached的区别

  2. redis的单线程模型

  3. redis单线程模型效率为什么高

  4. redis有哪些数据类型,和其所使用的场景

  5. redis的过期策略和内存淘汰机制

  6. 如何保证redis的高并发和高可用

  7. redis的主从复制原理

  8. redis的哨兵原理

  9. redis的持久化有哪几种方式?其优缺点,持久化机制具体底层?

  10. redis集群模式的工作原理?在集群模式下,redis的key是如何寻址的?分布式寻址都有哪些算法?了解一致性hash算法吗?

  11. redis的雪崩和穿透是什么以及其现象和解决办法

  12. 如何保证缓存与数据库双写一致性

  13. redis并发竞争问题以及解决方案

  14. 生产环境中redis是怎么部署的

分布式系统

  1. dubbo和thrift有什么区别?

  2. dubbo的工作原理?注册中心挂了可以继续通行吗?

  3. dubbo支持哪些序列化协议?hessian的数据结构?PB知道吗?为什么PB的效率是最高的

  4. dubbo的spi思想是什么?

  5. 如何基于dubbo进行服务治理、服务降级、失败重试以及超时重试

  6. 分布式服务接口的幂等性如何设计

  7. 分布式服务接口请求的顺序性如何保证

  8. 分布式锁,redis和zookeeper的区别与优缺点

  9. 如何解决分布式事务,TCC如果出现网络连不通怎么办,XA的一致性如何保证

  10. 集群部署时的分布式session如何实现

数据库

  1. 为什么要分库分表?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?具体是如何对数据库进行垂直拆分或水平拆分的

  2. 现在有一未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

  3. 如何设计可以动态扩容缩容的分库分表方案?

  4. 分库分表之后,id主键如何处理?

  5. 如何实现mysql的读写分离?mysql主从复制原理是什么?mysql主从同步的延时问题

高可用系统(hystrix)

  1. 如何限流?

  2. 如何进行熔断?熔断框架有哪些?具体实现原理是什么

  3. 如何进行降级?

JVM

  1. Java内存区域

    1. 介绍下Java内存区域(运行时数据区)

      • jdk1.8之前:jdk1.8之前

      • jdk1.8及之后:jdk1.8及之后

      线程私有的:

      • 程序计数器
      • 虚拟机栈
      • 本地方法栈

      线程共享的:

      • 方法区
      • 直接内存(非运行时数据区的一部分)
    2. Java对象的创建过程(五步)

      1. 类加载检查

        虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

      2. 分配内存

        在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

      3. 初始化零值

        内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

      4. 设置对象头

        初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

      5. 执行init方法

        在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

    3. 对象的访问定位的两种方式(句柄和直接指针两种方式)

      • 句柄 : 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

      • 直接指针 : 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

      • 比较 :这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

  2. JVM垃圾回收

    1. 如何判断对象是否死亡(两种方法)

      • 引用计数法 : 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
      • 可达性分析算法: 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
    2. 简单介绍一下强引用、软引用、弱引用、虚引用

      • 强引用:垃圾回收器绝不会回收;

      • 软引用:如果内存空间足够,垃圾回收器不会回收,反之回收;

      • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;

      • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

    3. 如何判断一个类是无用的类

      • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;

      • 加载该类的 ClassLoader 已经被回收;

      • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    4. 垃圾收集有哪些算法、各自的特点

      1. 标记-清除算法 :首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

      2. 复制算法 :它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

      3. 标记-整理算法 :根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

      4. 分代收集算法 :当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    5. 常见的垃圾回收器有哪些

      1. Serial 收集器 :Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器。这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

      2. ParNew 收集器 :ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

      3. Parallel Scavenge 收集器 :Parallel Scavenge 收集器是使用复制算法的多线程收集器。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

      4. Serial Old 收集器 :Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

      5. Parallel Old 收集器 :Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

      6. CMS 收集器 :CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

        • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快;

        • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方;

        • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短;

        • 并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

      7. G1 收集器 :G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

        • 并行与并发: G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。

        • 分代收集: 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。

        • 空间整合: 与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

        • 可预测的停顿: 这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

      G1 收集器的运作大致分为以下几个步骤:

      • 初始标记

      • 并发标记

      • 最终标记

      • 筛选回收

    6. Minor GC和Full GC有什么不同

    • 新生代 GC(Minor GC) :指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

    • 老年代 GC(Major GC/Full GC) :指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。