秋招结束,回馈牛客

双非菜鸡的秋招结束~
在牛客看了好多,发个帖子总结一下
全是个人总结,如有错误敬请谅解😁😁
如有错误,虚心接受各位大佬赐教更改😁😁
——————————————————————————————————————

大数据部分1

1、Flume

  • flume是一个分布式、可靠和高可用的海量日志采集、聚合和传输的系统

  • flume的管道是基于事务的,保证了数据在传送和接收时的一致性

  • 一个flume进程包括:Source(数据源) Channel(处理介质) Sink(目的地)

  • flume的管道(Channel自带两种)可以使用内存(Memory Channel),也可以使用文件方式(File Channel)来防止宕机

  • flume的最主要作用是可以实时读取服务器的日志数据

2、Spark

  • spark是什么:Spark是基于内存的迭代式计算引擎

  • Spark的运行原理以及什么时候发生宽窄依赖:

    • 运行原理:(客户端)Spark通过SparkContex向集群的Master提交执行作业的请求,Maste节点的资源管理器申请执行所需的CPU,内存等,然后Master节点在worker启动Executorj进程,然后向客户端反向注册,客户端会根据DAG有向无环图来划分Stage,为每一个stage创建一个task,并且封装至TaskSchedual中,然后讲task交给Executor去执行,在action算子执行之后将结果收集到客户端。

    • 宽窄依赖:RDD转换的时候发生宽窄依赖

    • Spark中join算子可能会发生宽依赖也可能不会

  • Spark的执行模式:

    • Local 本地执行,通过多线程实现并行计算

    • Standalone 是 Spark 提供的一种内置的集群模式,采用内置的资源管理器进行管理

    • Spark on Yarn模式:Spark 支持将作业提交到 Yarn 上运行,此时不需要启动 Master 节点,也不需要启动 Worker 节点

      • 需要配置在 spark-env.sh

      • YARN_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop # JDK安装位置 JAVA_HOME=/usr/java/jdk1.8.0_201

        启动时必须保证Hadoop已经启动了

  • RDD的属性: RDD叫做分布式数据集,是spark中最基本的数据抽象,它代表一个不可变、可分区、里边的元素可并行计算的集合,RDD的属性: 一组分区|分片、应用在分区上的函数、分区间的依赖关系、分区函数(对key-value的RDD才有)、存放每个分区的位置列表

  • RDD的两种操作(算子): 转换算子(Transformation) 动作算子(Action)

  • DStream和DStreamGraph的区别:在创建StreamingContext的时候会创建DStreamGraph

    • Spark Streaming里的DStream可以看成是Spark Core里的RDD的模板,DStreamGraph是RDD DAG的模板(是基于其方法中的inputStream和outputStream实现的)

  • Spark重新分区:

    • repartition(numPartitions:Int):RDD[T]

    • coalesce(numPartitions:Int,shuffle:Boolean=false):RDD[T]

    • 它们两个都是RDD的分区进行重新划分,repartition只是coalesce接口中shuffle为true的简易实现,(假设RDD有N个分区,需要重新划分成M个分区

      • N<M,一般情况下N个分区有数据分布不均匀的状况,利用HashPartitioner函数将数据重新分区为M个,这时需要将shuffle设置为true

      • 如果N>M并且N和M相差不多,(假如N是1000,M是100)那么就可以将N个分区中的若干个分区合并成一个新的分区,最终合并为M个分区,这时可以将shuff设置为false,在shuffl为false的情况下,如果M>N时,coalesce为无效的

3、Kafka

  • kafka的(控制器Broker)选举机制:Kakfa Broker Leader的选举:Kakfa Broker集群受Zookeeper管理。所有的Kafka Broker节点一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower。(这个过程叫Controller在ZooKeeper注册Watch)。

  • kafka的分区副本选举机制:Kafka在Zookeeper上针对每个Topic都维护了一个ISR(in-sync replica---已同步的副本)的集合,集合的增减Kafka都会更新该记录。如果某分区的Leader不可用,Kafka就从ISR集合中选择一个副本作为新的Leader。

  • 在kafka的集群中,会存在着多个主题topic,在每一个topic中,又被划分为多个partition,为了防止数据不丢失,每一个partition又有多个副本,在整个集群中,总共有三种副本角色:

    首领副本(leader):也就是leader主副本,每个分区都有一个首领副本,为了保证数据一致性,所有的生产者与消费者的请求都会经过该副本来处理。 跟随者副本(follower):除了首领副本外的其他所有副本都是跟随者副本,跟随者副本不处理来自客户端的任何请求,只负责从首领副本同步数据,保证与首领保持一致。如果首领副本发生崩溃,就会从这其中选举出一个leader。 首选首领副本:创建分区时指定的首选首领。如果不指定,则为分区的第一个副本。

  • kafka消费者组选主:在kafka的消费端,会有一个消费者协调器以及消费组,组协调器GroupCoordinator需要为消费组内的消费者选举出一个消费组的leader,如果消费组内还没有leader,那么第一个加入消费组的消费者即为消费组的leader,如果某一个时刻leader消费者由于某些原因退出了消费组,那么就会重新选举leader。

  • kafka传输的三种事务定义:(1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输

    (2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.

    (3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

  • kafka的幂等性:为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。

    • PID:每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。

  • Sequence Numbler:(对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number

    • Kafka可能存在多个生产者,会同时产生消息,但对Kafka来说,只需要保证每个生产者内部的消息幂等就可以了,所有引入了PID来标识不同的生产者。

  • 对于Kafka来说,要解决的是生产者发送消息的幂等问题。也即需要区分每条消息是否重复。 Kafka通过为每条消息增加一个Sequence Numbler,通过Sequence Numbler来区分每条消息。每条消息对应一个分区,不同的分区产生的消息不可能重复。所有Sequence Numbler对应每个分区

  • Broker端在缓存中保存了这seq number,对于接收的每条消息,如果其序号比Broker缓存中序号大于1则接受它,否则将其丢弃

4、kafka如何保证生产者不丢失数据,消费者不丢失数据

  • 生产者:

    • kafka的ack机制:在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到,其中状态有0,1,-1

    • 如果是0 ,那么代表发送过去,不等待kafka消息确认,认为成功,一定会丢失消息,可能kafka集群正在选举,此时就无法收到任何异常

    • 如果是1,那么代表发送过去,等待首领副本确认消息,认为成功首领肯定收到了消息,写入了分区文件(不一定落盘)

    • 如果是all, 那么代表发送过去之后,消息被写入所有同步副本之后 ,认为成功

    • 注意这里是 所有同步副本,不是所有副本。 具体是多少同步副本,还要取决于kafka集群设置的最小同步副本数,和集群当前的同步副本数

    • 选择这种配置,会可靠,但是牺牲效率,可以通过,增大批和使用异步模式,提高效率

    • 异步模式下的有个buffer,通过buffer来进行控制数据的发送,有两个值来进行控制,时间阈值与消息的数量阈值,如果buffer满了数据还没有发送出去,有个选项是配置是否立即清空buffer。可以设置为-1,永久阻塞,也就数据不再生产

  • 消费者:

    • 通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,会接着上次的offset进行消费

  • Broker端:

    • 设置unclean.leader.election.enable为false,禁止非ISR列表中的副本参与分区首领副本选举

5、Zookeeper的消息的发布订阅功能

  • 数据的发布/订阅系统,通常也用作配置中心。在分布式系统中,你可能有成千上万个服务节点,如果想要对所有服务的某项配置进行更改,由于数据节点过多,你不可逐台进行修改,而应该在设计时采用统一的配置中心。之后发布者只需要将新的配置发送到配置中心,所有服务节点即可自动下载并进行更新,从而实现配置的集中管理和动态更新

  • Zookeeper 通过 Watcher 机制可以实现数据的发布和订阅。分布式系统的所有的服务节点可以对某个 ZNode 注册监听,之后只需要将新的配置写入该 ZNode,所有服务节点都会收到该事件

6、Zookeeper的功能

  • maste选举:

    • 分布式系统一个重要的模式就是主从模式 (Master/Salves),Zookeeper 可以用于该模式下的 Matser 选举。可以让所有服务节点去竞争性地创建同一个 ZNode,由于 Zookeeper 不能有路径相同的 ZNode,必然只有一个服务节点能够创建成功,这样该服务节点就可以成为 Master 节点

  • 分布式锁:

    • 可以通过 Zookeeper 的临时节点和 Watcher 机制来实现分布式锁,这里以排它锁为例进行说明:

      • 分布式系统的所有服务节点可以竞争性地去创建同一个临时 ZNode,由于 Zookeeper 不能有路径相同的 ZNode,必然只有一个服务节点能够创建成功,此时可以认为该节点获得了锁。其他没有获得锁的服务节点通过在该 ZNode 上注册监听,从而当锁释放时再去竞争获得锁。锁的释放情况有以下两种:

        • 当正常执行完业务逻辑后,客户端主动将临时 ZNode 删除,此时锁被释放;

        • 当获得锁的客户端发生宕机时,临时 ZNode 会被自动删除,此时认为锁已经释放。

        当锁被释放后,其他服务节点则再次去竞争性地进行创建,但每次都只有一个服务节点能够获取到锁,这就是排他锁。

  • 集群管理:

    • 如可以通过创建临时节点来建立心跳检测机制。如果分布式系统的某个服务节点宕机了,则其持有的会话会超时,此时该临时节点会被删除,相应的监听事件就会被触发

    • 分布式系统的每个服务节点还可以将自己的节点状态写入临时节点,从而完成状态报告或节点工作进度汇报

    • 通过数据的订阅和发布功能,Zookeeper 还能对分布式系统进行模块的解耦和任务的调度

    • 通过监听机制,还能对分布式系统的服务节点进行动态上下线,从而实现服务的动态扩容

  • zookeeper特性:

    • 顺序一致性:从一个客户端发起的事务请求,最终都会严格按照其发起顺序被应用到 Zookeeper 中

    • 原子性:所有事务请求的处理结果在整个集群中所有机器上都是一致的;不存在部分机器应用了该事务,而另一部分没有应用的情况

    • 单一视图:所有客户端看到的服务端数据模型都是一致的

    • 可靠性:一旦服务端成功应用了一个事务,则其引起的改变会一直保留,直到被另外一个事务所更改

    • 实时性:一旦一个事务被成功应用后,Zookeeper 可以保证客户端立即可以读取到这个事务变更后的最新状态的数据

7、Spark中的shuffle机制

  • 概念:Shuffle就是对数据进行重组,由于分布式计算的特性和要求,在实现细节上更加繁琐和复杂

  • Shuffle Write:上一个stage的每一个map task就必须保证将自己处理的当前分区中的数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中

  • Shuffle Read:reduce task就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚在同一个节点上去处理和聚合

8、Spark与hadoop的区别

  • hadoop是一个生态圈,包含了hdfs(hadoop分布式文件系统),yarn(资源调度平台),mapreduce(基于yarn的离线计算框架),而Spark是基于内存的大数据并行计算框架,对标于MR

  • Spark并没有提供文件管理系统,它必须和其他的分布式文件系统进行集成才可以运作

  • Spark处理数据的设计模式与MR不同,MR在处理数据时涉及到多次磁盘IO,而Spark的设计模式是读取集群中的数据后在内存中进行存储计算,直到运算完毕后再存储至集群中

  • 与Hadoop的MapReduce相比,Spark基于内存的运算要快100倍以上,基于硬盘的运算也要快10倍以上

  • Spark通过RDD实现了对迭代计算

  • Spark有scala语言编写,而Hadoop由java语言编写

9、Hadoop里的namenode和datanode

  • namenode:

    • 是整个文件系统的管理节点,它维护着整个文件系统的文件目录树,文件/目录的元信息和每个文件对应的数据块列表

    • 接收用户的操作请求,同时在namenode上保存着:fsimage: 元数据镜像文件。存储某一时段NameNode内存储的元数据信息(序列化生成镜像文件)edits:操作日志文件 fstime:保存最近一次checkpoint的时间

  • datanode:提供真实的文件数据存储服务(再datanode中存储block数据块)

    • 数据块:hdfs中的基本存储单元 不同于普通文件系统的是,HDFS中,如果一个文件小于一个数据块的大小,并不占用整个数据块存储空间 数据块通过设置副本机制,默认是3个副本

10、spark的lazy体现在哪里

  • Spark中,Transformation方法都是懒操作方法,比如map,flatMap,reduceByKey等。当触发某个Action操作时才真正执行

  • Spark通过lazy特性,可以进行底层的spark应用执行的优化。在生活中,就像三思而后行。谋定而后动

  • 懒加载的意义:

    • 不运行job就触发计算,避免了大量的无意义的计算,即避免了大量的无意义的中间结果的产生,即避免产生无意义的磁盘I/O及网络传输

    • 更深层次的意义在于,执行运算时,看到之前的计算操作越多,执行优化的可能性就越高

11、Spark中数据倾斜

  • 数据倾斜:在任务执行期间,RDD会被分为一系列的分区,每个分区都是整个数据集的子集。当spark调度并运行任务的时候,Spark会为每一个分区中的数据创建一个任务。大部分的任务处理的数据量差不多,但是有少部分的任务处理的数据量很大,因而Spark作业会看起来运行的十分的慢,从而产生数据倾斜(进行shuffle的时候)

  • 数据倾斜只出现在shuffle过程中,可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等

  • 解决办法:

    • 过滤少量导致数据倾斜的key

    • 对key添加随机前缀,在执行操作后再去掉前缀

    • 自定义Partitioner

12、Yarn是什么

  • yarn是Hadoop中的资源调度平台

  • yarn中的角色:

    • ResourceManager:负责接收任务,管理集群中的资源和调度,以及监控运行再yarn上的程序

    • NodeManager:真正执行计算的节点

    • ApplicationMaster:一个job对应一个application master,创建task,启动yarnchild进程,并且分配task给yarnchild去执行

    • YarnChild:真正执行task的进程

13、hdfs读写流程

  • hdfs写:

    • 首先客户端给NameNode发送上传文件的请求(包含文件名,文件大小)

    • NameNode的给客户端响应,如果文件已经存在则报错,不存在则继续执行

    • 客户端将要上传的文件划分为block块并且请求上传第一个block(128mb)

    • NameNode以Pipeline的模式返回可上传文件的DataNode的列表

    • 客户端连接DataNode进行block块的上传

  • hdfs读:

    • 客户端将要读取的文件路径发送给namenode,namenode获取文件的元信息(主要是block的存放位置信息)返回给客户端,客户端根据返回的信息找到相应datanode逐个获取文件的block并在客户端本地进行数据追加合并从而获得整个文件

14、Hadoop的特性

  • 高可靠性:采用冗余数据存储方式,即使一个副本发生故障,其他副本也可以保证对外提供服务

  • 高效性:采用分布式存储和分布式处理两大核心你技术,能够高效的处理pb级数据

  • 高可扩展性:Hadoop的设计目标是可以高效稳定的运行在廉价的计算机群上,可以扩展到数以千计的计算机节点上

  • 高容错性:采用冗余数据存储方式,自动保存数据的多个副本,并且能够自动将失败的任务进行重新分配

  • 成本低:Hadoop采用廉价的计算机群,成本较低

  • 运行在Linux平台上:Hadoop是基于java语言开发的,可以很好的运行在Linux平台上

  • 支持多种编程语言:Hadoop的应用程序可以使用多种语言编写

15、Hadoop如何恢复被删除的文件

  • hadoop的hdfs中被删除文件的恢复原理和回收站原理是一样的,就是在删除hdfs文件时,被删除的文件被移动到了hdfs的.Trash文件夹中,恢复时只需将该文件夹中文件拿出即可。具体操作如下:

  • 设置.Trash文件夹

    如果需要恢复hdfs中文件,就需要设置.Trash,hadoop的.Trash默认是关闭的。具体设置如下   <property>  <name>fs.trash.interval</name>  <value>10080</value>  </property>

    该配置项在core-site.xml中,fs.trash.interval代表删除的文件保留的时间,时间单位为分钟,默认为0代表不保存删除的文件。我们只需要设置该时间即可打开.Trash

  • 设置后删除文件会显示删除的文件被移动到了hdfs://192.168.1.100:9000/user/hadoop/.Trash/Current中

  • 恢复时只需要将.Trash中文件移动到我们设置的目录即可

16、Spark的stage划分

  • Spark任务会根据RDD之间的依赖关系,形成一个DAG有向无环图,DAG会提交给DAGScheduler,DAGScheduler会把DAG划分相互依赖的多个stage,划分stage的依据就是RDD之间的宽窄依赖。遇到宽依赖就划分stage,每个stage包含一个或多个task任务。然后将这些task以taskSet的形式提交给TaskScheduler运行stage是由一组并行的task组成。

  • stage切割规则:从后往前,遇到宽依赖就切割stage

17、Spark为什么快

  • 消除了冗余的HDFS读写:Hadoop每次shuffle操作后,必须写到磁盘,而Spark在shuffle后不一定落盘,可以cache到内存中,以便迭代时使用。如果操作复杂,很多的shufle操作,那么Hadoop的读写IO时间会大大增加

  • 消除了冗余的MapReduce阶段:Hadoop的shuffle操作一定连着完整的MapReduce操作,冗余繁琐。而Spark基于RDD提供了丰富的算子操作,且reduce操作产生shuffle数据,可以缓存在内存中

  • JVM 的优化: Hadoop 每次 MapReduce 操作,启动一个 Task 便会启动一次 JVM,基于进程的操作。而 Spark 每次 MapReduce 操作是基于线程的,只在启动 Executor 时启动一次 JVM,内存的 Task 操作是在线程复用的。每次启动 JVM 的时间可能就需要几秒甚至十几秒,那么当 Task 多了,这个时间 Hadoop 不知道比 Spark 慢了多少

18、Spark有关资源分配的参数

  • 分配的资源:

    • 分配的 executor 数量

    • 每个 executor 需要的 core 数量

    • 每个 executor 需要的内存大小

    • driver 的内存大小 (这个影响不大)

    • 在spark提交作业的时候可以对这些资源进行配置

  • Spark在集群模式下的运行:

    • 构建Spark Application(就是提交的Spark运行程序)的运行环境(启动SparkContext),SparkContext向资源管理器(可以是Standalone、Mesos或YARN)注册并申请运行Executor资源

    • 资源管理器分配Executor资源,并启动监听程序,Executor运行情况将随着心跳发送到资源管理器上

    • SparkContext构建成DAG图,将DAG图分解成Stage,并把Task发送给Task Scheduler,Executor向SparkContext申请Task

    • Task Scheduler将Task发放给Executor运行同时SparkContext将应用程序代码发放给Executor

    • Task在Executor上运行,运行完毕释放所有资源

  • 资源分配的两种方式:

    • 静态分配:在提交任务的时候设置

    • 动态分配:根据数据的大小、需要的运算能力设置,便于以后类似任务的重用

19、Kafka为什么具有高吞吐量低延迟的特点

  • 传统的读取文件数据并发送到网络:

    • 操作系统将数据从磁盘文件中读取到内核空间的页面缓存

    • 应用程序将数据从内核空间读入用户空间缓冲区

    • 应用程序将读到数据写回内核空间并放入socket缓冲区

    • 操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送

    • “零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作

  • 实际上不管是内存还是磁盘,快或慢关键在于寻址的方式,磁盘分为顺序读写与随机读写,内存也一样分为顺序读写与随机读写

  • kafka是将消息记录持久化到本地磁盘中的,而kafka基于磁盘的持久化是基于硬盘的顺序读写的,而基于磁盘的顺序存储的速度甚至超过基于内存的随机读写,kafka的message是不断追加到本地磁盘文件末尾的

  • 为了优化读写性能,Kafka利用了操作系统本身的Page Cache,就是利用操作系统自身的内存而不是JVM空间内存,通过操作系统的Page Cache,Kafka的读写操作基本上是基于内存的,读写速度得到了极大的提升

  • 零拷贝:

    • linux操作系统 “零拷贝” 机制使用了sendfile方法, 允许操作系统将数据从Page Cache 直接发送到网络,只需要最后一步的copy操作将数据复制到 NIC 缓冲区, 这样避免重新复制数据

    • 通过这种 “零拷贝” 的机制,Page Cache 结合 sendfile 方法,Kafka消费端的性能也大幅提升

    • 零拷贝主要是避免了在内核空间和用户空间之间的拷贝

  • 批量读写:kafka在应用程序层面提供的技术,在写入数据时可以启用批次写入

  • 分区分段+索引:

    • Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的。这也非常符合分布式系统分区分桶的设计思想

    • 通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度

  • 批量压缩:

    • Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出

20、Hbase中Region分裂

  • 当MemStore的数据超过阈值时,将数据溢写磁盘,生成一个StoreFile文件。当Region中最大Store的大小超过阈值时,Region分裂,等分成两个Region,实现数据访问的负载均衡。新的Region的位置由HMaster来确定在哪个RegionServer中

  • Region分裂过程:

    • RegionServer决定本地的region分裂,第一步是,在zookeeper的/hbase/region-in-reansition/region-name下创建一个znode

    • Master通过父region-in-transition znode的watcher监测到刚刚创建的znode

    • RegionServer在HDFS中父region的目录下创建名为“.split”的子目录

    • 关闭要分裂的region的读写请求

    • 在split目录中创建所需要的文件结构

    • 移动两个新region文件目录到目录表中,并更新.META表

    • 异步复制父region中的数据到两个子region中(最主要的消耗时间)

    • 复制完成后、打开两个新产生的region,并上线,可以接收新的读写请求

    • 异步任务把定时把原来被分裂的region从.META表中清除掉,并从文件系统中删除该region所占用的空间

  • 分割点的确定: Region中StoreFile最大文件中的内部中点

21、Zookeeper相关

  • zookeeper的节点类型:

    • PERSISTENT:持久化节点

    • PERSISTENT_SEQUENTIAL:持久化有序节点

    • PHEMERAL:临时节点

    • EPHEMERAL_SEQUENTIAL:临时有序节点

  • zookeeper数据存储:

    • ZooKeeper的数据模型是一棵树,而从使用角度看, Zookeeper就像一个内存数据库一样。在这个内存数据库中,存储了整棵树的内容,包括所有的节点路径、节点数据及其ACL信息等,Zookeeper会定时将这个数据存储到磁盘上

    • DataTree:DataTree是内存数据存储的核心,是一个树结构,代表了内存中一份完整的数据。DataTree不包含任何与网络、客户端连接及请求处理相关的业务逻辑,是一个独立的组件

    • DataNode是数据存储的最小单元,其内部除了保存了结点的数据内容、ACL列表、节点状态之外,还记录了父节点的引用和子节点列表两个属性,其也提供了对子节点列表进行操作的接口

    • ZKDatabase:Zookeeper的内存数据库,管理Zookeeper的所有会话、DataTree存储和事务日志。ZKDatabase会定时向磁盘dump快照数据,同时在Zookeeper启动时,会通过磁盘的事务日志和快照文件恢复成一个完整的内存数据库

  • 目录结构:Bin 主要的一些执行命令 Conf 存放配置文件,需要修改zk.cfg Contrib 附加的一些功能

    Dist-maven Mvn编译后的目录 Docs文档 Lib 需要依赖的jar包 Src 源文件

  • leader选举如何实现:投票选举,id大的优先获得投票

22、Spark中的Stage、Job、Task的划分

  • Job的划分:根据Action算子进行划分,一个Action算子划分一个Job

  • Stage的划分:按照DAG图,从后往前,遇到宽依赖就进行Stage的划分

  • Task的划分:一个Stage中,最后一个RDD有多少个partition就划分多少个task

23、Kafka分区的目的及有序性

  • 分区对于 Kafka 集群的好处是:实现负载均衡

  • 分区对于消费者来说:可以提高并发度,提高效率

  • Kafka如何做到消息有序性的:

    • kafka是无法保证全局的消息顺序性的,只能保证主题的某个分区的消息顺序性

    • kafka 中的每个 partition 中的消息在写入时都是有序的,而且单独一个 partition 只能由一个消费者去消费,可以在里面保证消息的顺序性。但是分区之间的消息是不保证有序的

  • kafka如何保证数据的顺序消费:

    • kafka的顺序消息仅仅是通过partitionKey,将某类消息写入同一个partition,一个partition只能对应一个消费线程,以保证数据有序,除了发送消息需要指定partitionKey外,producer和consumer实例化无区别

    • 生产者在写的时候,可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的

  • 多线程进行消息消费如何保证顺序:

    • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性

24、Kafka消息是采用Push模式还是Pull模式

  • Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息

  • 一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式

  • Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略。 Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发送)

25、Kafka中的ZooKeeper是什么?是否可以脱离ZooKeeper独立运行?

  • Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用

  • 不可以,不可能越过Zookeeper直接联系Kafka broker,一旦Zookeeper停止工作,它就不能服务客户端请求

  • Zookeeper主要用于在集群中不同节点之间进行通信,在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取,除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等

26、ZooKeeper的ZAB协议

  • ZAB 协议是 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。通过该协议,Zookeepe 基于主从模式的系统架构来保持集群中各个副本之间数据的一致性

  • Zookeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据状态的变更以事务 Proposal 的形式广播到所有的副本进程上

  • ZAB协议包括两种基本的模式:

    • 崩溃恢复

      • 当整个服务框架在启动过程中,或者当 Leader 服务器出现异常时,ZAB 协议就会进入恢复模式,通过过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出恢复模式,进入消息广播模式

    • 消息广播

      • ZAB 协议的消息广播过程使用的是原子广播协议。在整个消息的广播过程中,Leader 服务器会每个事物请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播

27、Spark中的常用算子

  • 窄依赖:子RDD中一个分区中的数据来自于父RDD一个分区中的数据

  • 宽依赖:子RDD中一个分区中的数据来自于父RDD中的多个分区中的数据

  • union:合并两个RDD,窄依赖

  • join:在一个 (K, V) 和 (K, W) 类型的 Dataset 上调用时,返回一个 (K, (V, W)) 的 Dataset,等价于内连接操作。如果想要执行外连接,可以使用 leftOuterJoin, rightOuterJoinfullOuterJoin 等算子

    • 可能会发生宽依赖

    • 也可能会发生窄依赖

  • 会发生shuffle的算子: distinct() reduceByKey() groupBy() groupByKey() sortByKey() sortBy()

  • foreach:是对RDD中的每一个元素执行指定的函数

  • foreachPartition:是对RDD中的每一个Partition执行指定的函数

大数据部分2

1、Kafka如何保证数据不丢失

  • Producer端:

    • 如果设置ack=all,则生产者端不会产生数据丢失,会不断重试直至成功

    • 选择这种配置,会可靠,但是牺牲效率,可以通过,增大批和使用异步模式,提高效率

    • 异步模式下的有个buffer,通过buffer来进行控制数据的发送,有两个值来进行控制,时间阈值与消息的数量阈值,如果buffer满了数据还没有发送出去,有个选项是配置是否立即清空buffer。可以设置为-1,永久阻塞,也就数据不再生产

  • Broker端:

    • 在producer端设置ack=all,等待数据写入所有同步副本才算成功

    • 为每个分区维护至少两个同步副本(ISR列表中至少有两个)

  • Consumer:

    • 关闭自动更新offset,等到数据被处理后再手动跟新offset

    • 在消费前做验证前拿取的数据是否是接着上回消费的数据,不正确则return先行处理排错

2、ZooKeeper是如何保证数据一致性的

  • ZAB 协议是 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。通过该协议,Zookeepe 基于主从模式的系统架构来保持集群中各个副本之间数据的一致性

  • zookeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据状态的变更以事务Proposal的形式广播到所有的副本进程上去,以此来保证数据的一致性

  • 具体流程:所有的事务请求必须由唯一的Leader服务来处理,Leader服务将事务请求转换为事务Proposal,并将该Proposal分发给集群中所有的Follower服务,如果有半数的Follower服务进行了正确反馈,那么Leader服务就会再次向所有的Follower发出Commit消息,要求将前一个Proposal进行提交

3、Spark的内存模型

  • Spark在一个Executor中的内存分为三块,一块是execution内存,一块是storage内存,一块是other内存

    • execution内存是执行内存,map、join等都是在这块内存中执行,shuffle的数据也会先缓存在这个内存中,满了再写入磁盘中,可以减少IO

    • storage内存是存储broadcast、cache、persist数据的地方

    • other内存是Exeuctor程序执行时预留给自己的内存

  • 在Spark-1.6.0后加入了堆外内存,进一步优化了Spark的内存使用,堆外内存使用JVM堆以外的内存,不会被gc回收,可以减少频繁的full gc,所以在Spark程序中,会长时间逗留再Spark程序中的大内存对象可以使用堆外内存存储

4、Spark中的OOM(内存溢出)问题

  • OOM问题通常就出现在execution内存中

  • OOM问题的两种情况:

    • map执行中内存溢出

      • map执行中内存溢出代表了所有map类型的操作,包括:flatMap,filter,mapPatitions等

    • shuffle后内存溢出

      • shuffle后内存溢出的shuffle操作包括join,reduceByKey,repartition等操作

  • 内存溢出的解决办法:

    • map过程产生大量对象导致内存溢出:

      • 在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下,具体做法可以在会产生大量对象的map操作之前调用repartition方法,分区成更小的块传入map

    • 数据不平衡导致的内存溢出

      • 与map过程的相同,也是调用repartition进行重新分区

    • shuffle后的内存溢出:

      • shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的,如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量

    • standalone模式下资源分配不均匀导致内存溢出:

      • 在standalone的模式下如果配置了--total-executor-cores 和 --executor-memory 这两个参数,但是没有配置--executor-cores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置--executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀

    • 在RDD***用对象能够减少OOM的情况

5、Spark中的Shuffle机制

  • 一个 RDD 由一个或者多个分区(Partitions)组成。对于 RDD 来说,每个分区会被一个计算任务所处理,用户可以在创建 RDD 时指定其分区个数,如果没有指定,则默认采用程序所分配到的 CPU 的核心数

  • 在 Spark 中,一个任务对应一个分区,通常不会跨分区操作数据。但如果遇到 reduceByKey 等操作,Spark 必须从所有分区读取数据,并查找所有键的所有值,然后汇总在一起以计算每个键的最终结果 ,这称为 Shuffle

  • Shuffle Write:上一个stage的每一个map task就必须保证将自己处理的当前分区中的数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中

  • Shuffle Read:reduce task就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚在同一个节点上去处理和聚合

  • 会导致shuffle发生的算子:

    • 涉及到重新分区操作: 如 repartitioncoalesce

    • 所有涉及到 ByKey 的操作:如 groupByKeyreduceByKey,但 countByKey 除外

    • 联结操作:如 cogroupjoin

6、YARN中的资源调度器

  • 在yarn中负责给应用分配资源的Scheduler,共有三种资源调度器

    • FIFO Scheduler:把应用按照提交的顺序排成一个 先进先出 的队列,缺点是大的应用可能会占用集群的所有资源,导致其他应用被阻塞

    • Capacity Scheduler:容量调度器,以队列的形式配置集群资源,每个队列可以抢占队列资源(可以设置最大容量),多个队列可以同时执行任务,但每一个队列内部还是FIFO

    • Fair Schduler:公平调度器,同样的以队列的形式配置集群资源,每个队列可以抢占资源,当被抢占的队列有任务时,抢占的资源需要奉还(用完在奉还)(每个队列中的job都会分配到资源以确保公平)

7、RDD 中的 reduceByKey 与 groupByKey 哪个性能高

  • reduceByKey在进行网络传输之前首先会在每个分区上对相同key的数据进行聚合,然后在传输后再次进行聚合

  • groupByKey会将分区中所有的键值对经过网络传输之后再进行聚合

  • 所以在传输大数据集的时候reduceByKey的性能大于groupByKey

8、Flume中的事务

  • 在Flume中一共有两个事务,一个是在Source到Channel之间,一个是Channel到Sink之间。在Source到Channel之间的叫put事务,在Channel到Sink之间的叫Take事务

  • 从source到channel过程中,数据在flume中会被封装成Event对象,也就是一批event,把这批event放到一个事务中,把这个事务也就是这批event一次性的放入channel中。同理,Take事务的时候,也是把这一批event组成的事务统一拿出来到sink放到HDFS上

  • Flume事务失败会进行回滚

9、Spark的容错机制

  • RDD lineage血统图:

    • RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区(只支持粗粒度转换)

  • Checkpoint机制:

    • 检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助。lineage过长会造成容错成本过高

    • 计算此数据的花费时间较长并且对数据的持久的安全性有要求则使用checkpoint,宽依赖建议使用checkpoint

  • spark的Broadcast:广播变量,就是不把副本变量分发到每个 Task 中,而是将其分发到每个 Executor,Executor 中的所有 Task 共享一个副本变量

10、Spark和MapReduce的区别

  • Spark处理数据的设计模式与MR不同,Spark把中间的计算结果放在内存中,减少了磁盘IO,而MR在处理数据时涉及到多次磁盘IO操作

  • Spark基于RDD的设计可以进行高效的迭代计算,而MR无法实现迭代式计算

  • Spark基于DAG的血统图和checkpoint机制可以实现很好的容错性

  • MR只有map和reduce两种操作,如果需要实现复杂的操作则需要进行多个MR的复合设计,而Spark丰富的算子操作可以简单的设计完成复杂的操作

11、Spark性能调优

  • 在提交spark程序的时候合理的配置资源

  • 对多次使用的RDD进行持久化或者Checkpoint

  • 在reduceByKey和groupByKey中进行合理的选择

  • 对shuffle阶段进行优化

  • 广播共享数据

  • 优化所使用的数据结构

  • 使用序列化的持久化级别,将数据序列化之后再持久化,可以减小内存开销

12、YARN容错机制

  • map阶段或者reduce阶段中代码异常:

    • jvm会向application master发送错误报告,并将此次任务标记为fail,当application master被告知一个任务尝试失败后,将重新调度该任务执行,application master会试图避免在以前失败过的节点管理器上重新调度该任务。此外如果一个任务失败过4次,将不会再重试运行任务的最多尝试次数:mapreduce.map.maxattempts设置),默认情况下失败次数大于4次,整个作业会失败

  • 任务JVM突然退出:

    • 在这种情况下,节点管理器会通知application master将此次任务尝试标记为失败。在此之后,任务JVM进程将被自动杀死。任务被认为失败的时间默认为10分钟

  • application master运行失败:

    • application master向资源管理器发送周期性的心跳,当application master失败时,资源管理器将检测到该失败并在一个新的容器(由节点管理器管理)中开始一个新的master实例。对于MapReduce application master,它将使用作业历史来恢复失败的应用程序所运行任务的状态,使其不必重新运行。默认情况下恢复功能是开启的

  • NodeManager运行失败:

    • 如果节点管理器由于崩溃或运行非常缓慢而失败,就会停止向资源管理器发送心跳信息(或发送频率很低)。如果10分钟内没有收到一条心跳信息,资源管理器将会通知停止发送心跳信息的节点管理器,并且将其从自己的节点池中移除以调度启用容器,在失败的节点管理器上运行的所有任务或application master都会在其他节点上进行恢复

  • resourceManager运行失败:

    • 可以通过高可用配置两个资源管理器,如果主资源管理器失败了,那么备份资源管理器能够接替,且客户端不会感到明显的中断

13、spark中的partitioner(分区器)

  • HashPartitioner:

    • 它使用Java的Object.hashCode实现基于散列的分区,hashcode()的概念是相等的对象应该具有相同的哈希码。 因此,基于此hashcode()概念,HashPartitioner将划分具有相同hashcode()的键到同一分区,HashPartitioner是Spark的默认分区器

  • RangePartitioner:

    • 按范围将 可排序记录 划分为大致相等的范围。 范围通过对传入的RDD的内容进行采样来确定。即RangePartitioner将根据Key对记录进行排序,然后根据给定的值将记录划分为多个分区

  • CustomPartitioner(自定义分区器):

    • 我们还可以通过扩展Spark中的默认Partitioner类来自定义我们需要的分区数以及应该存储在这些分区中的内容。然后通过partitionBy()在RDD上应用自定义分区逻辑

  • Spark从HDFS读入文件的分区数默认等于HDFS文件的块数(blocks),HDFS中的block是分布式存储的最小单元。如果我们上传一个30GB的非压缩的文件到HDFS,HDFS默认的块容量大小128MB,因此该文件在HDFS上会被分为235块(30GB/128MB);Spark读取SparkContext.textFile()读取该文件,默认分区数等于块数即235

14、spark yarn模式下的cluster模式和client模式的区别

  • cluster模式:Driver程序在YARN中运行,应用的运行结果不能在客户端显示,所以最好运行那些将结果最终保存在外部存储介质(如HDFS、Redis、Mysql)而非stdout输出的应用程序,客户端的终端显示的仅是作为YARN的job的简单运行状况

  • client模式:Driver运行在Client上,应用程序运行结果会在客户端显示,所有适合运行结果有输出的应用程序(如spark-shell)(master通过rpc与worker进行通信,通知worker启动一个或多个executor进程)

  • Driver(applicationMaster):负责整个应用任务的job的划分和stage的切割以及task的切割和优化,并负责把task分发到worker对应的节点的executor进程中的task线程中执行, 并获取task的执行结果,Driver通过SparkContext对象与spark集群获取联系,得到master主机host,就可以通过rpc向master注册自己

  • Spark On Yarn 的优势 :1. Spark 支持资源动态共享,运行于 Yarn 的框架都共享一个集中配置好的资源池 2. 可以很方便的利用 Yarn 的资源调度特性来做分类·,隔离以及优先级控制负载,拥有更灵活的调度策略 3. Yarn 可以自由地选择 executor 数量 4. Yarn 是唯一支持 Spark 安全的集群管理器,使用 Yarn,Spark 可以运行于 Kerberized Hadoop 之上,在它们进程之间进行安全认证

15、RDD、DataFrame、DataSet 是什么,以及区别

  • RDD:弹性分布式数据集,Spark中最基础的数据抽象,特点是RDD只包含数据本身,没有数据结果

  • DataFrame:分布式数据容器,除了数据本身,还记录了数据的结构信息,及Schema(列名、列字段类型)

  • DataSet:不仅包含数据本身,记录了数据的结构信息schema,还包含了数据集的类型,也就是真正把数据集做成了一个java对象的形式,需要先创建一个样例类case class,把数据做成样例类的格式,每一列就是样例类里的属性

16、Hive中的分区表和分桶表

  • 分区表:

    • 分区可以理解为分类,通过分类把不同类型的数据放到不同的目录下

    • 分类的标准就是分区字段,可以一个,也可以多个

    • 分区表的意义在于优化查询。查询时尽量利用分区字段。如果不使用分区字段,就会全部扫描

    • 分区表中需要指定分区字段,其实分区字段就是目录名称,但是我们查询的时候却可以把分区字段当成查询条件来使用

  • 分桶表:

    • 分桶是相对分区进行更细粒度的划分。分桶将整个数据内容某列按照属性值的hash值进行区分,如要安装name属性分为3个桶,就是对name属性值的hash值对3取摸,按照取模结果对数据分桶。如取模结果为0的数据记录存放到一个文件,取模为1的数据存放到一个文件,取模为2的数据存放到一个文件

17、Kafka监控应该怎么做

  • 一套完整的kafka监控,应该包括:

    • 消费者监控,主要是存活告警,消费滞后告警

    • 生产者监控,主要是存活告警,生产者消费上游数据能力告警

    • broker监控,主要是存活告警,流量告警,isr列表,topic异常告警,control变换告警

  • kafka实现监控的三种方法:

    • 使用kafka自带的命令行工具kafka-consumer-groups.sh脚本查看

    • 使用kafka Consumer API编程

    • 使用kafka自带的JMX监控指标

18、SparkStreaming与Kafka集成问题

  • sparkStreaming 创建的 RDD 的分区数和 kafka partitions分区数是一致的

  • 如果Spark程序初始化时未设置核数,则会默认分区数与CPU核数相同

  • 在kafka中:一个消费线程可以对应若干个分区,但同一时刻一个分区只能被具体某一个消费线程消费

  • Spark程序可以通过Web UI对程序运行时间进行监控

19、SparkSql运行原理

  • 进行 DataFrame/Dataset/SQL 编程

  • 如果是有效的代码,即代码没有编译错误,Spark 会将其转换为一个逻辑计划

  • Spark 将此逻辑计划转换为物理计划,同时进行代码优化

  • Spark 然后在集群上执行这个物理计划 (基于 RDD 操作)

20、SparkSql与Hive的区别

  • Hive是一种基于HDFS的数据仓库,并且提供了基于SQL模型的,针对存储了大数据的数据仓库,进行分布式交互查询的查询引擎

  • SparkSQL并不能完全替代Hive,它替代的是Hive的查询引擎,SparkSQL由于其底层基于Spark自身的基于内存的特点,因此速度是Hive查询引擎的数倍以上,Spark本身是不提供存储的,所以不可能替代Hive作为数据仓库的这个功能

  • SparkSQL相较于Hive的另外一个优点,是支持大量不同的数据源,包括hive、json、parquet、jdbc等

  • SparkSQL由于身处Spark技术堆栈内,基于RDD来工作,因此可以与Spark的其他组件无缝整合使用,配合起来实现许多复杂的功能

21、HQL与SQL语句的区别

  • Hive不支持等值连接

    • SQL中对两表内联可以写成: •select * from dual a,dual b where a.key = b.key; •Hive中应为 •select * from dual a join dual b on a.key = b.key

  • hive支持嵌入mapreduce程序,来处理复杂的逻辑

  • hive支持将转换后的数据直接写入不同的表,还能写入分区、hdfs和本地目录

  • hive不支持事务

22、Flink的优势

  • Flink 是基于事件驱动 (Event-driven) 的应用,能够同时支持流处理和批处理

  • 基于内存的计算,能够保证高吞吐和低延迟,具有优越的性能表现

  • 支持精确一次 (Exactly-once) 语意,能够完美地保证一致性和正确性

  • 分层 API ,能够满足各个层次的开发需求

  • 支持高可用配置,支持保存点机制,能够提供安全性和稳定性上的保证

  • 多样化的部署方式,支持本地,远端,云端等多种部署方案

  • 具有横向扩展架构,能够按照用户的需求进行动态扩容

  • 活跃度极高的社区和完善的生态圈的支持

23、大数据中的海量小文件问题

  • LOSF(lots of samll file),大小在1MB以内的文件称为小文件,百万级数量及以上称为海量文件

  • 衡量存储系统性能主要有两个关键指标,即IOPS和数据吞吐量(IOPS (Input/Output Per Second) 即每秒的输入输出量 ( 或读写次数 ) ,是衡量存储系统性能的主要指标之一)

  • 海量小文件LOSF问题:

    • 元数据管理低效

    • 数据布局低效(数据块零散分配在磁盘的不同位置上)

    • IO流访问流程复杂

  • 磁盘最适合顺序的大文件I/O读写模式,但非常不适合随机的小文件I/O读写模式

  • 海量小文件由于数量巨大,而且通常需要进行共享和并发访问,目前通常采用分布式系统进行存储,包括分布式文件系统和分布式对象存储系统,每个存储节点底层采用磁盘文件系统进行管理

  • 优化策略:

    • 按照减少数据访问时间的优化思路,采用更高性能的硬件来提高LOSF性能

    • 小文件合并存储(减少了大量元数据,简化了IO访问流程)

    • 元数据管理优化(对元数据进行精简)

  • Hadoop中的小文件问题:

    • 在NameNode(内存)中保存的元数据每一条大约占用150byte,而hdfs中的小文件指的是其大小远远小于默认的block块的大小

    • 大量小文件对性能的影响:

      • 大量的小文件的存在会占用大量的NameNode内存,会影响hdfs的扩展能力

      • MapReduce时每一个block块会用一个Map来处理,而大量的会造成集群资源的过量消耗

    • 小文件的产生情况:

      • 实时流处理:SparkStreaming实时流处理后的数据直接存储至hdfs中,而由于SparkStreaming的微批处理模式和RDD的特性,会导致产生大量的小文件

      • Reduce的设置不合理导致产生大量的小文件

      • 数据本身的特点:在 HDFS 上存储大量的图片、短视频、短音频等文件,由于这些文件的特点,会导致产生大量的小文件

    • hdfs中小文件问题的解决方案:

      • Hadoop Archive(HRA-文件归档):是一种特殊的归档格式,Hadoop Archive映射到文件系统目录,一个HAR是以扩展名 .har结尾 ,一个HAR目录包含元数据(以index和masterindex的形式)和data(part-*)文件,Hadoop Archive是一个高效地将小文件放入HDFS块中的文件存档工具,它能将多个小文件打包成一个HAR文件,这样在减少NameNode内存使用的同时,仍然允许对文件进行透明的访问

      • SequenceFile:是一个由二进制序列化过的key/value字节流组成的文本存储文件,它可以在map/reduce过程中的input/output的format时被使用。通过改变文件的写出方式,写入到SequenceFile格式的文件中(因为文件内容很小,可以以文件名作为key,文件内容作为value,直接写到SequenceFile中)

      • hive中的大量小文件:1.动态分区插入数据,产生大量的小文件,从而导致map数量剧增。 2.reduce数量越多,小文件也越多(reduce的个数和输出文件是对应的)。 3.数据源本身就包含大量的小文件 也可以使用SequenceFile来解决

  • SparkStreaming中的小文件解决办法:

    • 增加batch的大小

    • 对SparkStreaming处理的数据进行再次合并处理后进行存储

大数据部分3

1、Hive中的数据倾斜

  • 数据倾斜就是数据的分布不平衡

  • key的分布不均匀或者说是某些key太集中

    • key的分布不均匀就会导致MR过程中的某些reduce的需要处理的数据量特别大,有些特别小,就会导致整个任务因为需要等待数据量特别大的reduce而迟迟无法完成

    • 解决办法:

      • 设置hive.map.aggr=true //开启map端部分聚合功能,就是将key相同的归到一起

      • 设置hive.groupby.skewindata=true //如果发生了数据倾斜就可以通过它来进行负载均衡

  • 业务数据自身的特性

    • 某些业务数据作为key的字段本身就很集中,会导致数据倾斜(本质也是key分布不均匀)

  • SQL语句造成的数据倾斜

    • 进行表的join这种业务操作时,经常会产生数据倾斜,因为数据要进入reduce,肯定要先进行分区,而分区就是根据key的hash值来进行的,既然数据的key本身就是不均匀的了(即某些key很集中,或者干脆就是有很多相同的key,比如key为无效值null),那这样分区的结果就是这些集中的key会被分到同一个分区中,而这些key的数量本就大,所以会产生数据倾斜

    • 解决办法:

      • 大小表Join: 使用map join让表(1000条以下的记录条数)先进内存,在Map端完成Reduce

      • 大表Join大表:

        • 当一张表中的大部分值都是0或者null时,容易shuffle给一个reduce,造成数据倾斜,而这种情况可以通过给异常key赋一个随机的值来分散key,来解决数据倾斜

        • 当key值都是有效值时,可以通过设定(set hive.optimize.skewjoin = true; set hive.skewjoin.key = skew_key_threshold (default = 100000))来控制倾斜的阈值,如果超过这个值,新的值会发送给那些还没有达到的reduce

  • group by造成的数据倾斜:

    • 对数据做类型统计的时候,如果按类型进行group by则当某种类型的数据量特别多的时候,会出现某些reduce的处理数据量特别大,从而导致数据倾斜

    • 可以通过设置(hive.map.aggr=true(默认true) 这个配置项代表是否在map端进行聚合,相当于Combiner hive.groupby.skewindata=true(默认false),有数据倾斜的时候进行负载均衡,当选项设定为 true,生成的查询计划会有两个 MR Job。第一个 MR Job 中,Map 的输出结果集合会随机分布到 Reduce 中,每个 Reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的 Group By Key 有可能被分发到不同的 Reduce 中,从而达到负载均衡的目的;第二个 MR Job 再根据预处理的数据结果按照 Group By Key 分布到 Reduce 中(这个过程可以保证相同的 Group By Key 被分布到同一个 Reduce 中),最后完成最终的聚合操作

2、Hive中的分区、分桶、内部、外部表

  • 内部表:未被external修饰的表为内部表,内部表数据由hive自身管理,在删除表的时候会删除其元数据以及存储数据

  • 外部表:建表时被external修饰的为外部表,外部表的数据由hdfs管理,在删除表的时候只会删除元数据,而由hdfs管理的文件则不会被删除

  • 分区表:分区可以理解为分类,通过分类把不同类型的数据放到不同的目录下,分区表中需要指定分区字段,然后可以在查询的时候将分区字段作为条件,查询指定的数据到指定的分区字段下,是对hive的一种优化

  • 分桶表:分桶是相对分区更细致的划分,分桶按照分桶字段的hash值去模除以分桶的个数,如要按照name属性分为3个桶,就是对name属性值的hash值对3取摸,按照取模结果对数据分桶

  • 分桶的作用:

    • 获得更高的查询处理效率,桶为表加上了额外的结构,Hive 在处理有些查询时能利用这个结构

    • 使取样(sampling)更高效,在处理大规模数据集时,在开发和修改查询的阶段,如果能在数据集的一小部分数据上试运行查询,会带来很多方便

    • 最大的作用是用来提高join操作的效率

  • hive的动态分区:hive提供了一个动态分区功能,其可以基于查询参数的位置去推断分区的名称,从而建立分区,可以通过配置参数实现

    • set hive.exec.dynamic.partition =true(默认false),表示开启动态分区功能;

    • set hive.exec.dynamic.partition.mode = nonstrict(默认strict),表示允许所有分区都是动态的,否则必须有静态分区字段

3、数仓分层

  • ETL(数据仓库技术)是将业务系统的数据经过抽取、清洗转换之后加载到数据仓库的过程,目的是将企业中的分散、零乱、标准不统一的数据整合到一起,为企业的决策提供分析依据

  • 数据仓库:是对整合的多个数据源的历史数据进行细粒度的、多维的分析,帮助高层管理者或者业务分析人员做出商业战略决策或商业报表

  • 数据仓库分为四层:

    • ODS层(原始数据层):存放原始数据,直接加载原始日志、数据,数据保存原貌不做处理

    • DWD层(明细数据层):结构与粒度原始表保持一致,对ODS层数据进行清洗(去除空值、脏数据、超过极限范围的数据)

    • DWS(服务数据层):以DWD为基础,进行轻度汇总

    • ADS(数据应用层):为各种统计报表提供数据

  • 为什么要分层:

    • 空间换时间:通过建设多层次的数据模型供用户使用,避免用户直接使用操作型数据,可以更高效的访问数据

    • 把复杂问题简单化:一个复杂的任务分解成多个步骤来完成,每一层只处理单一的步骤,比较简单和容易理解。而且便于维护数据的准确性,当数据出现问题之后,可以不用修复所有的数据,只需要从有问题的步骤开始修复

    • 便于处理业务的变化:随着业务的变化,只需要调整底层的数据,对应用层对业务的调整零感知

  • 数据仓库与数据集市区别:

    • 数据仓库是企业级的,能为整个企业各个部门的运行提供决策支持手段

    • 数据集市则是一种微型的数据仓库,它通常有更少的数据,更少的主题区域,以及更少的历史数据,因此是部门级的,一般只能为某个局部范围内的管理人员服务,因此也称之为部门级数据仓库

4、数仓建模

  • 数仓建模阶段划分:

    • 业务建模:生成业务模型,主要解决业务层面的分解和程序化

    • 领域概念建模:生成领域模型,主要是对业务模型进行抽象处理,生成领域概念模型

    • 逻辑建模:生成逻辑模型,主要是将领域模型的概念实体以及实体之间的关系进行数据库层次的逻辑化

    • 物理建模:生成物理模型,主要解决,逻辑模型针对不同关系型数据库的物理化以及性能等一些具体的技术问题

5、Spark垃圾回收

  • 在Spark程序运行时Executor内存主要用于:1、用于缓存RDD(cache) 2、分配给task,在task执行过程中创建对象

  • 默认情况下,Executor占用的内存,60%划分给RDD缓存,40%分配给task,存放运行期间创建的对象,这种默认情况可能会导致task用于运行期间创建对象的内存偏小了。当创建的对象把40%的内存空间用完,就会产生垃圾回收(GC),把不再被使用的对象清除出内存。所以说,如果给task分配的内存过低,会导致GC频繁发生,从而导致task工作线程频繁停止,降低了spark计算效率

  • SparkConf().set("spark.storage.memoryFraction","比例"),此处的比例为RDD缓存的内存分配比例,rdd的持久化也可通过序列化来降低内存的占用

6、Hive的优化

  • hive的小文件问题:

    • 动态分区插入数据,产生大量的小文件,从而导致map数量剧增(动态分区插入:通过在扫描输入表时动态确定应该创建和填充的分区)

    • reduce数量越多,小文件也越多

    • 数据源本身就包含大量的小文件

  • 小文件问题的影响:

    • 从Hive的角度看,小文件会开很多map,一个map开一个JVM去执行,所以这些任务的初始化,启动,执行会浪费大量的资源,严重影响性能

    • 在HDFS中,每个小文件对象约占150byte,如果小文件过多会占用大量内存。这样NameNode内存容量严重制约了集群的扩展

  • 小文件问题的解决:

    • 通过参数设置减少reduce的数量

    • 使用hadoop archive命令把小文件进行归档

    • 使用Sequencefile作为表存储格式在一定程度上可以减少小文件数量

  • 设置合理的map/reduce数量

  • 对SQL语句进行优化

  • 通过设置分区表优化查询

7、SparkSql特点

  • 能够将 SQL 查询与 Spark 程序无缝混合,允许您使用 SQL 或 DataFrame API 对结构化数据进行查询

  • 支持多达上百种的外部数据源,包括 Hive,Avro,Parquet,ORC,JSON 和 JDBC 等

  • 支持 HiveQL 语法以及 Hive SerDes 和 UDF,允许你访问现有的 Hive 仓库

  • 支持标准的 JDBC 和 ODBC 连接

  • 支持扩展并能保证容错

  • SparkSql窗口函数:

    • 窗口函数是spark sql模块从1.4之后开始支持的,主要用于解决对一组数据进行操作,同时为每条数据返回单个结果,比如计算指定访问数据的均值、计算累进和或访问当前行之前行数据等

    • Spark SQL支持三类窗口函数:排名函数、分析函数和聚合函数

8、SparkStreaming与Flink的区别

  • 处理模型与延迟:

    • Flink的处理模型为单条事件处理,亚秒级低延迟

    • SparkStreaming的处理模型为一个窗口内的所有事件,秒级延迟

  • 任务调度:

    • Spark Streaming 是基于微批处理的,实际上每个批次都是一个 Spark Core 的任务。对于编码完成的 Spark Core 任务在生成到最终执行结束主要包括以下几个部分:

      • 构建 DGA 图;

      • 划分 stage;

      • 生成 taskset;

      • 调度 task

    • 对于 flink 的流任务客户端首先会生成 StreamGraph,接着生成 JobGraph,然后将 jobGraph 提交给 Jobmanager 由它完成 jobGraph 到 ExecutionGraph 的转变,最后由 jobManager 调度执行

  • 运行角色不同:

    • Spark Streaming 运行时的角色(standalone 模式)主要有:

      • Master: 主要负责整体集群资源的管理和应用程序调度

      • Worker: 负责单个节点的资源管理,driver 和 executor 的启动等

      • Driver: 用户入口程序执行的地方,即 SparkContext 执行的地方,主要是 DGA 生成、stage 划分、task 生成及调度

      • Executor: 负责执行 task,反馈执行状态和执行结果

    • Flink 运行时的角色(standalone 模式)主要有:

      • Jobmanager: 协调分布式执行,他们调度任务、协调 checkpoints、协调故障恢复等。至少有一个 JobManager。高可用情况下可以启动多个 JobManager,其中一个选举为 leader,其余为 standby

      • Taskmanager: 负责执行具体的 tasks、缓存、交换数据流,至少有一个 TaskManager

      • Slot: 每个 task slot 代表 TaskManager 的一个固定部分资源,Slot 的个数代表着 taskmanager 可并行执行的 task 数

9、Spark on yarn 的提交流程

  • Spark Yarn Client 向 Yarn 中提交应用程序

  • ResourceManager 收到请求后,在集群中选择一个 NodeManager,并为该应用程序分配一个 Container,在这个 Container 中启动应用程序的 ApplicationMaster, ApplicationMaster 进行 SparkContext 等的初始化(一个Job对应一个AM)

  • ApplicationMaster 向 ResourceManager 注册,这样用户可以直接通过 ResourceManager 查看应用程序的运行状态,然后采用轮询的方式通过RPC协议为各个任务申请资源,并监控它们的运行状态

  • ApplicationMaster 申请到资源(也就是Container)后,Executor会向SparkContext注册并申请task

  • Executor运行task并向ApplicationMaster汇报运行状态和进度

  • 应用程序运行完成后,ApplicationMaster 向 ResourceManager申请注销并关闭自己

10、Yarn的架构

  • ResourceManager

  • ResourceManager 通常在独立的机器上以后台进程的形式运行,它是整个集群资源的主要协调者和管理者。ResourceManager 负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息,做出决策,然后以共享的、安全的、多租户的方式制定分配策略,调度集群资源

  • NodeManager

  • NodeManager 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康

    • 启动时向 ResourceManager 注册并定时发送心跳消息,等待 ResourceManager 的指令

    • 维护 Container 的生命周期,监控 Container 的资源使用情况

    • 管理任务运行时的相关依赖,根据 ApplicationMaster 的需要,在启动 Container 之前将需要的程序及其依赖拷贝到本地

  • ApplicationMaster

    • 在用户提交一个应用程序时,YARN 会启动一个轻量级的进程 ApplicationMasterApplicationMaster 负责协调来自 ResourceManager 的资源,并通过 NodeManager 监视容器内资源的使用情况,同时还负责任务的监控与容错

      • 根据应用的运行状态来决定动态计算资源需求

      • ResourceManager 申请资源,监控申请的资源的使用情况

      • 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息

      • 负责任务的容错

  • YarnChild

    • 真正执行task的进程

  • Container

    • Container 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 Container 表示的。YARN 会为每个任务分配一个 Container,该任务只能使用该 Container 中描述的资源

11、Kafka丢失数据的情况

  • producer配置acks=0 ,1

  • kafka的数据一开始就是存储在PageCache上的,定期flush到磁盘上的,也就是说,不是每个消息都被存储在磁盘了,如果出现断电或者机器故障等,PageCache上的数据就丢失了

  • 网络负载很高或者磁盘很忙写入失败的情况下(无重发重试)

  • 消费者崩溃,还没有处理的数据就被commit offset了

12、HDFS的block块相关

  • 为什么hdfs中的block块设置的较大:

    • hdfs块比磁盘的块大,其目的是为了最小化寻址开销。如果块设置的足够大,那么从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间,因而,传输一个由多个块组成的文件时间取决于磁盘的传输速率

  • hdfs中block为什么设置为128MB:

    • hdfs中的平均寻址时间为10ms,而传输速率普遍为100MB/s而在大量测试中得出寻址时间为传输时间的1%时,为最佳状态

    • 通过计算最佳传输时间=10ms/0.01=1s,进一步计算出Block块大小=1s*100MB=100MB,而我们磁盘block块的大小都是2^n倍,所以设置hdfs中的blcok大小为128MB

  • 如果block设置过大:

    • 在MapReduce中的map任务通常一次只处理一个块中的数据,因此设置的过大就会使作业的运行速度较慢

  • hdfs中小于blcok块大小的文件如何存储:

    • 当所存储的文件小于block块的大小时,它并不会占用整个block块的大小,只会占用它的实际存储大小

13、Hive的相关语法问题

  • order by 会对输入做全局排序,因此只有一个reducer,会导致当输入规模较大时,需要较长的计算时间

  • sort by不是全局排序,其在数据进入reducer前完成排序。因此,如果用sort by进行排序,并且设置mapred.reduce.tasks>1,则sort by只保证每个reducer的输出有序,不保证全局有序

  • distribute by根据distribute by指定的内容将数据分到同一个reducer

  • distribute by类似MR中partition(自定义分区),进行分区,结合sort by使用(distribute by的分区规则是根据分区字段的hash码与reduce的个数进行模除后,余数相同的分到一个区),Hive要求DISTRIBUTE BY语句要写在SORT BY语句之前

  • Hive的Join:

    • Hive 不支持非等值的连接,因为非等值连接非常难转化到 map/reduce 任务

    • 只支持等值查询,也可以join多于两个表

    • join 时,每次 map/reduce 任务的逻辑:

      • reducer 会缓存 join 序列中除了最后一个表的所有表的记录,再通过最后一个表将结果序列化到文件系统。这一实现有助于在 reduce 端减少内存的使用量。实践中,应该把最大的那个表写在最后(否则会因为缓存浪费大量内存)

14、Hive的join过程

  • select * from a inner join b on a.id=b.id 返回两张表中都有的信息

  • select * from a left join b on a.id=b.id 以前面的表作为主表和其他表进行关联,返回的记录数和主表的记录数相同,关联不上的字段用NULL

  • select * from a right join b on a.id=b.id 以后面的表为主表,和前面的表做关联,返回的记录数和主表一致,关联不上的字段为NULL

  • select * from a inner join b on a.id=b.id where b.id is null 查询两个表中不一样的数据(hive自从2.2以后开始支持不等值连接)

  • 如果不指定MapJoin或者不符合MapJoin的条件,那么Hive解析器会将Join操作转换成Common Join,即:在Reduce阶段完成join,整个过程包含Map、Shuffle、Reduce阶段

  • Map阶段:

    • 读取源表的数据,Map输出时候以Join on条件中的列为key,如果Join有多个关联键,则以这些关联键的组合作为key,Map输出的value为join之后所关心的(select或者where中需要用到的)列;同时在value中还会包含表的Tag信息,用于标明此value对应哪个表

  • Shuffle阶段:

    • 根据key的值进行hash,并将key/value按照hash值推送至不同的reduce中,这样确保两个表中相同的key位于同一个reduce中

  • Reduce阶段:

    • 根据key的值完成join操作,期间通过Tag来识别不同表中的数据

  • MapJoin:

    • MapJoin通常用于一个很小的表和一个大表进行join的场景

    • 在map 端进行join,其原理是broadcast join,即把小表作为一个完整的驱动表来进行join操作

    • 要连接的各个表里面的数据会分布在不同的Map中进行处理。即同一个Key对应的Value可能存在不同的Map中。这样就必须等到 Reduce中去连接。要使MapJoin能够顺利进行,那就必须满足这样的条件:除了一份表的数据分布在不同的Map中外,其他连接的表的数据必须在每 个Map中有完整的拷贝。MAPJION会把小表全部读入内存中,在map阶段直接拿另外一个表的数据和内存中表数据做匹配,由于在map是进行了join操作,省去了reduce运行的效率也会高很多

15、SparkStreaming与Kafka

  • Spark Streaming 中提供了如下三种位置策略,用于指定 Kafka 主题分区与 Spark 执行程序 Executors 之间的分配关系:

    • PreferConsistent : 它将在所有的 Executors 上均匀分配分区

    • PreferBrokers : 当 Spark 的 Executor 与 Kafka Broker 在同一机器上时可以选择该选项,它优先将该 Broker 上的首领分区分配给该机器上的 Executor

    • PreferFixed : 可以指定主题分区与特定主机的映射关系,显示地将分区分配到特定的主机,其构造器如下:

      • @Experimental def PreferFixed(hostMap: collection.Map[TopicPartition, String]): LocationStrategy =  new PreferFixed(new ju.HashMap[TopicPartition, String](hostMap.asJava))  @Experimental def PreferFixed(hostMap: ju.Map[TopicPartition, String]): LocationStrategy =  new PreferFixed(hostMap)
  • SparkStreaming消费Kafka的方式:

    • 基于Receiver的方式:把数据从kafka中读取出来然后缓存到内存然后再定时处理(会产生数据丢失的风险 如果要保证高可用必须开启WAL机制,影响性能)

    • 基于Direct的方式:周期性地查询kafka,来获得每个topic+partition的最新的offset,并且主动的进行数据获取

      • 可以简化并行读取:spark会创建跟kafka partition一样多的RDD partition,并且会并行从kafka中读取数据

      • 高性能:kafka中做了数据复制,可以通过kafka的副本进行恢复

      • 缺点是成本提高且无法通过zookeeper来监控消费者消费情况

16、Hadoop相关

  • NN与DN之间的交互:DataNode 会有一个后台线程,定时向NameNode 发送每个块的最新状态及信息,通过RPC通信

  • HA的实现:

    • 创建JournalNode集群来存储各NameNode所需的元数据信息,从而保持元数据信息同步

    • 每个NameNode都有一个对应的ZKFC作为自己的监护线程,HDFS系统初次启动时,多个NameNode对应的ZKFC会进行锁的争夺(即Zookeeper中的节点注册),抢到锁的ZKFC会将对应的NameNode置成Active。若是ZKFC在监视过程发现自己负责的Active NameNode没有信息反馈,则认为该NameNode已经故障,则会将抢到的锁释放。其他的ZKFC会对释放的锁进行争夺,获得者会进入原Active NameNode中将原Active NameNode 线程杀掉,然后将自己对应的NameNode置为Active对外服务,从而解决了单NameNode故障,集群无法工作及Active NameNode的选择问题

17、Spark累加器与广播变量

  • 在 Spark 中,提供了两种类型的共享变量:累加器 (accumulator) 与广播变量 (broadcast variable):

    • 累加器:用来对信息进行聚合,主要用于累计计数等场景

    • 广播变量:主要用于在节点间高效分发大对象

  • 累加器(accumulator)是 Spark 中提供的一种分布式的变量机制,其原理类似于 mapreduce,即分布式的改变,然后聚合这些改变。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数

18、Kafka消费者

  • 为什么要有消费者组:(一个分区只可以被一个消费者组中的消费者消费)

    • 消费者通常是消费者群组的一部分,多个消费者群组共同读取同一个主题时,彼此之间互不影响

    • 之所以引进消费者组是因为,Kafka 消费者经常会做一些高延迟的操作,比如把数据写到数据库或 HDFS ,或者进行耗时的计算,在这种情况下单个消费者无法跟上数据生成的速度,则此时可以增加更多的消费者,让它们分担负载,分别处理部分分区的消息,这就是 Kafka 实现横向伸缩的主要手段

    • 所以应该合适的设置消费者的数量,以免造成闲置和额外开销

  • 分区再均衡:

    • 因为群组里的消费者共同读取主题的分区,所以当一个消费者被关闭或发生崩溃时,它就离开了群组,原本由它读取的分区将由群组里的其他消费者来读取

    • 同时在主题发生变化时 , 比如添加了新的分区,也会发生分区与消费者的重新分配,分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。正是因为再均衡,所以消费费者群组才能保证高可用性和伸缩性

    • 消费者通过向群组协调器所在的 broker 发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发再均衡

  • 偏移量:

    • Kafka 的每一条消息都有一个偏移量属性,记录了其在分区中的位置,偏移量是一个单调递增的整数

    • 如果不能正确的提交偏移量则会导致数据丢失或者重复消费

    • Kafka 支持自动提交和手动提交偏移量两种方式

  • 监听分区再均衡:因为分区再均衡会导致分区与消费者的重新划分,有时候你可能希望在再均衡前执行一些操作:比如提交已经处理但是尚未提交的偏移量,关闭数据库连接等。此时可以在订阅主题时候,调用 subscribe 的重载方法传入自定义的分区再均衡监听器

  • Kafka 的设计目标是高吞吐和低延迟,所以在 Kafka 中,消费者通常都是从属于某个群组的,这是因为单个消费者的处理能力是有限的(也可以设置单个消费者,只需要把主题或者分区分配给消费者,然后开始读取消息井提交偏移量即可)

19、Kafka生产者

  • 生产者发送消息的过程:

    • Kafka 会将发送消息包装为 ProducerRecord 对象, ProducerRecord 对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送 ProducerRecord 对象前,生产者会先把键和值对象序列化成字节数组,这样它们才能够在网络上传输

    • 接下来,数据被传给分区器。如果之前已经在 ProducerRecord 对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区 ,那么分区器会根据 ProducerRecord 对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上

    • 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka,就返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数后还没有成功,则直接抛出异常,不再重试

  • Kafka 有着默认的分区机制:

    • 如果键值为 null, 则使用轮询 (Round Robin) 算法将消息均衡地分布到各个分区上

    • 如果键值不为 null,那么 Kafka 会使用内置的散列算法对键进行散列,然后分布到各个分区上

  • 可以通过实现Partitioner来自定义分区器,然后再创建生产者时指定分区器和所需要配置的参数

20、kafka副本机制

  • 为了保证高可用,kafka 的分区是多副本的,如果一个副本丢失了,那么还可以从其他副本中获取分区数据

  • 分区:kafka的主题被分为多个分区,分区是kafka最基本的存储单位,每个分区都可以有多个副本,其中一个副本是首领副本 (Leader replica),所有的事件都直接发送给首领副本;其他副本是跟随者副本 (Follower replica),需要通过复制来保持与首领副本数据一致,当首领副本不可用时,其中一个跟随者副本将成为新首领

  • ISR机制:每个分区都有5一个 ISR(in-sync Replica) 列表,用于维护所有同步的、可用的副本

  • 同步副本:

    • 与 Zookeeper 之间有一个活跃的会话,即必须定时向 Zookeeper 发送心跳

    • 在规定的时间内从首领副本那里低延迟地获取过消息

21、Yarn的作用

  • hadoop1.x版本的mapreduce执行任务时主要的两种后台服务进程:

    • JobTracker:负责资源管理,负责接收客户作业提交,调度任务到工作节点上运行,并提供诸如监控工作节点状态及任务进度等管理功能

    • TaskTracker:通过周期性的心跳来通知jobtracker其当前的健康状态,每一次心跳包含了可用的map和reduce任务数目、占用的数目以及运行中的任务详细信息

    • 该版本mapreduce的缺点:

      • JobTracker容易存在单点故障

      • jobTracker负担重,既要负责资源管理,又要进行作业调度;当需处理太多任务时,会造成过多的资源消耗

      • 在TaskTracker端,把资源强制划分为map task slotreduce task slot,如果当系统中只有map task或者只有reduce task的时候,会造成资源的浪费

  • hadoop2.x上的mapreduce是基于YARN 的,YARN支持多个计算框架

  • Yarn的作用:

    • YARN的设计减小了JobTracker的资源消耗

    • 对于资源的表示以内存为单位,比之前的表示方式更加合理

    • MRv1中JobTracker一个很大的负担就是监控job下的tasks的运行状况,现在这个部分由ApplicationMaster来实现

    • Container用来作为YARN的一个资源隔离组件,可以用来对资源进行调度和控制

22、MapReduce

  • 在shuffle过程中ruduce如何去对应的maptask拉去数据:

    • 每个reduce task不断地通过RPC从ApplicationMaster那里获取map task是否完成的信息(还有存储位置等),如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动

  • mapreduce中的两次排序:

    • 第一次排序发生在map阶段,在map阶段环形(内存)缓冲区的数据写入到磁盘的时候会根据reducetask的数量生成对应的分区,然后根据对应数据的哈希对分区数取模写入,然后会根据key值对分区中的数据使用快速排序算法进行排序,所以每个分区内的数据是有序的

    • 第二次排序发生在reduce阶段,reducetask去每一个maptask所在的节点上的对应的分区(可以根据分区好)上拉取数据,然后因为分区写入使用哈希取模算法,所以相同的数据必定写在同一reduce分区中,采用归并排序对所拉取数据的key进行排序

    • 最后有几个reduce分区就会生成几个partition文件

23、ZooKeeper相关

  • zookeeper的选举Master机制:通过过半选举机制产生leader(投票选举机制 ID大的优先获得投票)

  • zookeeper集群中节点数设置:

    • zookeeper选举规则:leader选举,要求 可用节点数量 > 总节点数量/2 (为了防止在集群出现脑裂的时候,可能会出现多个子集群同时服务的情况(即子集群各组选举出自己的leader), 这样对整个zookeeper集群来说是紊乱的)

    • 集群脑裂:集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的master节点,导致原有的集群出现多个master节点的情况,这就是脑裂

    • 所以可以通过leader选举规则,在容错能力相同的条件下,奇数个节点更加节省资源

  • zookeeper的数据模型:Zookeeper 通过树形结构来存储数据,它由一系列被称为 ZNode 的数据节点组成,类似于常见的文件系统。不过和常见的文件系统不同,Zookeeper 将数据全量存储在内存中,以此来实现高吞吐,减少访问延迟

  • zookeeper集群:可以由一组 Zookeeper 服务构成 Zookeeper 集群,集群中每台机器都会单独在内存中维护自身的状态,并且每台机器之间都保持着通讯,只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务

  • 集群角色:

    • Leader :为客户端提供读写服务,并维护集群状态,它是由集群选举所产生的

    • Follower :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态。同时也参与写操作“过半写成功”的策略和 Leader 的选举

    • Observer :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态,但不参与写操作“过半写成功”的策略和 Leader 的选举,因此 Observer 可以在不影响写性能的情况下提升集群的读性能

  • 顺序访问:对于来自客户端的每个更新请求,Zookeeper 都会分配一个全局唯一的递增 ID,这个 ID 反映了所有事务请求的先后顺序

  • 高性能:ZooKeeper 将数据存全量储在内存中以保持高性能,并通过服务集群来实现高可用,由于 Zookeeper 的所有更新和删除都是基于事务的,所以其在读多写少的应用场景中有着很高的性能表现

24、Hbase列族数量

  • Hbase官方文档中写明,目前列族数量最优不超过3个

  • 每个 RegionServer 包含多个 Region,每个 Region 包含多个Store,每个 Store 包含一个 MemStore 和多个 StoreFile,而在HBase中,每个列族则对应Region中的一个Store,而在Region的大小达到阈值的时候则会进行分裂,因此表中有多个列族时则会导致:

    • 一个Region中有多个Store,如果每个ColumnFamily的数据量分布不均匀时,如CF1为100万,CF2为1万,则 Region分裂时导致CF2在每个Region中的数据量太少,查询CF2时会横跨多个Region导致效率降低

    • 多个列族则代表会有多个Store,而每个Store则包含一个MemStore,而多个MemStore会导致内存的消耗量增大,影响使用性能

    • 在Region 中的缓存 刷新 和 压缩 是基本操作,即一个ColumnFamily出现缓存刷新或压缩操作,其它ColumnFamily也会同时做一样的操作,当列族太多时就会导致IO过于频繁

25、Hive与关系型数据库比较

  • Hive使用HDFS(hadoop的分布式文件系统),关系型数据库则是服务器本地的文件系统

  • Hive使用的计算模型是MapReduce,而关系型数据库则是自己设计的计算模型

  • Hive是为海量数据做数据挖掘分析而设计的,实时性差;而关系型数据库是为实时查询的业务进行设计的

  • Hive是基于Hadoop的,所以Hive的可扩展性高

  • Hive所支持的数据规模远远大于关系型数据库

  • Hive是针对数据仓库而设计的,所以Hive的数据更新延迟高(且行级更新,插入需要通过配置文件实现)

26、Hive调优

  • 根据使用场景的不同选择合适的文件存储格式

  • MapReduce任务的主要瓶颈在网络IO和磁盘IO,所以应该选择合适的数据压缩方式来减少数据量

  • 对Sql语句的优化

  • 避免产生数据倾斜

27、Hive常用的存储格式

  • TEXTFILE:按行存储,不支持块压缩,默认格式,数据不做压缩,磁盘开销大,加载数据的速度最高

  • RCFILE:

    • 数据按行分块,每块按列存储,结合了行存储和列存储的优点

    • RCFile 保证同一行的数据位于同一节点,因此元组重构的开销很低

    • RCFile 能够利用列维度的数据压缩,并且能跳过不必要的列读取

  • ORCFile:

    • 存储方式:数据按行分块 每块按照列存储

    • 压缩快 快速列存取

    • 效率比rcfile高,是rcfile的改良版本 使用了索引

    • 使用ORC文件格式可以提高hive读、写和处理数据的能力

  • PARQUET:按列存储,相对于ORC,Parquet压缩比较低,查询效率较低

  • SequenceFile:

    • Hadoop API提供的一种二进制文件,以<key,value>的形式序列化到文件中

    • 存储方式:行存储

28、Kafka 消息数据积压,Kafka 消费能力不足怎么处理

  • 如果是 Kafka 消费能力不足,则可以考虑增加 Topic 的分区数,并且同时提升消费 组的消费者数量,消费者数=分区数

  • 如果是 Kafka 消费能力不足,则可以考虑增加 Topic 的分区数,并且同时提升消费 组的消费者数量,消费者数=分区数

29、Rowkey的设计原则

  • 唯一原则:必须在设计上保证其唯一性

  • 排序原则:Hbase的rowkey是按照ASCII有序设计的

  • 散列原则:所设计的Rowkey应该均匀的分布在Hbase的各个节点上

  • 长度原则:行键最大为64kb,一般开发应该设计为10-100byte

  • 数据的热点问题:大量的连续rowkey集中在某个region中,造成regionserver的负载过大

    • rowkey设计不合理

    • 没有提前创建分区,Hbase 创建表默认只有一个分区

    • 只有一个regionserver,然后所有的rowkey都往该region里面写数据。最后regionserver就会承受不了压力

  • 如何解决热点问题:

    • 加盐,在rowkey的前面增加随机数

    • 选部分rowkey进行hash,可以达到负载均衡

    • 反转固定长度或者数字格式的rowkey

30、Zookeeper的leader选举

  • ServerId(即Myid 服务器id)

  • ZXID(事务ID):对于来自客户端的每个更新请求,Zookeeper 都会分配一个全局唯一的递增 ID,这个 ID 反映了所有事务请求的先后顺序

  • Zookeeper的Leader选举主要发生在两种状态下:

    • 服务器初始化启动的时候完成Leader选举:

      • 只有集群超过半数的节点启动以后才可以进行Leader选举

      • 使用半选举机制的投票方式来选举Leader

      • 在选举投票时会优先比较ZXID,其次比较Myid,id大的优先选举为Leader

      • 统计投票,在每轮投票后,服务器都会统计投票信息,判断是否已经有某个机器接受到了过半机器的相同的投票信息,若有,则选举其为Leader

    • 服务器运行时期的Leader选举:

      • 当 Leader 服务器出现异常时,ZAB 协议就会进入恢复模式,通过过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出恢复模式,进入消息广播模式

  • CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾

31、Zookeeper的原理

  • Zookeeper 是一个开源的分布式协调服务,主要用于解决分布式环境下的服务协调

  • Zookeeper的所有更新和删除都是基于事务的

  • 集群中的角色:

    • Leader :为客户端提供读写服务,并维护集群状态,它是由集群选举所产生的

    • Follower :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态。同时也参与写操作“过半写成功”的策略和 Leader 的选举

    • Observer :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态,但不参与写操作“过半写成功”的策略和 Leader 的选举,因此 Observer 可以在不影响写性能的情况下提升集群的读性能

  • zookeeper中所有的事务请求都会由Leader服务器来处理:Zookeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据状态的变更以事务 Proposal 的形式广播到所有的副本进程上去

  • Zookeeper的特性:

    • 顺序性一致性:从一个客户端发起的事务请求,最终都会严格按照其发起顺序被应用到 Zookeeper中

    • 原子性:所有事务请求的处理结果在整个集群中所有机器上都是一致的;不存在部分机器应用了该事务,而另一部分没有应用的情况

    • 单一视图:所有客户端看到的服务端数据模型都是一致的

    • 可靠性:一旦服务端成功应用了一个事务,则其引起的改变会一直保留,直到被另外一个事务所更改

    • 实时性:一旦一个事务被成功应用后,Zookeeper 可以保证客户端立即可以读取到这个事务变更后的最新状态的数据

  • 会话:Zookeeper 客户端通过 TCP 长连接连接到服务集群,会话 (Session) 从第一次连接开始就已经建立,之后通过心跳检测机制来保持有效的会话状态。通过这个连接,客户端可以发送请求并接收响应,同时也可以接收到 Watch 事件的通知,关于会话中另外一个核心的概念是 sessionTimeOut(会话超时时间),当由于网络故障或者客户端主动断开等原因,导致连接断开,此时只要在会话超时时间之内重新建立连接,则之前创建的会话依然有效

  • Zookeeper 数据模型是由一系列基本数据单元 Znode(数据节点) 组成的节点树,其中根节点为 /。每个节点上都会保存自己的数据和节点信息。Zookeeper 中节点可以分为:

    • 持久节点

    • 有序持久节点

    • 临时节点

    • 有序临时节点

32、Spark提交任务时的参数

  • executor-cores:每个executor使用的内核数,默认为1

  • num-executors:启动executor的数量,默认为2

  • executor-memory:executor内存大小,默认为1G

  • driver-cores:driver使用内核数,默认为1

  • driver-memory:driver内存大小,默认为512M

33、Spark调优

  • 对多次复用的RDD进行持久化(cache presist checkpoint),使用presist时可以根据不同的业务场景选择不同的持久化方式

  • 使用Broadcast与map进行对join的代替(在某一个rdd数据量较小时使用)

  • 选择使用更合适的高性能算子:

    • 使用reduceByKey来代替groupByKey

    • 使用mapPartitions来代替map(使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常)

    • 通常对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),可以使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中

  • 对在算子函数中使用的外部变量(较大时)使用Broadcast进行广播来提高性能

  • 对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据

  • 尽可能的避免使用shuffle算子

  • 在提交spark程序时设置合适的资源参数(资源参数调优)

34、Spark中的缓存与checkpoint机制

  • 缓存:persistcachecache 内部调用的也是 persist,它是 persist 的特殊化形式

    • // 所有存储级别均定义在 StorageLevel 对象中 fileRDD.persist(StorageLevel.MEMORY_AND_DISK) fileRDD.cache()
    • Spark 速度非常快的一个原因是 RDD 支持缓存。成功缓存后,如果之后的操作使用到了该数据集,则直接从缓存中获取。虽然缓存也有丢失的风险,但是由于 RDD 之间的依赖关系,如果某个分区的缓存数据丢失,只需要重新计算该分区即可

  • checkpoint机制:

    • Checkpoint 的产生就是为了相对而言更加可靠的持久化数据,在 Checkpoint 可以指定把数据放在本地并且是多副本的方式,在正常生产环境下通常放在 HDFS`上,借助HDFS 高可靠的特征来实现更可靠的数据持久化

  • checkpoint的使用:

    scale> sc.setCheckpointDir(checkpointDir.toString) scale> val rdd = sc.makeRDD(1 to 20, numSlices = 1) scale> rdd.cache() scale> rdd.checkpoint()
    • rdd.cache()原因:在makeRDD计算完毕后,checkpoint会再次通过RunJob做一次计算,将每个partition数据保存到HDFS,这样RDD将会计算两次,所以为了避免此类情况,最好将RDD进行cache

  • SparkCore中的checkpoint的使用:

    • 什么时候写checkpoint数据:当RDD的action算子触发计算结束后会执行checkpoint

    • 什么时候读checkpoint数据:当RDD使用cache机制从内存中读取数据,如果数据没有读到,会使用checkpoint机制读取数据。此时如果没有checkpoint机制,那么就需要找到父RDD重新计算数据了,因此checkpoint是个很重要的容错机制

35、Spark数据倾斜

  • 数据倾斜:在shuffle过程中产生的数据倾斜(数据倾斜与数据过量不同,数据倾斜是某几个 task 处理的数据量很大,数据过量是所有 task 处理的数据量都很大)

  • 数据倾斜调优:

    • 过滤少量导致数据倾斜的key(如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话)

    • 提高shuffle操作的并行度(增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据)

    • 两阶段聚合 局部聚合+全局聚合(将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果)

    • join时使用广播变量Broadcast将较小数据量广播至每个Executor(相当于reduce join转为map join)

36、Kafka的分区消费

  • kafka中每个分区只有leader分区负责读写,follwer只负责备份

  • kafka会为每个分区维护一个ISR(可同步列表),用于维护所有同步的,可用的副本,ISR需满足:

    • 与 Zookeeper 之间有一个活跃的会话,即必须定时向 Zookeeper 发送心跳

    • 在规定的时间内从首领副本那里低延迟地获取过消息

  • 为了维护数据的一致性和有序性,所以kafka只支持从分区leader中进行读写,其余follwer只负责备份实现高可用

  • Kafka会在Zookeeper上针对每个Topic维护一个称为ISR(in-sync replica,已同步的副本)的集合,该集合中是一些分区的副本,如果某个分区的Leader不可用,Kafka就会从ISR集合中选择一个副本作为新的Leader

Redis部分

1、redis的五种基本的数据结构

  • string(字符串):是一种动态的字符串,为了做到对内存的极致优化,针对不同长度的字符串使用不同的结构体来表示

  • hash(字典hash):相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 "数组 + 链表" 的链地址法来解决部分 哈希冲突实际上字典结构的内部包含两个 hashtable,在扩容缩容时候使用渐进式搬迁

  • list(列表):相当于java中的 LinkedList,是链表,插入和删除非常快,但索引定位慢

  • Set(集合):相当于java中的HashSet,它内部的键值对是无序的,唯一的

  • ZSet(有序链表):它类似于 Java 中 SortedSetHashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重,它内部实现的数据结构为“跳跃表”

2、redis的持久化方式

  • 持久化主要是做灾难恢复,数据恢复

  • RDB持久化:不定期的通过异步方式将数据保存到磁盘上(半持久化模式)

  • AOF持久化:(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)

  • 两种持久化方式区别:

    • RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

    • AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

  • 两种方式的优缺点:

    • RDB的优势:

      • redis数据库的备份全部在一个文件中,易于进行灾难恢复

      • 性能更好,在开始持久化时只需要服务进程fork出一个子进程,由子进程来完成持久化工作

      • 相比于AOF,如果数据集很大,RDB的启动效率更高

    • RDB的劣势

      • 如果在定时持久化之前出现宕机,则此前还没有来得及写入磁盘的数据将会丢失

      • 由于RDB是通过fork子进程来协助完成数据持久化工作的,若数据集很大,则会导致整个服务器暂停服务

    • AOF的优势:

      • 更高的数据安全性,有每秒同步,每修改同步和不同步

      • 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创 建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性

      • AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作

    • AOF的0劣势:

      • 对于相同数量是数据集,AOF的文件通常要大于RDB的文件,所以RDB在恢复大数据集时比AOF快

      • AOF在运行效率上慢于RDB

    • 二者选取

      • 系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)

3、redis的主从同步

  • Redis主从同步:数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器

  • 工作原理Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步

  • 全量同步:

    • Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份

      • 从服务器连接主服务器,发送SYNC(同步命令)命令;

      • 主服务器接收到SYNC命名后,开始执行BGSAVE(fork出一个子进程生成RDB文件)命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;

      • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;

      • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;

      • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;

      • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

      • 完成从服务器的初始化,此时从服务器可以接收来自用户的读请求

  • 增量同步:

    • Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程

    • 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令

  • redis的主从同步策略

    • 主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。slave 在任何时候都可以发起全量同步。redis 策略是无论如何首先会尝试进行增量同步,如不成功,要求从服务器进行全量同步

    • 只要slave启动就会进行全量同步

4、redis的zset是怎么实现的

  • zset的内部实现使用的是“跳跃表”的数据结构

  • 跳跃表 skiplist 是受到多层链表结构的启发而设计出来的。按照生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)

  • skiplist的复杂度和红黑树一样,而且实现起来更简单

  • 在并发环境下skiplist有另外一个优势,红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分,而skiplist的操作显然更加局部性一些,锁需要盯住的节点更少,因此在这样的情况下性能好一些

5、redis中key的过期策略

  • redis对过期key有三种清除策略:

    • 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key

    • 定期删除:Redis会定期主动淘汰一批已过期的key(随机抽取一批key检查)

    • 内存淘汰机制:当前已用内存超过maxmemory限定时,触发主动清理策略

  • redis采用的策略是 惰性删除+定期删除

  • 过期key对RDB和AOF无任何影响

6、redis如何保证高可用

  • redis哨兵机制(哨兵机制需要主从复制的支持)

    • Redis的哨兵(sentinel) 系统用于管理多个 Redis 服务器,该系统执行以下三个任务:

    • 监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。

    • 提醒(Notification):当被监控的某个Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。

    • 自动故障迁移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master; 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master。

  • 哨兵机制工作流程:

    • 哨兵(sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master.

    • 每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown).

    • 若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置.

    • 虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel)

    • 哨兵(sentinel) 的一些设计思路和zookeeper非常类似(哨兵机制是redis的自带功能)

  • Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

  • Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

7、redis为什么快

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速

  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的

  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

  • 使用多路I/O复用模型,非阻塞IO;

    • 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。

    • 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

8、redis为什么是单线程的

  • 因为redis是基于内存操作的,cpu不是redis的瓶颈,而redis的瓶颈最有可能使机器内存和网络带宽,也可以通过单机开多个redis实例来发挥cpu的多核性能

  • 这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的

9、redis的内存淘汰策略

  • redis共有6种内存淘汰策略方案:

    • volatile-lru:从设置过期时间的数据集中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失

    • volatile-ttl:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。

    • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。

    • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。

    • allkeys-random:从数据集中选择任意数据淘汰。

    • no-enviction:对于写请求不再提供服务,也就是当内存不足以容纳新入数据时,新写入操作就会报错,这也是系统默认的一种淘汰策略。

  • LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

  • TTL:Redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。与 LRU 数据淘汰机制类似,TTL 数据淘汰机制中会先从过期时间的表中随机挑选几个键值对,取出其中 ttl 大的键值对淘汰。同样,TTL淘汰策略并不是面向所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对

10、MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据

  • 可以使用volatile-lru内存淘汰策略,从已设置过期时间的数据集中淘汰最近最少使用的数据

11、redis内存优化

  • 做内存优化首先要了解redisObject对象:

    • Redis存储的数据都使用redisObject来封装,包括string,hash,list,set,zset在内的所有数据类型

    • type字段:表示当前对象使用的数据类型,Redis主要支持5种数据类型:string,hash,list,set,zset。可以使用type {key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。

    • encoding字段:表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要 ,同一个对象采用不同的编码实现内存占用存在明显差异,具体细节见之后编码优化部分。

    • lru字段:记录对象最后一次被访问的时间,当配置了 maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 时, 用于辅助LRU算法删除键数据。可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间。

    • refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount {key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。具体细节见之后共享对象池部分。

    • *ptr字段:与对象的数据内容相关,如果是整数直接存储数据,否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作。

  • 内存优化的几种方式:

    • 缩减键和值的长度

    • 共享对象池:指Redis内部维护[0-9999]的整数对象池,因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

    • 字符串优化

    • 编码优化:redis内部针对不同的类型存在不同的编码,编码是指底层具体由哪种数据实现

    • 控制key的数量

12、redis中缓存异常

  • 缓存雪崩:redis中大量数据同时失效

    • 解决办法:在缓存的时候给过期时间加上一个随机值

  • 缓存穿透:数据在redis不存在,数据库也不存在,返回空,一般来说空值是不会写入redis的,如果反复请求同一条数据,那么则会发生缓存穿透。

    • 解决办法:可以使用布隆过滤器(存在一定的误判率) 或者找不到时将这个空对象加入缓存中(设置一个较短的过期时间)

  • 缓存击穿:一个很热门的数据突然失效,大量请求到数据库中去

    • 解决方法:设置热点数据永不过期

13、redis中事务

  • redis中事务的概念:

    • redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令

  • 单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

  • 事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

  • redis中事务的三个阶段:

    • 开始事务

    • 命令入队

    • 执行事务

  • redis事务并不支持回滚,但是取消事务后事务上下文中的命令都不会执行

  • redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式,可以保证命令执行过程中不会被其他客户端命令打断

14、redis集群会有写操作丢失吗

  • Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作

    • 过期 key 被清理

    • 最大内存不足,导致 Redis 自动清理部分 key 以节省空间

    • 主库故障后自动重启,从库自动同步

    • 单独的主备方案,网络不稳定触发哨兵的自动切换主从节点,切换期间会有数据丢失

15、redis哈希槽的概念

  • Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽

  • redis集群之间是通过异步复制的

  • 最大节点个数:Redis集群预分好16384个桶(哈希槽)

16、redis分区

  • 分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集

  • 分区的优势:

    • 通过利用多台计算机内存的和值,允许我们构造更大的数据库

    • 通过多核和多台计算机,允许我们扩展计算能力;通过多台计算机和网络适配器,允许我们扩展网络带宽

  • 分区的缺点:

    • 涉及多个key的操作通常是不被支持的。举例来说,当两个set映射到不同的redis实例上时,你就不能对这两个set执行交集操作

    • 涉及多个key的redis事务不能使用

    • 当使用分区时,数据处理较为复杂,比如你需要处理多个rdb/aof文件,并且从多个实例和主机备份持久化文件

  • 分区的实现方案

    • 客户端分区:就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取

    • 代理分区: 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据

    • 查询路由(Query routing) :意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。

17、RedLock

  • RedLock:基于redis实现的分布式锁,具有以下特性

    • 安全特性:互斥访问,即永远只有一个 client 能拿到锁

    • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况

    • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

  • 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)

18、Redis与Memcached的区别

  • Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作

  • 使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached

  • 存储数据安全--memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化)

  • 灾难恢复--memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复;

  • 在存储大量数据时Memcached的性能优于redis

19、关于redis的补充

  • 高并发下的双写一致性:

    • 先删除缓存再更新数据库

    • 先更新数据库再删除缓存

    • 删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算(因为缓存可能涉及到多个表),不管它会不会用到,而是让它到需要被使用的时候再重新计算

  • redis插入大量数据:

    • 使用luke协议,而且从Redis 2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作

  • redis的内部编码:

    • 可以改进内部编码,而对外的数据结构和命令没有影响

    • 多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但在列表元素较多的情况下性能有所下降,这时 Redis 会根据配置选项将列表类型的内部实现转换为 linkedlist

20、Redis实现分布式锁

  • 可以使用set来进行实现:

    • jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参

    • 第一个为key,我们使用key来当锁,因为key是唯一的

    • 第二个为value,我们传的是requestId,原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成

    • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作

    • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定

    • 第五个为time,与第四个参数相呼应,代表key的过期时间

  • 为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功

21、布隆过滤器

  • 布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难

  • 布隆过滤器的原理(基本思想)是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在

  • Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率

  • 布隆过滤器缺点:

    • 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素

    • 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断

  • 常见的几个应用场景:

    • 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤

    • 垃圾邮件过滤

线程部分

1、线程的状态及状态之间的转换

  • 在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)5种状态

    • 新建状态,当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值

    • 就绪状态,当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行

    • 运行状态,如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

    • 阻塞状态,当处于运行状态的线程失去所占用资源之后,便进入阻塞状态

      • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中

      • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中

      • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

    • 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生

2、线程池的使用

  • 什么是线程池:创建若干个可执行的线程放入一个池中,在有任务需要处理时,会将其提交到线程池中,线程池分配线程去执行任务,且在执行完任务之后线程不会销毁,而是归还至线程池中

  • /*  corePoolSize(核心线程数)  maximumPoolSize(线程池最大大小)  keepAliveTime 空闲线程存活时间  TimeUnit 空闲线程存活的时间单位  workQueue 工作队列 新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务  threadFactory 线程工厂  handler 拒绝策略  */ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,10,1000, TimeUnit.HOURS,new ArrayBlockingQueue<Runnable>(10)); /*  其他线程池:   newFixedThreadPool(int nThreads) 创建固定大小的线程池   newSingleThreadExecutor() 创建只有一个线程的线程池   newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行     阻塞队列:   无界队列:常用的为无界的LinkedBlockingQueue,队列大小无限制,可能会导致cpu和内存飙升服务器挂掉   有界队列:ArrayBlockingQueue,遵循FIFO原则的队列     PriorityBlockingQueue,优先级队列 */

3、进程和线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

  • 资源开销每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数(PC),线程之间切换的开销小

  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

  • 内存分配同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮

  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

  • 从jvm角度来说:一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈本地方法栈

4、进程间的通信

  • 管道:管道可以分为两类:匿名管道和命名管道

  • 消息队列:消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法

  • 共享内存:两个进程各自拿出一块虚拟地址空间来,映射到相同的物理内存中,就完成了内存共享机制

  • socket:通过socket套接字进行通信

5、Java有哪些锁

  • 公平锁/非公平锁

  • 可重入锁

  • 独享锁/共享锁

  • 乐观锁/悲观锁

  • 分段锁

  • 偏向锁/轻量级锁/重量级锁

    • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁

    • 轻量级锁是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

    • 重量级锁:等待锁的线程都会进入阻塞状态

  • 自旋锁

    • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

      在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

      而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁

  • 死锁

    • 当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去。这种就是最简答的死锁形式(或者叫做"抱死")

6、volatile

  • volatile定义:

    • Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量,如果一个字段被申明为volatile,线程内存模型确保所有线程能够看到的这个变量的值是一致的

    • 被volatile修饰的变量发生读写操作,就会发生两个动作:

      • 1,将当前处理器缓存行的数据写到系统内存中(这是Lock前缀指令会引起的)

      • 2,然后这个操作使得其他CPU缓存了该内存地址的数据无效

      • 当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中,然后在多处理器下,会实现缓存一致性协议

7、线程池的饱和拒绝策略

  • 四种线程池拒绝策略

    •  ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。(默认拒绝策略)  ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务  ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 

8、同步/异步/阻塞/非阻塞 IO 的区别

  • 同步和异步是通信机制,阻塞和非阻塞式调用状态

  • 同步 IO 是用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行。异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程,或调用用户线程注册的回调函数

  • 阻塞 IO 是 IO 操作需要彻底完成后才能返回用户空间 。非阻塞 IO 是 IO 操作调用后立即返回一个状态值,无需等 IO 操作彻底完成

9、线程间的通信

  • 使用线程间的共享内存进行通信(volatile)

  • 等待通知机制:Object类提供了线程间通信的方法:wait()notify()notifyaAl(),它们是多线程通信的基础(wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁)

  • 管道 IO 流用于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者

  • 信号量机制

10、线程的创建以及方法

  • 线程的创建方式:

    • 继承 Thread 类并重写 run 方法。实现简单,但不符合里氏替换原则,不可以继承其他类

    • 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦

    • 实现 Callable 接口并重写 call 方法。可以获取线程执行结果的返回值,并且可以抛出异常

  • 线程的方法:

    • sleep 方法导致当前线程进入休眠状态,与 wait 不同的是该方法不会释放锁资源,进入的是 TIMED-WAITING 状态

    • yiled 方法使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到 RUNNABLE 状态,与其他线程一起重新竞争CPU时间片

    • join 方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁

  • sleep与wait的区别:

    • sleep()与yiled()属于Thread类,而wait和notify属于Object类

    • sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法

    • sleep可以在任何地方使用,而wait只能在同步方法和同步块中使用

    • 使用wait后需要使用notify去重新唤醒线程(不确定是哪个)

  • 守护线程:守护线程是一种支持型线程,可以通过 setDaemon(true) 将线程设置为守护线程,但必须在线程启动前设置

11、线程池的好处

  • 线程池作用:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间

  • 降低资源消耗,复用已创建的线程,降低开销、控制最大并发数

  • 隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响

  • 实现任务线程队列缓冲策略和拒绝机制

  • 实现某些与时间相关的功能,如定时执行、周期执行等

12、线程池处理任务的流程

  • 核心线程池未满,创建一个新的线程执行任务,此时 workCount < corePoolSize

  • 如果核心线程池已满,工作队列未满,将线程存储在工作队列,此时 workCount >= corePoolSize

  • 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务,此时 workCount < maximumPoolSize,这一步也需要获取全局锁

  • 如果超过大小线程数,按照拒绝策略来处理任务,此时 workCount > maximumPoolSize

  • 线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行

  • ThreadLoacl :是线程共享变量,主要用于一个线程内跨类、方法传递数据

13、多线程

  • 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程

  • 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销

  • 多核时代多线程主要是为了提高CPU的利用率

  • 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换

  • 并行:单位时间段内,多个任务同时执行

  • 并发:同一时间段内,多个任务都在执行

  • 并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等

14、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法

  • new 一个 Thread,线程进入了新建状态;调用 start() 方***启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方***把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作

  • 总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行

15、Synchronized

  • 关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)

  • synchronized关键字的使用方式:

    • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    • 修饰静态方法:访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

    • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

  • synchronized 关键字底层原理属于 JVM 层面

  • sychronized和ReentrantLock的区别:

    • 两者都是可重入锁

    • synchronized 是依赖于 JVM 实现的,ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)

  • ReentrantLock 的一些高级功能:

    • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情

    • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的

    • 实现选择性通知(锁可以绑定多个条件)

集合部分

集合部分我看的是GitHub上的JavaGuide作者写的!!!因为没有写可否转载,所以大家可以自行搜索查看

JVM部分

1、GC相关

  • GC的目的:回收堆内存中不再使用的对象,释放资源

  • GC优化的目的:

    • 将转移到老年代的对象数量降低到最小

    • 减少full GC的执行时间

  • GC优化方式:

    • 减少使用全局变量和大对象

    • 调整新生代的大小到最合适

    • 设置老年代的大小为最合适

    • 选择合适的GC收集器

  • cms回收器和g1回收器:

    • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担。CMS无法处理浮动垃圾。CMS的“标记-清除”算***导致大量空间碎片的产生

      • CMS是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合

    • G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

      • G1将堆空间划分成了互相独立的区块,可以用相对较少的时间优先回收包含垃圾最多区块

  • 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。 Major GC 是清理永久代。Full GC 是清理整个堆空间—包括年轻代和永久代

  • MinorGC(次要的) 是发生在新生代的GC, 采用了复制算法

    • 复制算法将可用内存按容量划分为相等的两部分, 然后每次只使用其中的一块, 当一块内存用完时, 就将还存活的对象复制到第二块内存上, 然后一次性清楚完第一块内存, 再将第二块上的对象复制到第一块. 但是这种方式内存的代价太高, 每次基本上都要浪费一半的内存

  • MajorGC(重要的) 是发生在老年代的GC, 一般也伴随着MinorGC, 它采用了标记-清除算法, 速度比Minor GC慢10倍以上

    • 标记哪些要被回收的对象, 然后统一回收. 这种方法很简单, 但是会有两个主要问题:1.效率不高, 标记和清除的效率都很低;2.会产生大量不连续的内存碎片, 导致以后程序在分配较大的对象时, 由于没有充足的连续内存而提前触发一次GC动作

  • Full GC触发条件:老年代满、方法区满、内存碎片过多无法分配当前对象

2、JVM内存模型


  • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存(几乎所有!!!大家可以去看一下特例),jvm只有一个堆区(heap)被所有线程共享,堆区中不存放基本类型和对象引用,只存放对象本身。 堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。

  • 虚拟机栈(jvm栈):虚拟机栈描述的是Java方法执行的内存模型每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期,而每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息,每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程

  • 本地方法栈:本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法

  • 方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等数据

  • 程序计数器:是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器

    • 字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。是唯一在虚拟机规范中没有规定内存溢出情况的区域

  • 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

  • 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的

3、双亲委派模型

  • 类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器

  • 一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试

  • 类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性

4、GC优化

  • 对象优先在 Eden 区分配

    • 大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC

  • 大对象直接进入老年代

  • 长期存活对象进入老年代

  • 动态对象年龄判定

    • 为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代

5、运行时数据区

  • 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干不同的数据区,这些区域有各自的用途、创建和销毁时间

  • 线程私有:程序计数器、Java虚拟机栈、本地方法栈

  • 线程共享:Java堆、方法区

  • 运行时常量池:

    • 运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池

    • 运行时常量池相对于 Class 文件常量池的一个重要特征是动态性,Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是 String 的 intern 方法

6、JVM

  • JVM(Java Virtual Machine)是用于运行Java字节码文件的虚拟机,JVM运行在操作系统之上,不与硬件设备直接交互

  • Java虚拟机包括一个类加载子系统、运行时数据区、执行引擎和本地接口库

  • jdk1.8中为什么要移除永久代:

    • 永久代内存经常不够用或发生内存泄露

    • 对永久代的调优过程非常困难

7、GC加强

  • Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收

  • Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等

  • 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中

  • 判断对象是否死亡的方法:

    • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的

      • 无法解决 对象间相互循环引用 的问题

    • 可达性分析:过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的

      • 可做为GC Roots的对象:1.Java虚拟机栈(栈帧的本地变量表)中引用的对象 2.本地方法栈 中 JNI引用对象 3.方法区 中常量、类静态属性引用的对象

      • 当一个对象到 GC Roots 没有任何引用链相连时,则判断该对象不可达

    • 注意:

      • 可达性分析 仅仅只是判断对象是否可达,但还不足以判断对象是否存活 / 死亡

      • 当在 可达性分析 中判断不可达的对象,只是“被判刑” = 还没真正死亡

      • 不可达对象会被放在”即将回收“的集合里

        • 要判断一个对象真正死亡,还需要经历两个阶段:

          1. 第一次标记 & 筛选

          2. 第二次标记 & 筛选

  • 四种引用分为:强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

  • 垃圾收集算法:标记-清除算法 复制算法 标记整理算法 分代收集算法

  • CMS收集器:

    • CMS(Concurrent Mark Sweep)收集器是一种 以获取最短回收停顿时间 为目标的收集器。它非常符合在注重用户体验的应用上使用

    • CMS 收集器是一种 “标记-清除”算法实现的

    • 主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

      • 对 CPU 资源敏感

      • 无法处理浮动垃圾

      • 它使用的“标记-清除算法”在收集结束时会产生大量的空间碎片

  • G1收集器:

    • G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征

    • G1收集器的特点:

      • 并行与并发

      • 分代收集

      • 空间整合

      • 可预测的停顿

    • G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)


Mysql部分

1、数据库隔离级别

  • 隔离级别有四种

    • 读未提交:是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。会出现脏读,幻读,不可重复读

    • 读已提交:是指一个事务只能读取另一个事务已经提交的数据,不能读取未提交的数据。克服了脏读,但会出现不可重复读现象,幻读

    • 可重复读:可重复读解决了不可重复读的问题,保证了在同一个事务中多次读取同样的记录结果一致。但还是无法解决幻读,所谓幻读指的是当某个事务在读取某个范围内的记录时,会产生幻行。InnoDB 存储引擎通过多版本并发控制MVCC 解决幻读的问题

    • 可串行化:最高的隔离级别,通过强制事务串行执行,避免幻读。可串行化会在读取的每一行数据上都加锁,可能导致大量的超时和锁争用的问题

    • MySQL默认的隔离级别是可重复读

2、不可重复读与幻读区别

  • 不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

  • 脏读: 当一个事务的操作正在多次修改数据,而在事务还未提交的时候,另外一个并发事务来读取了数据,就会导致读取到的数据并非是最终持久化之后的数据,这个数据就是脏读的数据

  • 不可重复读:指在一个事务A内,多次读同一个数据,但是事务A没有结束时,另外一个事务B也访问该同一数据。那么在事务A的两次读数据之间,由于事务B的修改导致事务A两次读到的数据可能是不一样的。这就发生了在一个事务内两次读到的数据不一样,这就被称作不可重复读

    • 与脏读的区别:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但实际上是违反了事务的一致性原则

  • 幻读:指同一个事务内多次查询返回的结果集不一样(比如增加了或者减少了行记录)。比如同一个事务 A 内第一次查询时候有 n 条记录,但是第二次同等条件下查询却又 n+1 条记录,这就好像产生了幻觉

  • mysql解决幻读:

    • InnoDB在RR隔离级别下可以解决幻读

3、MySql的存储引擎以及各自优点

  • MySQL支持InnoDB、MyISAM、MEMORY等存储引擎。

  • InnoDB引擎(MySQL5.5以后默认使用):

    • 灾难恢复性好

    • 支持事务

    • 使用行级锁和表级锁,能支持更多的并发量

    • 查询不加锁

    • 支持外键关联

    • 支持热备份

    • 实现缓冲管理

  • MyISAM引擎:

    • 不支持事务

    • 使用表级锁,并发性差

    • 主机宕机后,MyISAM表易损坏,灾难恢复性不佳

    • 可以配合锁,实现操作系统下的复制备份、迁移

    • 只缓存索引

    • 数据紧凑存储,因此可获得更小的索引和更快的全表扫描性能

  • 两者主要区别:

    • InnoDB支持事务,MyISAM不支持事务处理等高级处理

    • InnoDB支持行级锁,而MyISAM仅支持表级锁

    • MyISAM类型的表强调的是性能,其执行速度比InnoDB类型更快

    • MyISAM适合查询以及插入为主的应用,InnoDB适合频繁修改以及涉及到安全性较高的应用

    • InnoDB支持外键,MyISAM不支持

    • MyISAM支持全文搜索,而InnoDB 1.2.x版本后才支持。

    • 对于自增长的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中可以和其他字段一起建立联合索引

  • MyIASM为什么比InnoDB快:

    • InnoDB 的表是根据主键进行展开的 B+tree 的聚集索引。MyIsam 则非聚集型索引,myisam 存储会有两个文件,一个是索引文件,另外一个是数据文件,其中索引文件中的索引指向数据文件中的表数据

    • INNODB在做SELECT的时候,要维护的东西比MYISAM引擎多

      • INNODB要缓存数据块,MYISAM只缓存索引块

      • innodb寻址要映射到块,再到行,MYISAM记录的直接是文件的OFFSET,定位比INNODB要快

      • INNODB还需要维护MVCC一致

  • MyISAM和InnoDB都使用B+树来实现索引

    • MyISAM的索引与数据分开存储,MyISAM的索引叶子存储指针,主键索引与普通索引无太大区别

    • InnoDB的聚集索引和数据行统一存储,InnoDB的聚集索引存储数据行本身,普通索引存储主键

    • InnoDB采用聚簇索引,而MyISAM采用非聚簇索引

4、乐观锁与悲观锁

  • 乐观锁:是指一种不使用数据库锁和不阻塞线程并发的思路。它的特点是先进行业务操作,不到万不得已不去拿“锁”。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。乐观锁在数据库的实现完全是逻辑的,不需要数据库提供特殊的支持,一般的做法是在需要锁的数据上增加一个版本号,或者时间戳

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

  • 两种锁的使用场景:从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

5、高并发下如何做到安全的修改同一行数据

  • 使用悲观锁。本质是当前只有一个线程执行操作,排斥外部请求的修改。遇到加锁的状态,就必须等待。结束了唤醒其他线程进行处理。但是,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。

  • FIFO(先进先出)缓存队列思路,直接将请求放入队列中,这样就不会导致某些请求永远获取不到锁。有点强行把多线程变成单线程的感觉。

  • 使用乐观锁。相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。

6、MySQL的索引原理,索引的类型有哪些

  • 索引是什么:索引也叫键,是一种用来加快查询速度的数据结构

  • 在MySQL中,首先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。索引可以包括一个或多个列的值,如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能使用索引的最左前缀

  • mysql中索引采用的数据结构主要有B+Tree、Hash、平衡二叉树等

  • 索引的类型可以分为:

    • 普通索引(INDEX),最基本的索引,没有任何的约束。INDEX index_name (name)

    • 唯一索引(UNIQUE),与普通索引类似,但索引列的值必须唯一,但允许有控制(注意和主键不同)。如果是组合索引,则列值的组合必须唯一,创建方法和普通索引类似。UNIQUE index_name (name)

    • 全文索引( FULLTEXT ),MyISAM 表全系支持,InnoDB 1.2.x后支持。FULLTEXT (content)

    • 主键索引(PRIMARY KEY),特殊的唯一索引,一个表只能有一个,不允许有空值。

    • 复合索引,将多个列组合在一起创建索引,可以覆盖多个列。

  • 索引优化:

    • 区分度高,离散度大,作为索引的字段值尽量不要有大量相同值

    • 索引的长度不要太长(比较耗费时间)

  • 聚集索引:

    • 聚集索引即索引结构和数据一起存放的索引,主键索引属于聚集索引

    • 在 Mysql 中,InnoDB引擎的表的 .ibd文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。

    • 优点:聚集索引的查询速度非常的快,因为整个B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。

    • 缺点:

      • 依赖于有序的数据 :因为B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或UUID这种又长又难比较的数据,插入或查找的速度肯定比较慢。

      • 更新代价大 : 如果对索引列的数据被修改时,那么对应的索引也将会被修改, 而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的, 所以对于主键索引来说,主键一般都是不可被修改的。

  • 非聚集索引:

    • 非聚集索引即索引结构和数据分开存放的索引。

    • MYISAM引擎的表的.MYI文件包含了表的索引, 该表的索引(B+树)的每个非叶子节点存储索引, 叶子节点存储索引和索引对应数据的指针,指向.MYD文件的数据。

    • 优点:更新代价比聚集索引要小 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的

    • 缺点:

      • 跟聚集索引一样,非聚集索引也依赖于有序的数据

      • 可能会二次查询(回表) :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

  • B树索引:

    • B-Tree 通常意味着所有的值都是按顺序存储的,并且每个叶子页到根的距离相同

    • B-Tree 索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索

    • 通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限

  • Hash索引:

    • 哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效

    • 对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针

    • 只有 Memory 引擎显式支持哈希索引,这也是 Memory 引擎的默认索引类型

    • 因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这让哈希索引的速度非常快

  • B树和B+树区别

    • B树的所有节点既存放 键(key) 也存放 数据(data);而B+树只有叶子节点存放 key 和 data,其他内节点只存放key

    • B树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。

    • B树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。

  • Hash索引和 B+树索引优劣分析

    • 对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

    • Hash索引定位快,Hash索引指的就是Hash表,最大的优点就是能够在很短的时间内,根据Hash函数定位到数据所在的位置,这是B+树所不能比的

    • Hash索引不支持顺序和范围查询是它最大的缺点

    • B+树是有序的,在这种范围查询中,优势非常大,直接遍历比500小的叶子节点就够了

  • B+树做索引而不用B树:

    • 那么Mysql如何衡量查询效率呢?– 磁盘IO次数。

    • 一般来说索引非常大,尤其是关系型数据库这种数据量大的索引能达到亿级别,所以为了减少内存的占用,索引也会被存储在磁盘上。B-树/B+树 的特点就是每层节点数目非常多,层数很少,目的就是为了减少磁盘IO次数,但是B-树的每个节点都有data域(指针),这无疑增大了节点大小,说白了增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时),而B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少。

      • 优点一: B+树只有叶节点存放数据,其余节点用来索引,而B-树是每个索引节点都会有Data域。

      • 优点二: B+树所有的Data域在叶子节点,并且所有叶子节点之间都有一个链指针。 这样遍历叶子节点就能获得全部数据,这样就能进行区间访问啦。在数据库中基于范围的查询是非常频繁的,而B树不支持这样的遍历操作。

  • B+树做索引而不用红黑树:

    • AVL 树(平衡二叉树)和红黑树(二叉查找树)基本都是存储在内存中才会使用的数据结构。在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。

  • 索引什么时候会失效:

    • 如果条件中有or,即使其中有条件带索引也不会使用(这就是为什么尽量少使用or的原因)(注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引)

    • like查询是以%开头

    • 如果列类型是字符串,那一定要在条件中使用引号引起来,否则不会使用索引

    • 如果mysql估计使用全表扫描比使用索引快,则不使用索引

7、数据库三大范式

  • 第一范式:数据表中的每一列(字段),必须是不可拆分的最小单元,也就是确保每一列的原子性

  • 第二范式:满足第一范式的关系模式后,要求表中所有的列,都必须依赖于主键,而不能有任何一列和主键没有关系,也就是说一个表只描述一件事

  • 第三范式:首先必须先满足第二范式,表中的每一列只能与主键直接相关而不是间接相关

  • 浅拷贝:只是增加了一个指针指向已存在的内存地址

  • 深拷贝:是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存

8、MySQL 的锁策略有什么

  • 表锁是MySQL中最基本的锁策略,并且是开销最小的策略。表锁会锁定整张表,一个用户在对表进行写操作前需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获取读锁,读锁之间不相互阻塞

  • 行锁可以最大程度地支持并发,同时也带来了最大开销。InnoDB 和 XtraDB 以及一些其他存储引擎实现了行锁。行锁只在存储引擎层实现,而服务器层没有实现

9、数据库死锁

  • 死锁是指多个事务在同一资源上相互占用并请求锁定对方占用的资源而导致恶性循环的现象。当多个事务试图以不同顺序锁定资源时就可能会产生死锁,多个事务同时锁定同一个资源时也会产生死锁

  • 解决办法

    • 为了解决死锁问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,例如InnoDB 存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误

    • 当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB 目前处理死锁的方法是将持有最少行级排它锁的事务进行回滚

    • 死锁发生之后,只有部分或者完全回滚其中一个事务,才能打破死锁

  • 死锁产生的四个条件:

    • 互斥(Mutual exclusion):进程对所分配到的资源进行排它性使用,在一段时间内某资源只由一个进程占用

    • 持有并等待(Hold and wait):指某个进程已经持有了一个或多个资源,但是还要请求其他资源,而它请求的资源不能立即获得,需要等待

    • 不可抢占(No preemption):即进程已经获取的资源在使用过程中不能被其他进程抢占,只能在使用完后,由该进程自己释放

    • 环路等待(Circular wait):即形成进程和请求资源之间的环路

10、MVCC 是什么

  • InnoDB 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别

  • MVCC 是多版本并发控制,在很多情况下避免加锁,大都实现了非阻塞的读操作,写操作也只锁定必要的行

  • MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据是一致的。根据事务开始的时间不同,每个事物对同一张表,同一时刻看到的数据可能是不一样的

  • InnoDB 的MVCC 通过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的创建时间,一个保存行的过期时间

  • 对数据库的任何修改的提交都不会直接覆盖之前的数据,而是产生一个新的版本与老版本共存,使得读取时可以完全不加锁。这样读某一个数据时,事务可以根据隔离级别选择要读取哪个版本的数据。过程中完全不需要加锁

  • MVCC对数据库隔离级别的实现:

    • Read Committed - 一个事务读取数据时总是读这个数据最近一次被commit的版本

    • Repeatable Read - 一个事务读取数据时总是读取当前事务开始之前最后一次被commit的版本(所以底层实现时需要比较当前事务和数据被commit的版本号)。

11、MySql主从复制

  • 复制解决的基本问题是让一台服务器的数据与其他服务器保持同步,一台主库的数据可以同步到多台备库上,备库本身也可以被配置成另外一台服务器的主库。主库和备库之间可以有多种不同的组合方式

  • MySQL 支持两种复制方式:基于行的复制和基于语句的复制

  • MySQL 复制大部分是向后兼容的

  • 复制解决的问题:数据分布、负载均衡、备份、高可用性和故障切换、MySQL 升级测试

12、MySql的读写锁

  • 写锁:写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,确保在给定时间内只有一个用户能执行写入并防止其他用户读取正在写入的同一资源

  • 读锁:读锁是共享的,相互不阻塞,多个客户在同一时刻可以同时读取同一个资源而不相互干扰

13、事务

  • 事务是什么:

    • 事务是一组原子性的 SQL 查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说事务内的语句要么全部执行成功,要么全部执行失败

  • 事务的特性:

    • 原子性atomicity

      • 一个事务在逻辑上是必须不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说不可能只执行其中的一部分

    • 一致性 consistency

      • 数据库总是从一个一致性状态变为另一个一致性状态

    • 隔离性 isolation

      • 针对并发事务而言,隔离性就是要隔离并发运行的多个事务之间的相互影响,一般来说一个事务所做的修改在最终提交以前,对其他事务是不可见的

    • 持久性 durability

      • 一旦事务提交成功,其修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失

14、查询的执行流程

  • 客户端发送一条查询给服务器

  • 服务器先检查查询缓存,如果命中了缓存则立刻返回存储在缓存中的结果,否则进入下一阶段

  • 服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划

  • MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询

  • 将结果返回给客户端

15、如何定位低效SQL

  • 一种是通过慢查询日志定位,可以通过慢查询日志定位那些已经执行完毕的 SQL 语句

  • 另一种是使用 SHOW PROCESSLIST 查询,慢查询日志在查询结束以后才记录,所以在应用反应执行效率出现问题的时候查询慢查询日志不能定位问题,此时可以使用 SHOW PROCESSLIST 命令查看当前 MySQL 正在进行的线程,包括线程的状态、是否锁表等,可以实时查看 SQL 的执行情况,同时对一些锁表操作进行优化

  • 通过 SHOW PROFILE 可以分析 SQL 语句性能消耗,例如查询到 SQL 会执行多少时间,并显示 CPU、内存使用量,执行过程中系统锁及表锁的花费时间等信息

16、SQL优化策略

  • 避免全表扫描:

    • 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where (避免全表扫描)及 order by(提高排序速度) 涉及的列上建立索引

      • 在MySQL中,支持两种排序方式:FileSort和Index排序,并且index排序的效率更高。

        Index排序:索引可以保证数据的有序性,因此不需要再进行排序。

        FileSort排序:一般在内存中进行排序,占用CPU较多。如果待排结果较大,会产生临时文件I/O到磁盘进行排序,效率较低

  • 避免不等值判断:

    • 应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描

  • 避免使用or逻辑:

    • 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描

  • 避免查询条件中字段计算:

    • 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描

  • 慎用in和not in逻辑:

    • in和not in的使用也会导致全表扫描

  • 选择合适的列建立索引

17、Mysql语法问题

  • where与having区别:

    • where是数据从磁盘读入内存的时候进行判断, 而having是磁盘读入内存后再判断

    • where不可以使用字段的别名,而having可以使用

    • having能够使用聚合函数当做条件,但是where不能使用,where只能使用存在的列当做条件

  • 索引建立原则:

    • 对查询频率高的字段创建索引

    • 对排序、分组、联合查询频率高的字段创建索引

    • 尽量使用数据量少的索引

    • 尽量使用前缀来索引

    • 索引的数量不应过多

  • 索引类型:

    • 主键索引:一种特殊的唯一索引,不允许有空值

    • 唯一索引:与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值

    • 普通索引:最基本的索引,没有任何限制

    • 全文索引:仅可用于 MyISAM 表, 用于在一篇文章中,检索文本信息的

    • 复合索引:为了更多的提高mysql效率可建立组合索引,遵循”最左前缀“原则

  • 最左前缀:就是最左优先,创建lname_fname_age多列索引,相当于创建了(lname)单列索引,(lname,fname)组合索引以及(lname,fname,age)组合索引,MySQL会一直向右匹配直到遇到范围查询 (>,<,BETWEEN,LIKE)就停止匹配

18、Mysql的行锁与表锁

  • InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则都会从行锁升级为表锁

  • InnoDB 自动给修改操作加锁,给查询操作不自动加锁

  • 使用行锁,锁冲突概率低,并发性高

  • 表锁响应的是非索引字段,即全表扫描,全表扫描时锁定整张表

19、视图

  • Mysql中视图的两个概念:

    • 一个是view,它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view … ,而它的查询方法与表一样

    • 另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现

  • 可重复读隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图

  • 读提交隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的,在这个隔离级别下,事务在每次查询开始时都会生成一个独立的ReadView

  • 可重复读,在第一次读取数据时生成一个ReadView,对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的

  • 这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,数据的可重复读其实就是ReadView的重复使用

20、Sql语句执行顺序

  • from >> join >> on >> where >> group by >> avg,sum... >> having >> select >> distinct >>order by

计算机网络部分

1、在浏览器输入URL,按下回车会经历哪些流程

  • 首先DNS解析解析域名,得到IP地址:

    • DNS解析流程:

      1.浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束

      2.如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址

      3.本地的DNS服务器向根域名服务器发送查询请求,根域名服务器返回该域名的一级域名服务器

      4.该本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址

  • 解析出IP地址后,根据IP地址和默认端口80和服务器建立连接

  • 浏览器发出读取文件(URL中域名后边部分对应的文件)的HTTP请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器

  • 服务器对浏览器的请求作出响应,并把对应的html文本发送给浏览器

  • 释放TCP连接(四次挥手断开连接)

  • 浏览器解析该HTML文本并显示内容

  • 过程中用到的协议以及作用:

    • 域名解析用到DNS协议

    • DNS服务器是基于UDP的,因此会用到UDP协议

    • 得到IP地址后,浏览器会与服务器建立HTTP连接,用到HTTP协议

    • http协议生成GET请求报文,将该报文传给TCP层处理,用到了TCP协议

    • 如果用到了HTTPS协议还会对HTTP协议内容进行加密

    • TCP层若有需要会对HTTP数据报分片,分片依据路径MTU和MSS

    • TCP的数据报会发送给IP层,用到IP协议

    • IP层通过路由选择,将数据发送给目的端口

    • 以太网协议需要知道目的IP的物理地址,需要用到ARP协议

  • 用到了网络中的哪些层,每一层有什么用:

    • 1.DNS协议,HTTP协议,HTTPS协议属于应用层

      应用层是体系结构中的最高层。应用层确定进程之间通信的性质满足用户的需要。这里所说的进程就是指正在运行的程序。应用层不仅需要提供应用进程需要的信息交换,而且还要作为相互作用的应用进程的用户代理,来完成一些为进行语义上有意义的交换所必须的功能。

    • 2.TCP/UDP属于传输层

      传输层的任务就是负责主机中两个进程间的通信。因特网的传输层可使用两种不同的协议;即面向连接的传输控制协议TCP和无连接的用户数据报协议UDP。面向连接的服务能够提供可靠的交付,两种方式都各有其优点

    • 3.IP协议和ARP协议属于网络层

      网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将传输层产生的报文段或用户数据报封装成分组或者包进行传送。在TCP/IP体系中,分组也叫作IP数据报。网络层的另一个任务就是选择合适的路由,使源主机传输层传下来的分组能够交付到目的主机。

    • 4.数据链路层

      当发送数据时,数据链路层的任务是将在网络层交下来的IP数据报组装成帧,在两个相邻节点间的链路上传送以帧为单位的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制以及流量控制等信息)。控制信息使接收端能够知道一个帧从那个比特开始到那个比特结束。控制信息还使接收端能够检测到所收到的帧中有没有差错。

    • 5.物理层

      物理层的任务就是透明的传送比特流。在物理层上所传输的数据的单位是比特。传递信息所利用的一些物理媒介,如双绞线、同轴电缆、光缆等,并不是在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第0层。

2、HTTP和HTTPS的区别

  • HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。

    • HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。

      • HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。

  • 区别:(1)HTTPS 协议需要到CA(电子商务认证机构)申请证书,一般免费证书很少,需要交费。

    (2)HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的ssl加密传输协议

    (3)HTTP 和 HTTPS 使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。 (4)HTTP 的连接很简单,是无状态的 (5)HTTPS 协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比 HTTP 协议安全 (6)HTTPS 内容传输经过完整性校验 (7)HTTPS 内容经过对称加密,每个连接生成一个唯一的加密密钥 (8)HTTPS 第三方无法伪造服务端(客户端)身份

  • HTTPS保证数据安全的机制:

    1.客户端向服务器端发起SSL连接请求;

    1. 服务器把公钥发送给客户端,并且服务器端保存着唯一的私钥;

    3.客户端用公钥对双方通信的对称秘钥进行加密,并发送给服务器端;

    4.服务器利用自己唯一的私钥对客户端发来的对称秘钥进行解密,在此过程中,中间方无法对其解密(即使是客户端也无法解密,因为只有服务器端拥有唯一的私钥),这样保证了对称秘钥在收发过程中的安全,此时,服务器端和客户端拥有了一套完全相同的对称秘钥。

    5.进行数据传输,服务器和客户端双方用公有的相同的对称秘钥对数据进行加密解密,可以保证在数据收发过程中的安全,即是第三方获得数据包,也无法对其进行加密,解密和篡改

  • SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。

3、TCP连接的三次握手和四次挥手

  • 三次握手: 三次握手的目的是建立可靠的通信信道,即双方确认自己的接收与发送是正常的

    • 第一次握手:建立连接时,客户端发送带有syn标志的数据包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

    • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

    • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

  • 四次挥手:

    • 1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

    • 2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

    • 3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

    • 4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

    • 5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

    • 6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

  • 为什么 TCP 连接需要三次握手,两次不可以么,为什么?

    • 为了防止已失效的连接请求报文突然又传送到了服务端,因而产生错误。

  • TIME_WAIT和CLOSE_WAIT的区别

    • TIME_WAIT表示主动关闭,CLOSE_WAIT表示被动关闭。

    • TCP协议规定,对于已经建立的连接,网络双方要进行四次挥手才能断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接,所以很有必要保证无用连接完全断开,否则大量僵死的连接会浪费许多服务器资源。在众多TCP状态中,最值得注意的状态有两个:CLOSE_WAIT和TIME_WAIT。

      • TIME_WAIT 是主动关闭链接时形成的,等待2MSL时间,约4分钟。一方面是为了把原来的连接里面的重复数据包都已经在网络中消逝。避免老的连接的数据影响新建立的连接(新老连接的IP和端口号相同,新的被称为老的连接到化身)。 另一方面, 假如客户端回复的ACK丢失,服务端会重发FIN,客户端此时还能接收到FIN,还能再回复一个ACK(此时time_wait会重新计时)(MSL是指一个包的最大存活时间,一般是两分钟。)

      • 另一种对于TIME_WAIT的解释:如果没有TIME_WEIT这个等待,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的 TCP 报文可能与新 TCP 连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的 TCP 连接的活跃报文全部死翘翘,2MSL 时间可以满足这个需求(尽管非常保守)!

      • CLOSE_WAIT是被动关闭连接是形成的。根据TCP状态机,服务器端收到客户端发送的FIN,则按照TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果服务器端不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。此时,可能是系统忙于处理读、写操作,而未将已收到FIN的连接,进行close。此时,recv/read已收到FIN的连接socket,会返回0。

4、TCP和UDP的区别

  • TCP和UDP都属于传输层协议,它们之间的区别在于:

    • TCP 是面向连接的;UDP 是无连接的。

    • TCP 是可靠的;UDP 是不可靠的。

    • TCP 只支持点对点通信;UDP 支持一对一、一对多、多对一、多对多的通信模式。

    • TCP 是面向字节流的;UDP 是面向报文的。

    • TCP 有拥塞控制机制;UDP 没有拥塞控制,适合媒体通信。

    • TCP 首部开销(20 个字节),比 UDP 的首部开销(8 个字节)要大。

5、TCP/IP如何保证可靠性

  • TCP提供一种面向连接的、可靠的字节流服务

  • 对于可靠性,TCP通过以下方式来保证

    • 数据包校验:目的是检验数据在传输过程中的变化,若检验包有错,则丢弃报文段并且不给出响应,这时TCP发送数据超时后会重发数据。

    • 序号机制(序号、确认号):确保了数据是按序、完整到达。

    • 对失序数据包重排序:既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层。TCP传输时将每个字节的数据都进行了编号,这就是序列号。TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。

    • 丢弃重复数据:对于重复数据,能够丢弃重复数据。这是在超时重传情况下可能发生,判断依据就是序列号。

    • 应答机制:当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。

    • 超时重发:当TCP发出一个段后,他启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报文段。

    • 流量控制:TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。TCP使用的流量控制协议是可变大小的滑动窗口协议。

    • 拥塞控制: TCP传输过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题,网络可能在开始的时候就很拥堵,如果给网络再扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,以及大量的超时重传,严重影响传输。所以TCP引入了慢启动的机制,在刚开始发送数据时,先发送少量的数据探路,探清当前的网络状态如何,再决定多大的速度进行传输。这个时候就引入了一个叫做拥塞窗口的概念+。拥塞窗口是发送端根据网络拥塞情况确定的窗口值。在刚刚开始发送报文的时候,先把拥塞窗口设置1,每经过一个传输轮次(把拥塞窗口所允许发送的报文段都连续发送出去,并收到接受方的确认应答),拥塞窗口就加倍,这个增长速度是指数级别的,为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。慢开始的“慢”并不是指拥塞窗口的增长速率慢,而是指在TCP开始发送报文段时先设置拥塞窗口=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大拥塞窗口。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取最小的值作为实际发送的窗口。一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。

6、Http请求中get和post的区别以及数据包格式

  • GET:对服务器资源的简单请求,把参数包含在URL中。

  • POST:用于发送包含用户提交数据的请求,通过request body传递阐述。

    另外,对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

7、HTTP请求和响应的报文格式

  • HTTP请求:

    • 请求行,用来说明请求类型、要访问的资源以及所使用的HTTP版本。例如 GET /books/java.html HTTP/1.1

    • 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息,第二行起为请求头部。

    • 空行,请求头部后面的空行是必须的。

    • 请求数据,也叫主体,可以添加任意的其他数据。

  • HTTP响应:

    • 状态行,由HTTP协议版本号、状态码、状态消息三部分构成。

    • 消息报文,用来说明客户端要使用的一些附加消息。

    • 空行,消息报文后面的空行是必须的。

    • 响应报文,服务器返回给客户端的文本信息。

8、网络模型

  • 自上而下分别是:

    • 应用层(数据):通过应用进程间的交互来完成特定的网络应用,应用层协议包含域名系统DNS,HTTP协议,SMTP协议(电子邮件传输)

    • 表示层(数据):主要解决用户信息的语法表示问题,如加密解密。在表示层进行代码/编码转换。

    • 会话层(数据):提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制,如服务器验证用户登录便是由会话层完成的。在会话层封装会话控制参数。

    • 传输层(段):负责向两台主机进程之间的通信提供通用的数据传输服务,运输层协议:TCP协议,UDP协议

    • 网络层(包):在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多个通信子网,网络层的任务就是选择网间路由和交换节点,确保数据及时传送, 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报

    • 数据链路层(帧):数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧

    • 物理层(比特流):在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异

9、如何理解HTTP协议的无状态性

  • 无状态,是指协议对于事务处理没有记忆功能。HTTP 是一个无状态协议,这意味着每个请求都是独立的,Keep-Alive 没能改变这个结果。无状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就很快。

    无状态,更容易做服务的扩容,支撑更大的访问量。

10、拥塞控制

  • 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

    为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

    TCP的拥塞控制采用了四种算法,即 慢开始拥塞避免快重传快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

  • 慢开始: 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。

  • 拥塞避免: 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1.

  • 快重传与快恢复: 在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。  当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

11、网络中数据传输过程

  • ARP协议(地址解析协议),它的主要功能是将网络层IP地址转化为数据链路层MAC地址。从IP地址到物理地址的映射有两种方式:表格方式和非表格方式。在以太网中或者在同一局域网中,所有对IP地址的访问都转换为对数据链路层网卡MAC地址的寻找。如果主机A的ARP列表中没有主机B的IP地址和对应的MAC地址,那么在传输数据时是不可能到达主机B的

  • DNS(域名服务器),它的主要功能是将域名转换为对应的IP地址。在不同网段的数据传输中,主机A要先根据主机B的IP地址与子网掩码做与运算所得的结果——主机B所在的网络号找到主机B所在的网络,再根据MAC地址找到主机B

  • 同一网段的数据传输:

    • 假设在同一网段中的两台主机A和B想要通信,A如果想给B发送数据,必须先将B的IP地址与它的子网掩码做与运算得出B所在的网络号,A将所得的B的网络号和自己的做比较,以判断B和A是否在同一网段中,如果相同,则在同一网段,如果不同,则不在同一网段。如果A和B在同一网段,但是A没有B的IP地址所对应的MAC地址信息,则利用第二层广播形式发送ARP请求报文,在报文中包含了A(源主机)和B(目标主机)的IP地址信息。同一网段中的所有主机都可以收到并分析ARP报文,如果发现目标主机的IP地址和自己的不同,则丢弃报文,否则,就向A(源主机)发送ARP请求响应报文,报文的内容包括B(目标主机)的MAC地址。

    • 为了减少广播量,网络设备通过ARP表在缓存中保存IP与MAC地址的映射信息。在一次 ARP的请求与响应过程中,通信双方都把对方的MAC地址与IP地址的对应关系保存在各自的ARP表中,以在后续的通信中使用。ARP表使用老化机制,删除在一段时间内没有使用过的IP与MAC地址的映射关系。

    • 如果中间要经过交换机,交换机内部有一个数据库专门用来保存所有端口对应的网卡MAC地址。根据交换机的工作原理,它通过分析Ethernet包的包头信息(其中包含原MAC地址,目标MAC地址,信息的长度等),取得目标B的MAC地址后,查找交换机中存储的地址对照表(MAC地址对应的端口),确认具有此MAC地址的网卡连接在哪个端口上,然后将数据包发送到这个对应的端口,也就相应的发送到目标主机B上。

  • 不同网段的数据传输:

    • 在不同网段的数据传输中, 主机A并不需要获取远程主机C的MAC地址,而是将IP分组发给默认网关,由网关网络层完成转发过程。如果A(源主机)没有默认网关MAC地址的缓存记录,则它会通过ARP协议获取默认网关的MAC地址,因此在A的ARP表中只能观察到网关的MAC地址记录,而观察不到远程主机C的 MAC地址

    • A要发送数据包到C,如果A没有C的IP地址,则A首先要发出一个DNS请求,路由器B或者DNS域名解析服务器会给A回应C的IP地址,这样A关于数据包网络层和传输层的IP地址信息就全了:源IP地址:A,目的IP地址:C

    • 下来A要知道如何到达C,A会发送一个ARP的地址解析请求,发送这个地址解析请求,不是为了获得目标主机C的MAC地址,而是把请求发送到了路由器B中,然后路由器B会将自己的MAC地址会发送给源主机A,这样A的数据包的数据链路层信息也全了,源MAC地址:A的MAC地址,目的MAC地址:路由器B的MAC地址

    • 然后数据会到达交换机A,交换机A看到数据包的数据链路层的目的MAC地址,是去往路由器B的,就把数据包发送到路由器B,路由器B收到数据包,首先查看数据包的网络层目的IP地址,根据路由选择算法得到一张路由表,如果在自己的路由表中有去往C的路由,说明这是一个可路由的数据包

    • 接下来就要进行路由传送了,首先,由路由器B对报文进行IP重组或分组,修改数据链路层的报头——将源MAC地址改为自己的,将目的MAC地址改为下一跳路由器C的MAC地址。将数据报传送给路由器C,路由器C重复路由器B的工作,直至到达目的主机C所在的网络,再利用局域网通信,找到相应的主机C

    • 从路由器B到路由器C再到主机C的过程在一个庞大的网络系统中,在这个网络中还要封装一系列的协议,以完成数据的正确传输

    • 如果在传输期间丢包或者到达主机C的数据包有错误,此时就需要发挥传输层TCP协议的作用了,对丢失的数据包进行重传

    • 总结:数据传输的过程在主机A(源主机)中,就是利用遵循的某种协议逐层为数据添加报头,添加报头是为了找到目的主机并如何将数据安全正确的传输到目的主机

12、http长连接与短链接

  • http长连接指的是使用完一次http服务之后连接不断开,可以继续下一次服务使用

  • 短连接指的是每请求完一个资源,就断开连接,想要再次请求,就得再建立连接

  • http无状态:协议对交互场景没有记忆能力。浏览器对服务器完成一次资源请求后,再次对服务器进行资源请求,服务器不会记得之前的行为

设计模式部分

1、单例模式

  • 为什么要有单例模式:

    • 对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。

    • 一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。

  • 在单例模式的实现过程中,需要注意如下三点:

    • 单例类的构造函数为私有;

    • 提供一个自身的静态私有成员变量;

    • 提供一个公有的静态工厂方法。

  • 模式定义:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

  • 单例模式的要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

    • 优点:

    • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

    • 2、避免对资源的多重占用(比如写文件操作)。

    • 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

  • 单例模式的几种实现方式:

  • 懒汉式:在第一次调用的时候才进行初始化,避免了内存浪费,但是必须加锁 synchronized 才能保证单例,但加锁会影响效率

    • public class Singleton {   private static Singleton instance;   private Singleton (){}     public static Singleton getInstance() {   if (instance == null) {   instance = new Singleton();   }   return instance;   }  }
  • 饿汉式:在类进行装载的时候就进行实例化,不用加锁,执行效率较高,但是会浪费内存

    • 它基于 classloader 机制避免了多线程的同步问题

    • public class Singleton {   private static Singleton instance = new Singleton();   private Singleton (){}   public static Singleton getInstance() {   return instance;   }  }
  • 双重锁模式:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

    • /* 为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,
      空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,
      而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。 */ public class Singleton {   //当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序  //被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。  private volatile static Singleton singleton;   private Singleton (){}   public static Singleton getSingleton() {   if (singleton == null) {   synchronized (Singleton.class) {   if (singleton == null) {   singleton = new Singleton();   }   }   }   return singleton;   }  }

2、简单工厂模式

  • 为什么要有简单工厂模式:

    • 我们希望可以在知道类的一个参数的时候,通过一个简单的方法传入参数就可以创建类的实例

  • 简单工厂模式定义:

    • 简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

  • 模式结构:

    • 简单工厂模式包含如下角色:

      • Factory:工厂角色

        工厂角色负责实现创建所有b实例的内部逻辑

      • Product:抽象产品角色

        抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口

      • ConcreteProduct:具体产品角色

        具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例

  • 简单工厂模式特点:

    • 优点:

      • 只需要知道名称就可以创建对象

      • 扩展性高

      • 对用户屏蔽了具体实现

    • 缺点:

      • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。

    • 适用场景:

      • 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。

      • JDK类库中广泛使用了简单工厂模式,如工具类java.text.DateFormat,它用于格式化一个本地日期或者时间。

3、工厂方法模式

  • 工厂方法模式定义:

    • 工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

  • 模式结构:

    • 工厂方法模式包含如下角色:

      • Product:抽象产品

      • ConcreteProduct:具体产品

      • Factory:抽象工厂

      • ConcreteFactory:具体工厂

  • 模式分析:

    • 工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责哪一个产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。

    • 优点:

      • 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。

      • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。

      • 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。

    • 缺点:

      • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。

      • 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。

  • 总结:

    • 工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。

    • 工厂方法模式的主要优点是增加新的产品类时无须修改现有系统,并封装了产品对象的创建细节,系统具有良好的灵活性和可扩展性;其缺点在于增加新产品的同时需要增加新的工厂,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性。

    • 工厂方法模式适用情况包括:一个类不知道它所需要的对象的类;一个类通过其子类来指定创建哪个对象;将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定。

4、抽象工厂模式

  • 模式定义:

    • 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。

    • 工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。

      为了更清晰地理解工厂方法模式,需要先引入两个概念:

      • 产品等级结构 :产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。

      • 产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。

    • 当系统所提供的工厂所需生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构中属于不同类型的具体产品时需要使用抽象工厂模式。

    • 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。

    • 抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。

  • 模式结构:

    • 抽象工厂模式包含如下角色:

      • AbstractFactory:抽象工厂

      • ConcreteFactory:具体工厂

      • AbstractProduct:抽象产品

      • Product:具体产品

  • 模式分析:

    • 优点:

      • 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。

      • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。

      • 产品族难扩展,产品等级易扩展。

    • 缺点:

      • 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。

      • 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)。

    • 适用环境:

      • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。

      • 系统中有多于一个的产品族,而每次只使用其中某一产品族。

      • 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。

      • 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

    • 总结:

      • 抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。

      • 抽象工厂模式包含四个角色:抽象工厂用于声明生成抽象产品的方法;具体工厂实现了抽象工厂声明的生成抽象产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中;抽象产品为每种产品声明接口,在抽象产品中定义了产品的抽象业务方法;具体产品定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法。

      • 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。

      • 抽象工厂模式的主要优点是隔离了具体类的生成,使得客户并不需要知道什么被创建,而且每次可以通过具体工厂类创建一个产品族中的多个对象,增加或者替换产品族比较方便,增加新的具体工厂和产品族很方便;主要缺点在于增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。

      • 抽象工厂模式适用情况包括:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节;系统中有多于一个的产品族,而每次只使用其中某一产品族;属于同一个产品族的产品将在一起使用;系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

5、代理模式

  • 模式定义:

    • 代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式。

  • 模式结构:

    • 代理模式包含如下角色:

      • Subject: 抽象主题角色

      • Proxy: 代理主题角色

      • RealSubject: 真实主题角色

  • 静态代理:

    • 在使用静态代理时,被代理对象与代理对象需要一起实现相同的接口或者是继承相同父类,因此要定义一个接口或抽象类

  • JDK动态代理

    • 动态代理的主要特点就是能够在程序运行时JVM才为被代理对象生成代理对象。

      常说的动态代理也叫做JDK代理也是一种接口代理,JDK中生成代理对象的代理类就是Proxy,所在包是java.lang.reflect

      代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能使用动态代理,因此这也算是这种方式的缺陷。

  • CGLib动态代理:

    • 上面的静态代理和动态代理模式有个相同点就是都要求目标对象是实现一个接口的对象,然而并不是任何对象都会实现一个接口,也存在没有实现任何的接口的对象,

    • 这时就可以使用继承目标类以目标对象子类的方式实现代理,这种方法就叫做:Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展.

    • 使用JDK动态代理有一个限制,就是被代理的对象必须实现一个或多个接口,若想代理没有实现接口的类,就需要使用Cglib实现.

    • 使用需要导入jar包

  • 模式分析:

    • 优点:

      • 代理模式能够协调调用者和被调用者,在一定程度上降低了系 统的耦合度。

      • 远程代理使得客户端可以访问在远程机器上的对象,远程机器 可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。

      • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系 统资源的消耗,对系统进行优化并提高运行速度。

      • 保护代理可以控制对真实对象的使用权限。

    • 缺点:

      • 由于在客户端和真实主题之间增加了代理对象,因此 有些类型的代理模式可能会造成请求的处理速度变慢。

      • 实现代理模式需要额外的工作,有些代理模式的实现 非常复杂。

    • 总结:

      • 在代理模式中,要求给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式。 - 代理模式包含三个角色:抽象主题角色声明了真实主题和代理主题的共同接口;代理主题角色内部包含对真实主题的引用,从而可以在任何时候操作真实主题对象;真实主题角色定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的方法。

6、访问者模式

  • 在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作

  • 封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作

  • 主要解决稳定的数据结构和易变操作的耦合问题

7、观察者模式

  • 观察者模式属于行为型模式,也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新

  • 主要解决一个对象状态改变给其他对象通知的问题,缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的

8、适配器模式

  • 适配器模式属于结构型模式,它作为两个不兼容接口之间的桥梁,结合了两个独立接口的功能,将一个类的接口转换成另外一个接口使得原本由于接口不兼容而不能一起工作的类可以一起工作

  • 缺点是过多使用适配器会让系统非常混乱,不易整体把握

简单数据结构部分

1、什么是 AVL 树?

  • 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

  • AVL 树是平衡二叉查找树,增加和删除节点后通过树形旋转重新达到平衡

  • 右旋是以某个节点为中心,将它沉入当前右子节点的位置,而让当前的左子节点作为新树的根节点,也称为顺时针旋转

  • 左旋是以某个节点为中心,将它沉入当前左子节点的位置,而让当前的右子节点作为新树的根节点,也称为逆时针旋转

2、红黑树

  • 红黑树(也属于二叉查找树)的主要特征是在每个节点上增加一个属性表示节点颜色,可以红色或黑色

  • 红黑树和 AVL 树类似,都是在进行插入和删除时通过旋转保持自身平衡,从而获得较高的查找性能,但是与 AVL 树相比,红黑树不追求所有递归子树的高度差不超过 1,保证从根节点到叶尾的最长路径不超过最短路径的 2 倍,所以最差时间复杂度是 O(logn)

  • 红黑树通过重新着色和左右旋转,更加高效地完成了插入和删除之后的自平衡调整

  • 红黑树必须满足的五个约束条件:

    • 节点只能是红色或者黑色

    • 根节点必须是黑色

    • 所有 NIL 节点都是黑色的

    • 一条路径上不能出现相邻的两个红色节点

    • 在任何递归子树中,根节点到叶子节点的所有路径上包含相同数目的黑色节点

  • 这五个约束条件保证了红黑树的新增、删除、查找的最坏时间复杂度均为 O(logn)。如果一个树的左子节点或右子节点不存在,则均认定为黑色。红黑树的任何旋转在 3 次之内均可完成

  • 红黑树的自平衡:

    • 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变

    • 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变

    • 变色:结点的颜色由红变黑或由黑变红

3、红黑树与AVL树的区别

  • 红黑树的平衡性不如 AVL 树,它维持的只是一种大致的平衡,不严格保证左右子树的高度差不超过 1。这导致节点数相同的情况下,红黑树的高度可能更高,也就是说平均查找次数会高于相同情况的 AVL 树

  • 在插入时,红黑树和 AVL 树都能在至多两次旋转内恢复平衡,在删除时由于红黑树只追求大致平衡,因此红黑树至多三次旋转可以恢复平衡,而 AVL 树最多需要 O(logn) 次。AVL 树在插入和删除时,将向上回溯确定是否需要旋转,这个回溯的时间成本最差为 O(logn),而红黑树每次向上回溯的步长为 2,回溯成本低。因此面对频繁地插入与删除红黑树更加合适

4、B树和B+树的区别

  • B 树中每个节点同时存储 key 和 data,而 B+ 树中只有叶子节点才存储 data,非叶子节点只存储 key。InnoDB 对 B+ 树进行了优化,在每个叶子节点上增加了一个指向相邻叶子节点的链表指针,形成了带有顺序指针的 B+ 树,提高区间访问的性能

  • B+ 树的优点在于:① 由于 B+ 树在非叶子节点上不含数据信息,因此在内存页中能够存放更多的 key,数据存放得更加紧密,具有更好的空间利用率,访问叶子节点上关联的数据也具有更好的缓存命中率。② B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子节点即可。而 B 树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有 B+树好

  • B 树也有优点,由于每个节点都包含 key 和 value,因此经常访问的元素可能离根节点更近,访问也更迅速

5、B+树

  • B+ 树是一种 树 数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点

  • B+ 树通常用于数据库和操作系统的文件系统中, B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度, B+ 树元素自底向上插入

  • 每个非叶子节点只保存索引,不保存数据,所有数据都保存在叶子节点

  • 所有的叶子节点形成有序链表,便于范围查询

  • 数据量相同的情况下,B+树比B树更加“矮胖“,因此使用的IO查询次数更少

6、一致性哈希算法

  • 一致性hash算法广泛应用于分布式系统中

  • 一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1

  • 首先可以通过对服务器的ip地址或者主机名作为关键字进行hash计算,然后对2^32取模,将服务器映射到环上

  • 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器

  • 一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性

7、算法的稳定

  • 稳定排序:待排序的记录序列中可能存在两个或两个以上关键字相等的记录。排序前的序列中Ri领先于Rj(即i<j).若在排序后的序列中Ri仍然领先于Rj,则称所用的方法是稳定的。比如int数组[1,1,1,6,4]中a[0],a[1],a[2]的值相等,在排序时不改变其序列,则称所用的方法是稳定的

  • 稳定排序:插入排序、基数排序、归并排序、冒泡排序

  • 不稳定排序:快速排序、希尔排序、简单选择排序、堆排序

补充一个散记的Java相关

1、接口与抽象类

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在方法体

  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的

  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口

  • 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象

  • 应用场景:

    • 接口(interface):

      • 类与类之前需要特定的接口进行协调,而不在乎其如何实现

      • 需要实现特定的多项功能,而这些功能之间可能完全没有任何联系

      • 作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识

    • 抽象类(abstract):

      • 定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。可以用abstract class定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖

      • 规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能

2、注解

  • 注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override 标识一个方法是重写方法

  • 元注解是自定义注解的注解,例如:

    @Target:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。

    @Rentention:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。

    @Documented:表明这个注解应该被 javadoc 记录

3、不可变类

  • 是指创建该类的实例后,该实例的实例变量是不可改变的。java中已有类例如Double和String等

  • 自定义不可变类需遵守:

    • 使用private和final修饰符来修饰该类的成员变量

    • 提供带参数的构造器,根据传入的参数来初始化类里的成员变量

    • 仅为该类的成员变量提供getter方法,不要提供setter方法,因为普通方法无法修改final修饰的成员变量

  • 不可变类的实例状态不可改变,可以很方便被多个对象共享

4、反射

  • 在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。缺点是破坏了封装性以及泛型约束。反射是框架的核心,Spring 大量使用反射

5、泛型

  • 泛型本质是参数化类型,解决不确定对象具体类型的问题。泛型在定义处只具备执行 Object 方法的能力

  • 泛型的好处:① 类型安全,放置什么出来就是什么,不存在 ClassCastException。② 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。③ 代码重用,合并了同类型的处理代码

  • 泛型擦除:泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类

6、自动装箱/拆箱是什么

  • 每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。

    自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。

    自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。

    比较两个包装类数值要用 equals ,而不能用 ==

7、BIO NIO AIO

  • 同步与非同步关注的是消息通信机制

    • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了

    • 同步 IO 是用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行。异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程,或调用用户线程注册的回调函数

  • 阻塞和非阻塞式调用状态

    • 阻塞与非阻塞关注的是程序在等待调用结果的状态

    • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

  • BIO:同步阻塞式IO,服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要的线程开销,适用连接数目少且服务器资源多的场景

  • NIO:同步非阻塞式IO,服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理,适用连接数目多且连接时间短的场景

  • AIO:异步非阻塞IO,服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据,适用连接数目多且连接时间长的场景

  • 序列化与反序列化:

    • Java 对象 JVM 退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化

  • IO多路复用:

    • 当使用io多路复用时,有多个C端同时发送请求,这些IO操作会被selector(epoll,kqueue)给暂时挂起,入内存队列。此时S端可以自己选择什么时候读取、处理这些io,也就是说S端可以同时hold住多个io

    • I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作

    • select:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符

    • poll:pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符

    • epoll:相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

8、谈一谈Synchronized

  • synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被他修饰的方法或者代码块在任意时刻只有一个线程执行

  • 每个 Java 对象都有一个关联的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取

  • 可重入锁:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞(synchronized 是可重入锁)

  • 三种用法:修饰同步方法、静态方法、同步代码块

9、锁

  • JDK 6 对 synchronized 做了很多优化,引入了自适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提高锁的效率,锁一共有 4 个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率

  • 自旋锁:同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核空间完成,许多应用对共享数据的锁定只持续很短的时间,起和恢复线程并不值得。如果机器有多个处理器核心,我们可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待只需让线程执行一个忙循环,这项技术就是自旋锁

  • 自适应自旋:JDK6 对自旋锁进行了优化,自旋时间不再固定,而是由前一次的自旋时间及锁拥有者的状态决定

  • 锁消除:指即时编译器对检测到不可能存在共享数据竞争的锁进行消除

  • 锁粗化:原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待锁的线程尽快拿到锁,但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把同步的范围扩展到整个操作序列的外部

  • 偏向锁:偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步,一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执行

  • 轻量锁:轻量级锁是为了在没有竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗

  • 偏向锁、轻量锁、重量锁的区别:

    • 偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景

    • 轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU,适用追求响应时间、同步代码块执行快的场景

    • 重量级锁的优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景

  • Lock和Synchronized的区别:

    • Lock 接是 juc 包的顶层接口,基于Lock 接口,用户能够以非块结构来实现互斥同步,摆脱了语言特性束缚,在类库层面实现同步。Lock 并未用到 synchronized,而是利用了 volatile 的可见性

    • lock是一个接口,而synchronized是java的一个关键字

    • synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生

    • lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断

  • ReentrantLock:

    • 提供了无条件的,可轮询的,定时的以及可中断的锁获取操作

    • 加锁和解锁都是可见的

    • ReentrantLock 是排他锁,同一时刻只允许一个线程访问

    • 属于可重入锁

  • AQS:

    • AQS 队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程

    • 每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源

  • AQS的两种模式:

    • 独占模式表示锁只会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取锁,同一时间只能有一个线程获取到锁

    • 共享模式表示多个线程获取同一个锁有可能成功,ReadLock 就采用共享模式

  • 重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁

  • 写锁是可重入排他锁,读锁是可重入的共享锁

11、类加载

  • java程序是怎么样运行的:

    • 首先通过 Javac 编译器将 .java 转为 JVM 可加载的 .class 字节码文件,之后通过即时编译器 JIT 把字节码文件编译成本地机器码

  • 类加载是什么:

    • Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制

    • Java 中类型的加载、连接和初始化都是在运行期间完成的

    • 一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 的动态绑定

  • 类加载器:

    • 启动类加载器:在JVM启动时创建,负责加载最核心的类

    • 平台类加载器:负责加载一些系统的扩展类

    • 应用类加载器:负责加载用户类路径上的类库

  • 双亲委派模型:

    • 类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应有自己的父类加载器

    • 一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试

    • 类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性

  • 如何判断两个类是否相等:

    • 任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性

    • 两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等

  • 类加载的方式:

    • 静态加载:

      • 通过new关键字来创建实例的时候

    • 动态加载:

      • 通过Class.forName()来加载类,然后调用类的newInstance()方法实例化对象

      • 通过类加载器的ClassLoader.loadClass()方法来加载类,然后调用类的newInstance()方法实例化对象

    • Class.ForName与ClassLoader的异同:

      • ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中,加载的类默认不会进行初始化

      • Class.forName()方法实际上也是调用的ClassLoader来实现的,但是加载的类默认会进行初始化,也可以使用重载版本进行手动指定是否会进行初始化

12、JMM(Java内存模型)

  • Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则

  • JMM 规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存

  • 指令重排序:

    • 为了提高性能,编译器和处理器通常会对指令进行重排序,重排序指从源代码到指令序列的重排序,分为三种:① 编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。② 指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。③ 内存系统的重排序

  • 原子性:基本数据类型的访问都具有原子性

  • 可见性:可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的方式实现可见性,无论普通变量还是 volatile 变量都是如此,区别是 volatile 保证新值能立即同步到主内存以及每次使用前立即从主内存刷新

  • 有序性:有序性可以总结为:在本线程内观察所有操作是有序的,在一个线程内观察另一个线程,所有操作都是无序的,Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁止指令重排序的语义,而 synchronized 保证一个变量在同一时刻只允许一条线程对其进行 lock 操作,确保持有同一个锁的两个同步块只能串行进入

  • volatile:JMM 为 volatile 定义了一些特殊访问规则,当变量被定义为 volatile 后具备两种特性

    • 保证变量对所有线程可见:

      • 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。volatile 变量在各个线程的工作内存中不存在一致性问题,但 Java 的运算操作符并非原子操作,导致 volatile 变量运算在并发下仍不安全

    • 禁止指令重排序优化:

      • 使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前

    • 写一个 volatile 变量时,把该线程工作内存中的值刷新到主内存,读一个 volatile 变量时,把该线程工作内存值置为无效,从主内存读取

13、JDK JRE JVM

  • JVM:Java 编译器可生成与计算机体系结构无关的字节码指令,字节码文件不仅可以轻易地在任何机器上解释执行,还可以动态地转换成本地机器代码,转换是由 JVM 实现的,JVM 是平台相关的,屏蔽了不同操作系统的差异

  • JDK:Java Development Kit,开发工具包。提供了编译运行 Java 程序的各种工具,包括编译器、JRE 及常用类库,是 JAVA 核心

  • JRE: Java Runtime Environment,运行时环境,运行 Java 程序的必要环境,包括 JVM、核心类库、核心配置工具

14、子类初始化顺序

  • 父类静态代码块和静态变量

  • 子类静态代码块和静态变量

  • 父类普通代码块和普通变量

  • 父类构造方法

  • 子类普通代码块和普通变量

  • 子类构造方法

15、集合

  • ArrayList: 是容量可变的非线程安全列表,使用数组实现,集合扩容时会创建更大的数组,把原有数组复制到新数组。支持对元素的快速随机访问,但插入与删除速度很慢。ArrayList 实现了 RandomAcess 标记接口,如果一个类实现了该接口,那么表示使用索引遍历比迭代器更快

  • LinkedList:本质是双向链表,与 ArrayList 相比插入和删除速度更快,但随机访问元素很慢。除继承 AbstractList 外还实现了 Deque 接口,这个接口具有队列和栈的性质,LinkedList 的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率较高

  • Set 不允许元素重复且无序,常用实现有 HashSet、LinkedHashSet 和 TreeSet

    • HashSet:通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,所有 Key 都使用相同的 Value ,一个名为 PRESENT 的 Object 类型常量。使用 Key 保证元素唯一性,但不保证有序性。由于 HashSet 是 HashMap 实现的,因此线程不安全,HashSet 判断元素是否相同时,对于包装类型直接按值比较。对于引用类型先比较 hashCode 是否相同,不同则代表不是同一个对象,相同则继续比较 equals,都相同才是同一个对象

    • LinkedHashSet: 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序

    • TreeSet: 通过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序

  • TreeMap:TreeMap 基于红黑树实现,增删改查的平均和最差时间复杂度均为 O(logn) ,最大特点是 Key 有序。Key 必须实现 Comparable 接口或提供的 Comparator 比较器,所以 Key 不允许为 null

  • HashMap:

    • JDK8 之前底层实现是数组 + 链表,JDK8 改为数组 + 链表/红黑树,节点类型从Entry 变更为 Node。主要成员变量包括存储数据的 table 数组、元素数量 size、加载因子 loadFactor

    • HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75

    • resize:扩容数组:

      • jdk7:如果当前容量达到了最大值,则不进行继续扩容,否则创建一个容量为 newCapacity 的 Entry 数组,调用 transfer 方法将旧数组的元素转移到新数组

      • jdk8:重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列

    • 线程不安全:

      • jdk7:JDK7 存在死循环和数据丢失问题

      • jdk8:在 resize 方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据

16、内存

  • 直接内存:直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出,直接内存不在java堆内,并且java堆内存往外写需要拷贝到native堆

  • 内存溢出:内存溢出 OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用

  • 内存泄露:内存泄露 Memory Leak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出

  • 堆溢出:

    • 堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例

    • 堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出

  • 方法区溢出:

    • 方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出

17、创建对象

  • 创建对象的过程:

    • NEW:如果找不到 Class 对象则进行类加载。加载成功后在堆中分配内存,从 Object 到本类路径上的所有属性都要分配

    • 执行角度:

      • 当 JVM 遇到字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载

      • 在类加载检查通过后虚拟机将为新生对象分配内存

      • 内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用

      • 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等

      • 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量

18、docker和虚拟机的区别

  • 虚拟机在创建的时候会虚拟独立的系统内核,而docker共享宿主机内核,系统级虚拟化,占用资源少

  • 启动时间:Docker秒级启动,虚拟机分钟级启动

  • 轻量级:docker镜像大小通常以M为单位,虚拟机以G为单位。容器资源占用小,要比虚拟机部署更快速

  • 使用要求:VM基于硬件的完全虚拟化,需要硬件CPU虚拟化技术支持; docker共享宿主机内核,可运行在主流的Linux发行版,不用考虑CPU是否支持虚拟化技术

  • 安全性:由于共享宿主机内核,只是进程级隔离,因此隔离性和稳定性不如虚拟机,docker具有一定权限访问宿主机内核,存在一定安全隐患

19、哈希表

  • 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做哈希表

  • hash冲突:即不同key值产生相同的地址

  • hash冲突的解决办法:开放定址法 链地址法


感谢阅读~ 是我个人的秋招复盘总结积累下的,若有错误欢迎各位大佬指出
祝大家秋招顺利





#面经##数据开发工程师##校招##学习路径#
全部评论
1 回复
分享
发布于 2020-09-30 19:13
每天来学一点
1 回复
分享
发布于 2020-10-24 20:40
饿了么
校招火热招聘中
官网直投
点赞 回复
分享
发布于 2020-09-30 18:41
点赞 回复
分享
发布于 2020-09-30 23:04
tql
点赞 回复
分享
发布于 2020-10-01 00:04
mark
点赞 回复
分享
发布于 2020-10-02 00:06
mark,楼主tql
点赞 回复
分享
发布于 2020-10-08 09:54
害怕
点赞 回复
分享
发布于 2020-10-08 10:01
点赞 回复
分享
发布于 2020-10-08 12:37
mark
点赞 回复
分享
发布于 2020-10-10 11:32
tql
点赞 回复
分享
发布于 2020-10-10 20:40
TQL
点赞 回复
分享
发布于 2020-10-12 21:17
tql
点赞 回复
分享
发布于 2020-10-14 19:36
mark,膜
点赞 回复
分享
发布于 2020-10-15 18:40
非常感谢同学分享的优质面经内容以及对牛客社区的贡献与支持, 特别赠送同学100元京东卡一张~  只要把面经链接放到面经知识汇总专场下面,就可以领奖品了哟! 活动专场:https://www.nowcoder.com/discuss/447528
点赞 回复
分享
发布于 2020-10-16 14:23
tql
点赞 回复
分享
发布于 2020-10-18 10:58
tp
点赞 回复
分享
发布于 2020-10-22 15:16
违反密钥
点赞 回复
分享
发布于 2020-11-26 19:24
tql
点赞 回复
分享
发布于 2020-12-03 10:22
太全了我去
点赞 回复
分享
发布于 2021-11-22 16:10

相关推荐

63 330 评论
分享
牛客网
牛客企业服务