在上一篇 我们中,我们分享了几大互联网公司面试的题目,本文就来详细分析面试题答案以及复习参考和整理的面试资料,小民同学的私藏珍品🐶。
首先是面试题答案公布,在讲解时我们主要分成如下几块:语言的基础知识、中间件、操作系统、计算机网络、手写算法、开放题和项目经历。对面试题和涉及的知识点进行整理,这样更容易让各位同学理解。不会按照提问的顺序进行讲解,还请见谅。
其次是 Java 复习参考和整理的面试资料。由于内容比较多,学习有 道
非常重要,我们介绍一下其中的要点和目录,完整文件可以参见笔者提供的 pdf 资料。
jenkins 涉及到 DevOps 相关的知识,主要用于自动化集成,持续、自动地构建/测试软件项目,监控一些定时任务。
持续集成(CI)已成为当前许多软件开发团队在整个软件开发生命周期内侧重于保证代码质量的常见做法。它是一种实践,旨在缓和和稳固软件的构建过程。
MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。不同存储引擎的 MVCC 实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
MVCC 简单来讲就是对数据库的任何修改的提交都不会直接覆盖之前的数据,而是产生一个新的版本与老版本共存,使得读取时可以完全不加锁。这样读某一个数据时,事务可以根据隔离级别选择要读取哪个版本的数据。过程中完全不需要加锁。
可以认为 MVCC 是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。大多数的 MVCC 都实现了非阻塞的读操作,写操作也只锁定必要的行。MVCC只能在可重复读和可提交读的隔离级别下生效。不可提交读不能使用它的原因是不能读取符合事务版本的行版本。它们总是读取最新的行版本。可序列化不能使用MVCC的原因是,它总是要锁定行。
MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
MySQL有两种方式可以实现ORDER BY:
取出满足过滤条件作为排序条件的字段,以及可以直接定位到行数据的行指针信息,在 Sort Buffer 中进行实际的排序操作,然后利用排好序的数据根据行指针信息返回表中取得客户端请求的其他字段的数据,再返回给客户端。
这种方式,在使用explain分析查询的时候,显示Using index。而文件排序显示Using filesort。
SQL语句中,WHERE子句和ORDER BY子句都可以使用索引:WHERE 子句使用索引避免全表扫描,ORDER BY 子句使用索引避免 filesort(用“避免”可能有些欠妥,某些场景下全表扫描、filesort 未必比走索引慢),以提高查询效率。
虽然索引能提高查询效率,但在一条 SQL 里,对于一张表的查询 一次只能使用一个索引(注:排除发生 index merge 的可能性),也就是说当 WHERE 子句与ORDER BY子句要使用的索引不一致时,MySQL 只能使用其中一个索引(B+树)。
也就是说,一个既有WHERE又有ORDER BY的SQL中,使用索引有三个可能的场景:
filesort仅仅是排序而已,是否会放入磁盘看情况而定。filesort是否会使用磁盘取决于它操作的数据量大小。总结来说就是,filesort按排序方式来划分 分为两种:
数据量大的情况下涉及到磁盘 io,所以效率会低一些。根据回表查询的次数,filesort又可以分为两种方式:
单次传输排序的弊端在于会将所有涉及到的列都放入排序缓冲区,排序缓冲区一次能放下的tuples更少了,进行归并排序的概率增大。列数据量越大,需要的归并路数更多,增加了额外的I/O开销。所以列数据量太大时,单次传输排序的效率可能还不如两次传输排序。
两次传输排序会进行两次回表操作:第一次回表用于在WHERE子句中筛选出满足条件的rowid以及rowid对应的ORDER BY的列值;第二次回表发生在ORDER BY子句对指定列进行排序之后,通过rowid回表查出SELECT子句需要的字段信息。
对于order by字段加入索引本身这个问题,如果最终的结果集是以order by字段为条件筛选的,将order by字段加入索引,并放在索引中正确的位置,会有明显的性能提升。
Eureka 是 Spring Cloud 推荐的服务注册与发现组件,包括Eureka Server 和 Eureka Client:
Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中注册,这样Server中的服务注册表中将会存储所有可用的服务节点的信息;
Eureka Client是一个Java客户端,用于简化与Eureka Server交互,客户端同时具备一个内置的、使用轮询负载均衡算法的负载均衡器;
在应用启动后,将会向Eureka Server发送心跳(默认周期30s),如果Eureka Server在多个心跳周期没有收到某个节点的心跳,Eureka Server会从服务注册表中把这个服务节点删除(默认为90s);
Eureka Server之间通过复制的方式完成数据的同步;
Eureka Client具有缓冲机制,如果Eureka Server全部宕机的情况,客户端依然可以利用缓存的信息消费其他服务API;
Eureka region可以理解为地理上的分区,没有具体大小的限制;
Eureka zone可以理解为region内具体的机房信息;
使用Jersey框架实现自身的Restful HTTP接口,peer之间同步与服务注册通过HTTP协议实现,定时任务(发送心跳、定时清理过期服务、节点同步等)通过JDK自带的Timer实现,内存缓存实现Google的guava实现;
当服务注册中心Server检测服务提供者宕机时,在服务中心将服务置为DOWN状态,并将该服务向其他订阅者发布,订阅者更新本地缓存信息;
当Eureka Server节点在短时间内丢失过多的客户端时,这个节点会进入自我保护模式,不再注销任何服务;
分布式系统环境下,服务间类似依赖非常常见,一个业务调用通常依赖多个基础服务。如下图,对于同步调用,当库存服务不可用时,商品服务请求线程被阻塞,当有大批量请求调用库存服务时,最终可能导致整个商品服务资源耗尽,无法继续对外提供服务。并且这种不可用可能沿请求调用链向上传递,这种现象被称为雪崩效应。
Hystrix 中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。
分布式锁在微服务架构中很常用,主要有几下实现方式:
基于表主键唯一做分布式锁
思路:利用主键唯一的特性,如果有多个请求同时提交到数据库,数据库只会保证只有一个操作可以成功,那么就可以认为操作成功的线程获取到了该方法的锁。当方法执行完毕之后,通过删除该行数据就可释放锁。
基于表字段版本号做分布式锁
基于mysql的mvcc机制,只有版本号一致才能进行对应的修改,修改后版本号加1,通过CAS的方式进行修改。
基于数据库排他锁做分布锁
通过事务和 for update 语句实现,数据库会在该事务下给数据库增加排他锁。在 InnoDB 引擎加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则使用表级锁。
基于Redis的SETNX()、EXPIRE()方法做分布式锁
setnx 方法的语义为 SET if Not Exists,其主要有两个参数,setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前的 key 成功,返回1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。
expire 方法设置过期时间,setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。
如果在 setnx 执行成功后,在 expire 命令执行成功,执行的线程出现宕机的现象,就可能出现死锁现象。
基于 Redis 的 SETNX()、GET()、GETSET() 方法做分布式锁
getset方法有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1;getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2。 使用步骤:
计算 newExpireTime = 当前时间 + 过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值 currentExpireTime。 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
基于 REDLOCK 做分布式锁
Redlock 是 Redis 的作者开发的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点.
客户端获取当前时间,以毫秒为单位。客户端尝试获取N个节点的锁(每个节点获取锁的方式和前面说的缓存锁一样),N 个节点以相同的 key 和 value 获取锁。客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置5-50ms。这样可以在有redis节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。
客户端计算在获得锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端获得了超过3个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。 如果客户端获取锁失败了,客户端会依次删除所有的锁。
基于 REDISSON 做分布式锁
Redisson 是 redis 官方的分布式锁组件。在Redisson中,如果如果线程获取锁成功,Redisson会在后台起开一个定时任务watchdog,定时任务会定时检查调用renewExpirationAsync(threadId)对锁进行续约。定时调度的时间差为internalLockLeaseTime/3,即10秒,默认加锁时间为30s。
基于Consul的分布式锁主要利用Key/Value存储API中的acquire和release操作来实现。acquire和release操作是类似Check-And-Set的操作。 acquire操作只有当锁不存在持有者时才会返回true,并且set设置的Value值,同时执行操作的session会持有对该Key的锁,否则就返回false release操作则是使用指定的session来释放某个Key的锁,如果指定的session无效,那么会返回 false,否则就会set设置 Value 值,并返回 true。
Zookeeper 有如下的角色:
Zookeeper 的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式和广播模式。
当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server的完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和server具有相同的系统状态
一旦leader已经和多数的follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。这时候当一个server加入zookeeper服务中,它会在恢复模式下启动,发现leader,并和leader进行状态同步。待到同步结束,它也参与消息广播。Zookeeper服务一直维持在 Broadcast 状态,直到leader崩溃了或者leader失去了大部分的followers支持。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识 leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
高级开发工程师的面试一般都会涉及源码,为什么很多同学觉得原理、源码是造火箭,其实这个和自己的经历是有很大关系的。首先不排除,确实又一部分面试题的确是造火箭。很多同学做的项目比较简单,就拿 Java 举例,业务可能增删改查居多,长久就形成了工作并不需要看源码,甚至觉得阅读源码、熟悉原理对自己帮助并不大的错觉。看优秀的源码是一种更深入的学习。
首先理解序列化和反序列化。
优点:可以实现对象的"持久性”, 所谓持久性就是指对象的生命周期不取决于程序。
Java 中的序列化方式包括Java原生以流的方法进行的序列化、Json序列化、FastJson序列化、Protobuff序列化。Protobuff序列化支持跨语言。
Java原生序列化方法即通过Java原生流(InputStream和OutputStream之间的转化)的方式进行转化。需要注意的是JavaBean实体类必须实现Serializable接口,否则无法序列化。
Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。现在的大多数公司都将json作为服务器端返回的数据格式。比如调用一个服务器接口,通常的请求为xxx.json?a=xxx&b=xxx的形式。
fastjson 是由阿里巴巴开发的一个性能很好的Java 语言实现的 Json解析器和生成器。特点:速度快,测试表明fastjson具有极快的性能,超越任其他的java json parser。功能强大,完全支持java bean、集合、Map、日期、Enum,支持范型和自省。无依赖,能够直接运行在Java SE 5.0以上版本支持Android。使用时候需引入FastJson第三方jar包。
ProtocolBuffer是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
优点:跨语言;序列化后数据占用空间比JSON小,JSON有一定的格式,在数据量上还有可以压缩的空间。
缺点:它以二进制的方式存储,无法直接读取编辑,除非你有 .proto 定义,否则无法直接读出 Protobuffer的任何内容。
其与thrift的对比:两者语法类似,都支持版本向后兼容和向前兼容,thrift侧重点是构建跨语言的可伸缩的服务,支持的语言多,同时提供了全套RPC解决方案,可以很方便的直接构建服务,不需要做太多其他的工作。 Protobuffer主要是一种序列化机制,在数据序列化上进行性能比较,Protobuffer相对较好。
ProtoBuff序列化对象可以很大程度上将其压缩,可以大大减少数据传输大小,提高系统性能。对于大量数据的缓存,也可以提高缓存中数据存储量。原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。其实用上也比较简洁,通过对JavaBean中的字段进行注解就行,不需要撰写.proto文件和实用编译器将其生成.java文件,百度的jprotobuf都替我们做了这些事情了。
redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失。幸好 Redis 还为我们提供了持久化的机制,分别是RDB(Redis DataBase)和 AOF(Append Only File)。
RDB 其实就是把数据以快照的形式保存在磁盘上。快照可以理解成把当前时刻的数据拍成一张照片保存下来。
RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。
RDB 机制是通过把某个时刻的所有数据生成一个快照来保存,那么就应该有一种触发机制,是实现这个过程。对于 RDB 来说,提供了三种机制:save、bgsave、自动化。我们分别来看一下:
RDB 具有如下的优势:
也存在不足之处,RDB 快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。
全量备份总是耗时的,有时候我们提供一种更加高效的方式 AOF,工作机制很简单,redis 会将每一个收到的写命令都通过 write 函数追加到文件中。通俗的理解就是日志记录。
AOF 的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩 AOF 的持久化文件。redis 提供了 bgrewriteaof 命令。将内存中的数据以命令的方式保存到临时文件中,同时会 fork 出一条新进程来将文件重写。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
AOF也有三种触发机制:
AOF 可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据;AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损;AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写;AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据。
当然,对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大;AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高;以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
和Mysql主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
Redis主从复制的配置十分简单,它可以使从服务器是主服务器的完全拷贝。需要清除Redis主从复制的几点重要内容:
主从复制结构,一般slave服务器不能进行写操作,但是这不是死的,之所以这样是为了更容易的保证主和各个从之间数据的一致性,如果slave服务器上数据进行了修改,那么要保证所有主从服务器都能一致,可能在结构上和处理逻辑上更为负责。不过你也可以通过配置文件让从服务器支持写操作。
主从服务器之间会定期进行通话,但是如果master上设置了密码,那么如果不给slave设置密码就会导致slave不能跟master进行任何操作,所以如果你的master服务器上有密码,那么也给slave相应的设置密码。
spring Cloud 通过 Spring Boot 封装了各家公司开发的一系列成熟、稳定的服务框架,去除了繁琐的配置,给开发者提供了一套简单易用、方便部署、易于维护的分布式系统开发工具包。spring Boot 是 Spring 的一套快速配置脚手架,可以基于 Spring Boot 快速开发单个微服务,Spring Boot 专注于快速、方便的集成单个个体,Spring Cloud 关注全局的服务治理框架。
Spring Boot 可以离开 Spring Cloud 独立使用开发项目,但是 Spring Cloud 离不开 Spring Boot,属于依赖关系。
注册中心主要涉及到三大角色:
它们之间的关系大致如下:
当我们在浏览器的地址栏输入 www.google.com ,然后回车,回车这一瞬间到看到页面到底发生了什么呢?
拿到域名对应的IP地址之后,User-Agent(一般是指浏览器)会以一个随机端口(1024 < 端口 < 65535)向服务器的WEB程序(常用的有 httpd,nginx等)80 端口发起 TCP 的连接请求。这个连接请求(原始的http请求经过TCP/IP4层模型的层层封包)到达服务器端后(这中间通过各种路由设备,局域网内除外),进入到网卡,然后是进入到内核的 TCP/IP 协议栈(用于识别该连接请求,解封包,一层一层的剥开),还有可能要经过 Netfilter 防火墙(属于内核的模块)的过滤,最终到达WEB程序(本文就以Nginx为例),最终建立了 TCP/IP 的连接。
TCP 3 次握手之后,浏览器发起了http的请求(看第包),使用的http的方法 GET 方法,请求的URL是 / ,协议是HTTP/1.0。
服务器端 WEB 程序接收到 http 请求以后,就开始处理该请求,处理之后就返回给浏览器 html 文件。关闭TCP连接。
浏览器拿到index.html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,就向服务器端去请求下载(会使用多线程下载,每个浏览器的线程数不一样),这个时候就用上 keep-alive 特性了,建立一次 HTTP 连接,可以请求多个资源,下载资源的顺序就是按照代码里的顺序,但是由于每个资源大小不一样,而浏览器又多线程请求请求资源,所以从下图看出,这里显示的顺序并不一定是代码里面的顺序。
浏览器在请求静态资源时(在未过期的情况下),向服务器端发起一个http请求(询问自从上一次修改时间到现在有没有对资源进行修改),如果服务器端返回304状态码(告诉浏览器服务器端没有修改),那么浏览器会直接读取本地的该资源的缓存文件。
最后,浏览器利用自己内部的工作机制,把请求到的静态资源和html代码进行渲染,渲染之后呈现给用户。
共享内存就是允许两个或多个进程共享一定的存储区。就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝,所以这是最快的一种 IPC。
但是,当有多个程序同时向共享内存中读写数据时,问题就会出现。共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。
网关和 rpc 负载均衡进行配置,比如在 API 网关统一配置接口的超时时间。在单个微服务中,服务之间的调用配置超时时间,如 Ribbon Timeout。
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。基于TCP的应用层协议,它不关心数据传输的细节,HTTP(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,只有遵循统一的 HTTP 请求格式,服务器才能正确解析不同客户端发的请求,同样地,服务器遵循统一的响应格式,客户端才得以正确解析不同网站发过来的响应。
HTTP 请求由请求行、请求头、空行、请求体组成
请求行:请求方式 + URL + 协议版本
请求头:客户端向服务器发送请求的补充说明:
请求体:一般携带的请求参数
application/json:{"name":"value","name1":"value2”} application/x-www-form-urlencoded: name1=value1&name2=value2 multipart/from-data:表格形式 text/xml content-type:octets/stream 复制代码
在HTTP的请求头中,可以使用Content-type来指定不同格式的请求信息。HTTP协议规定 POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须 使用什么编码方式 。实际上,开发者完全可以自己决定消息主体的格式,只要最后发送的 HTTP 请求满足上面的格式就可以。
数据发送出去,还要服务端解析成功才有意义。一般服务端语言如 php、python 等,以及它们的 framework,都内置了自动解析常见数据格式的功能。服务端通常是根据请求头(headers)中的 Content-Type 字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。
form表单中enctype属性可以用来控制对表单数据的发送前的如何进行编码,enctype有三种,分别为:
其中 application/x-www-form-urlencoded 为默认类型。
HTTPS:是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。
HTTPS 协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
HTTP 协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
HTTPS和HTTP的区别主要如下:
尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案。
大家都知道,我们从一台机器向另外一台机器发送数据的时候,数据并不是一口气也不可能一口气传输给接收方。这个并不难理解,因为网络环境特别的复杂,有些地方快有些地方慢。所以,操作系统把这些数据写成连续的数据包,并且以一定的速率发给对方。
我们要考虑到带宽缓冲区等因素,如果一下子发送所有的数据只会加大网络压力,造成丢包重试,轻则传输更慢,重则网络崩溃。因为TCP是顺序发送的,操作系统将这些数据包一批一批的发送给对方,就像一个窗口,不停地往后移动,所以,我们称之为TCP滑动窗口协议。
在TCP中,窗口的大小是在TCP三次握手后协定的,并且窗口的大小并不是固定的,而是会随着网络的情况进行调整。TCP为了更好的传输效率,就会调整窗口的大小。TCP 通过滑动窗口机制检测丢包,并在丢包发生时调整数据传输速率。
对于发送端来说,即将要发送的数据包排成一个队列,对于发送者来说,数据包总共分成四类。分别是在窗口前的,已经发送给接收方,并且收到了接收方的答复,我们称之为已发送。在窗口中的,有两种状态,一个是已经发送给接收方,但是接收方还没确认送达,我们称之为已发送未确认,另外一个是可以发送了,但是还没有发送,我们称之为允许发送未发送。最后的是在窗口外面的,我们称之为不可发送,除非窗口滑到此处,否则不会进行发送。
一旦前面的数据已经得到服务端确认了,这个窗口就会慢慢地往后滑,如下图所示,P1,P2两个数据包被确认之后,窗口就往后移动,后面新的数据包就由不可发送待发送变成了可发送状态了。
TCP的滑动窗口协议有什么意义呢?首先当然是可靠性,滑动窗口只有在队列前部的被确认之后,才会往后移动,保证数据包被接收方确认并接收。其次是传输效率,假如没有窗口,服务端是杂乱无章地进行发包,因为TCP的队首效应,如果有前面的包没有发送成功,就会不停的重试,反而造成更差的传输效率。最后是稳定性,TCP的滑动窗口大小,是整个复杂网络商榷的结果,会进行动态调整,可以尽量地避免网络拥塞,更加稳定。
当前大部分的网络访问都是基于 TCP/IP 协议开发,而 TCP/IP 是基于 IP 寻址的。又因为大多数人记不住没有意义的 ip 地址,因此希望使用一些简单的域名代替地址,而 DNS 就充当了这样一个“翻译器”。我们输入域名,DNS帮我们查询对应的域名绑定的IP地址。
根据 tcp 关闭时四次握手流程,主动关闭方会在发送完最后的ACK包后进入time_wait 状态,该状态持续时间为 2MSL (MSL 是指报文的最大生存时间,超过 MSL 时间没被接受的数据包将会被丢弃,RFC793 中建议为 2 min),时间到达后将进入 close 状态,关闭本次 tcp 连接。
主动关闭方进入2MSL的 time_wait 状态的目的有二:
延伸一下,如果在tcp客户端出现大量的time_wait状态该如何处理?
提示一下连接复用。
第一百次抽取可以从54张牌中抽大小王中度的一张,概率为2/54=1/27。第二次只能从53张牌中抽到那张知剩下的王,概率道为1/53。所以抽专两张是大小王的概率=(1/27)*(1/53)=1/1431。
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序中,Ai=Aj,且Ai在Aj之前,在排序后的序列中,Ai仍在Aj之前,则称这种排序算法是稳定的;否则该算法是不稳定的。
要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。
在长度为 N 的乱序数组中寻找第k(n>=k)大的元素。
有好几种方法:
下面我们基于方法三进行实现算法:
public static int findNumberK(int[] array, int k) { //1.用前k个元素构建小顶堆 buildHeap(array, k); //2.继续遍历数组,和堆顶比较 for (int i = k; i < array.length; i++) { if(array[i] > array[0]) { array[0] = array[i]; downAdjust(array, 0, k); } } //3.返回堆顶元素 return array[0]; } private static void buildHeap(int[] array, int length) { //从最后一个非叶子节点开始,依次下沉调整 for (int i = (length - 2) / 2; i >= 0; i--) { downAdjust(array, i, length); } } /** * 下沉调整 * @param array 待调整的堆 * @param index 要下沉的节点 * @param length 堆的有效大小 */ private static void downAdjust(int[] array, int index, int length) { //temp保存父节点的值,用于最后的赋值 int temp = array[index]; int childIndex = 2 * index + 1; while (childIndex < length) { //如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子 if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) { childIndex++; } //如果父节点小于任何一个孩子的值,直接跳出 if (temp <= array[childIndex]) break; //无需真正交换,单项赋值即可 array[index] = array[childIndex]; index = childIndex; childIndex = 2 * childIndex + 1; } array[index] = temp; } public static void main(String[] args) { int[] array = new int[] {7, 5, 15, 3, 17, 2, 20, 24, 1, 9, 12, 8}; System.out.println(findNumberK(array, 5)); } 复制代码
具体操作步骤如下:
这个模型类似抽奖的模型,有 n 张彩票,n 个人每人一张,怎样选出 m 个人出来中奖。即我们仅仅须要模拟一个公正的抽奖过程便能得到等概率的 m 个人。
我们都知道,抽奖不分先后。每一个人中奖的机率都一样。因此。最简单的做法是将 n 个人随机化排成一列,再取前 m 个人中奖就可以。
这个问题本身不难理解,但是关键的地方是等概率,还有一个隐性的条件,那就是不能重复取。
面临当前元素时。使用一个概率(这个概率可能是动态变化的。或者不变的)决定去留,若留,则与某个已选择的元素置换。以下再给出一种方法。
设 A 为源数组。B 为辅助数组(装入已选择的元素)。A 长度为 n。B 长度为 m。须要从 A 中取 m 个数字放入 B。使它们等概率。
遍历 A,在面临第 i 个元素 x 时,记 p 为还须要从 A 中选出的元素个数。q 为从 x 向后数,将 A 数完的个数。包含 x。决定 x 被选中的概率设置为 p/q。这也能够达到等概率。
依此类推,不管哪个元素被选中的概率都为 m/n。以下,我们证明随意一个元素被选中的概率都为 m/n。
假设按上面的思路去证明将非常复杂。可是有一个非常巧妙的证明方法。
我们看这个问题的模型,实际上,它就是一个抽奖模型,如今有一个箱子里面装着 n 张奖券,写着“中”。或“不中”,当中。写着“中”的有 m 张,如今问。第 k 次抽奖,中奖的概率为多少?这显然为 m/n!
还记得 "抽奖与顺序无关” 吗?于是。我们独立写出第 k 次中奖的概率的表达式:
C(m,1)*A(n-1,m-1) / A(n,m) = m/n。 复制代码
故,上面的操作方法,随意一个元素被选中的概率都为 m/n。
选用后缀,后缀字符串的前缀包含了字符串S的部分子串,只要求出字符串S的所有后缀就可间接的表示了S所有的子串。具体步骤如下:
public class Main{ //保存最长公共子串 static String result = ""; public static void main(String [] args) throws InterruptedException { Scanner sc = new Scanner(System.in); String s = sc.nextLine(); ArrayList<String> list = new ArrayList<String>(); //得到字符串的所有后缀 for(int i = s.length()-1;i>=0;i--) { list.add(s.substring(i)); } Collections.sort(list); int maxLen = 0; for(int i = 0;i<s.length()-1;i++) { int len = getComlen(list.get(i),list.get(i+1)); maxLen = Math.max(maxLen, len); } System.out.println(result+":"+maxLen); } //得到两个字符串最长公共长度 public static int getComlen(String str1,String str2) { int i; for(i =0;i<str1.length()&&i<str2.length();i++) { if(str1.charAt(i)!=str2.charAt(i)) { break; } } String temp = str1.substring(0,i); if(temp.length()>result.length()) result = temp; return i; } } 复制代码
单链表每k个节点为一组进行反转(最后不满k个时不反转)
leetcode 原题,提供其中的一种解法如下:
public class LinkReverse { public static Node mhead=null; public static Node mtail=null; public static Node newLink(int n){ Node head = new Node(); head.setData(1); head.setNext(null); Node tmp = head; for(int i=2;i<=n;i++){ Node newNode = new Node(); newNode.setData(i); newNode.setNext(null); tmp.setNext(newNode); tmp = newNode; } return head; } public static void main(String[] args) { Node node = newLink(10); pritNode(node); // Node node1 = reverseKLink(node,3); // Node node1 = reverse(node,2); Node node1 = reverseLink3(node,4); pritNode(node1); } public static void pritNode(Node head){ Node temp = head; while(temp !=null){ System.out.print(temp.getData()+"->"); temp = temp.getNext(); } System.out.println(); } /*public static Node reverseLink(Node head){ Node pre=null,cur=head,next=null; while(cur!=null){ next=cur.getNext(); cur.setNext(pre); pre=cur; cur=next; } return pre; }*/ /*public static Node reverseKLink(Node head,int k){ Node pre=null,cur=head,next=null; Node pnext=null,global_head=null; int i=0; Node tmp=null; while(cur!=null){ next = cur.getNext(); if(tmp==null) tmp=cur; //tmp记录当前组反转完最后一个节点 if(pnext!=null) pnext.setNext(cur); //pnext是上一组反转完最后一个节点 cur.setNext(pre); pre=cur; cur = next; i++; if(i>=k){ //当前组反转完成的时候 if(global_head==null){ global_head=pre; } if(pnext!=null){ //将上一组反转完的最后一个节点指向 当前组反转完后的第一个节点 pnext.setNext(pre); } pnext=tmp; //迭代 i=0; //新的一组反转时 关键数据初始化 tmp=null; pre=null; } } return global_head; }*/ //反转每组 public static void inreverse(Node left,Node right){ Node pre=null,cur=left,next=null; while(pre!=right){ next = cur.getNext(); cur.setNext(pre); pre=cur; cur=next; } if(mtail!=null) mtail.setNext(right); mhead=right; mtail=left; } //每k个节点为一组反转 public static Node reverseLink3(Node head,int k){ Node cur=head,global_head=head; int i=1; Node left=null,right=null; while(cur!=null){ if(i%k==1) left=cur; right=cur; cur=cur.getNext(); if(i%k==0){ inreverse(left,right); if(mtail!=null){ mtail.setNext(cur); } if(global_head==head){ global_head = mhead; } } i++; } return global_head; } } 复制代码
输入: 数据库中有几十万的国内 IP 段 (start_ip, end_ip) 一个待验证的 IP
输出: YES or NO
首先,整理 IP 段配置。为了方便 IP 进行比较,这里将 IP 转换为 long 格式。
得到类似如下的结果:
('16842752','16843007'),('16843264','16859135') 复制代码
接下来使用二分法查找即可,较为简单。
出自LeetCode第二十三题-合并n个有序链表 最简单粗暴的方法是建立一个集合,遍历所有链表,将其元素添加到集合中,将集合通过数组的方式升序排序,将其添加到一个新的链表中并返回。
这种方法的时间复杂度:o(n2)外层遍历一遍数组内层遍历链表的元素,即双层遍历,还有一个单层遍历,所以结果近似于o(n2);空间复杂度:o(n)定义了数组长度多的变量listNode,定义了集合长度的链表长度即o(n)。
考虑优化如上的方法,用分治的方法进行合并。
public ListNode mergeKLists(List<ListNode> lists) { // write your code here if (lists == null || lists.size() == 0) { return null; } ListNode dummy = new ListNode(0); ListNode p = dummy; Queue<ListNode> queue = new PriorityQueue<ListNode>(lists.size(), new Comparator<ListNode>() { @Override public int compare(ListNode o1, ListNode o2) { return o1.val - o2.val; } }); for (ListNode node : lists) { if (node != null) { queue.offer(node); } } while (!queue.isEmpty()) { ListNode node = queue.poll(); p.next = node; if (node.next != null) { queue.offer(node.next); } p = p.next; } return dummy.next; } public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(-1); ListNode p = dummy; ListNode p1 = l1; ListNode p2 = l2; while (p1 != null && p2 != null) { if (p1.val < p2.val) { p.next = p1; p1 = p1.next; } else { p.next = p2; p2 = p2.next; } p = p.next; } if (p1 != null) { p.next = p1; } if (p2 != null) { p.next = p2; } return dummy.next; } 复制代码
开放题和项目经历,需要根据自己的实际经验进行事先整理。
兼容处理,在接口层级进行兼容,加一层适配器。这个需要谨慎处理,如果项目规模大,维护特别麻烦。
这个要看平时的习惯,要有刨根问底的探索精神。多看些优秀的源码。
Java和Python两门语言都是目前非常热门的语言,主要有如下的区别:
总结之间失败的经验。
紧扣工作和学习两个维度回答:
这不仅是应付面试官,更是对自己人生的思考,很重要。
博客和开源项目都是加分项,有最好了,平时注意积累。
考察团队协作和问题解决的能力。
同上,注意总结之前的经验。
挑熟悉且大型的项目说,比较容易应对。
HR是想通过这个问题来考察你获取知识和经验的路径与方法。通过平时的阅读习惯来了解求职者的知识结构和求知习惯。
学习的方法可以根据自己的实际进行解答,比如说重复练习,强化记忆,科学的方法应用等等。学习的能力很重要,自己需要凸显这一点。
上述所列出的面试题只能是帮助各位同学理解大部分的问题,并不会面面俱到,这里推荐一些师弟的面试复习资料,仅供参考。
师弟拿下阿里、抖音、腾讯、美团的offer,目前入职阿里。首先给大家提供他自己准备的一份复习资料。
福利,后台回复“面试”,即可获取 PDF 文件。下面是参考的一些复习资料,内容质量很高,面试头条这样的一线大厂,算法也是必不可少,所以也给大家提供了经典算法的链接。
leetcode.com/problems/lo… leetcode.com/problems/la… leetcode.com/problems/ru… leetcode.com/problems/ma… leetcode.com/problems/nu… leetcode.com/problems/de… leetcode.com/problems/lo…
leetcode.com/problems/pa…
leetcode.com/problems/pa…
leetcode.com/problems/lo… leetcode.com/problems/ed… leetcode.com/problems/di… leetcode.com/problems/mi…
leetcode.com/problems/pa… leetcode.com/problems/pa…
leetcode.com/problems/co… leetcode.com/problems/co… leetcode.com/problems/co… leetcode.com/problems/pe… leetcode.com/problems/mi… leetcode.com/problems/la…
leetcode.com/problems/mi… leetcode.com/problems/mi… leetcode.com/problems/bu…
leetcode.com/problems/ma… leetcode.com/problems/ra… leetcode.com/problems/du… leetcode.com/problems/tr… leetcode.com/problems/ma… leetcode.com/problems/mi…
leetcode.com/problems/ta… leetcode.com/problems/lo… leetcode.com/problems/lo… leetcode.com/problems/ma…
leetcode.com/problems/be… leetcode.com/problems/be… leetcode.com/problems/be… leetcode.com/problems/be… leetcode.com/problems/be… leetcode.com/problems/be…
leetcode.com/problems/ou… leetcode.com/problems/kn…
leetcode.com/problems/pr… leetcode.com/problems/st…
leetcode.com/problems/gr… leetcode.com/problems/de… leetcode.com/problems/pe… leetcode.com/problems/co… leetcode.com/problems/lo… leetcode.com/problems/nu…
关于面经,大家多看看牛客网,里面很全面。
福利,整理的一份
基础知识的掌握是一个积累过程,面试虽然有很多的技巧,可以去恶补。但是如果我们在平时就能够保持良好的学习习惯,以及最重要的求知欲,会使得我们准备面试时能够事半功倍。
最后给大家留一道思考题,检验下自己学习之后的效果😀,欢迎在留言区写出你的想法,或者私信我交流。如有实在做不出来,笔者最后会提供思路和答案。
输入: 用户日志(time, user_id, login | logout)
输出:同时在线人数的峰值, 峰段(峰值的90%) (如 19:50到22:10, 峰值3亿,最低2.7亿)
面试合集