首页 > IT职业 > IT职场 > 百度提前批,有点难度!
来源:小林coding
今天分享百度提前批的 Java 后端开发面经,整体上考察的点挺多的,主要重点考察了网络i/o、网络协议、linux系统、mysql,Java 问的不多,可能是百度的后端开发的语言不是主要以 Java 为主,所以重点看面试者的计算机基础是否扎实。
除了这些知识点的拷打之外,还出了1 个大数据场景题目+1 个算法。
废话不多说,看看你能应对多少题?
select 和 poll 内部都是使用「线性结构」来存储进程关注的 Socket 集合。
在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。
很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。

主要是依赖Reactor 模式实现了高性能网络模式,这个是在i/o多路复用接口基础上实现的了网络模型。Reactor 翻译过来的意思是「反应堆」,这里的反应指的是「对事件反应」,也就是来了一个事件,Reactor 就有相对应的反应/响应。
Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下:
Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:
Redis 6.0 之前使用的 Reactor 模型就是单 Reactor 单进程模式。单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。

但是,这种方案存在 2 个缺点:
所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
Redis 是由 C 语言实现的,在 Redis 6.0 版本之前采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。
Netty 是采用了多 Reactor 多线程方案,如下图:

多 Reactor 多线程的方案优势:
Netty 是多 Reactor 多进程方案,不过方案与标准的多 Reactor 多进程有些差异。

具体差异表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
主要通过加密和身份校验机制来防范中间人攻击的。

中间人攻击的关键在于攻击者冒充服务器与客户端建立连接,并同时与服务器建立连接。但由于攻击者无法获得服务器的私钥,因此无法正确解密客户端发送的加密数据。同时,客户端会在建立连接时验证服务器的证书,如果证书验证失败或存在问题,客户端会发出警告或中止连接。

URL 进行解析,解析出域名、方法、资源等,然后生成 http 请求报文。发起DNS查询:当用户在浏览器中输入一个域名(如)后,操作系统会首先检查本地的DNS缓存,如果有对应的IP地址,则直接返回结果。如果缓存中没有对应的IP地址,操作系统会向本地DNS服务器发送一个DNS查询请求。
本地DNS服务器查询:本地DNS服务器收到DNS查询请求后,会先检查自己的缓存,如果有对应的IP地址,则直接返回结果给操作系统。如果本地DNS服务器没有缓存对应的IP地址,它会向根DNS服务器发送一个迭代查询请求。
根DNS服务器查询:根DNS服务器是顶级DNS服务器,它存储了全球顶级域名服务器的信息。当根DNS服务器收到迭代查询请求后,它会根据请求的顶级域名(如.com)返回对应的顶级域名服务器的IP地址给本地DNS服务器。
顶级域名服务器查询:本地DNS服务器收到根DNS服务器返回的顶级域名服务器的IP地址后,会向顶级域名服务器发送查询请求。顶级域名服务器根据请求的域名(如baidu.com)返回该域名对应的权威域名服务器的IP地址。
权威域名服务器查询:本地DNS服务器收到顶级域名服务器返回的权威域名服务器的IP地址后,会向权威域名服务器发送查询请求。权威域名服务器是负责管理该域名下所有主机记录的服务器,它会根据请求的域名返回对应的主机记录的IP地址。
返回结果:本地DNS服务器收到权威域名服务器返回的主机记录的IP地址后,会将结果返回给操作系统。操作系统将IP地址返回给浏览器,浏览器根据IP地址建立与服务器的TCP连接,并发起HTTP请求。
分布式拒绝服务(DDoS)攻击是通过大规模互联网流量淹没目标服务器或其周边基础设施,以破坏目标服务器、服务或网络正常流量的恶意行为。
DDoS 攻击是通过连接互联网的计算机网络进行的。这些网络由计算机和其他设备(例如 IoT 设备)组成,它们感染了恶意软件,从而被攻击者远程控制。这些个体设备称为机器人(或僵尸),一组机器人则称为僵尸网络。

一旦建立了僵尸网络,攻击者就可通过向每个机器人发送远程指令来发动攻击。当僵尸网络将受害者的服务器或网络作为目标时,每个机器人会将请求发送到目标的 IP 地址,这可能导致服务器或网络不堪重负,从而造成对正常流量的拒绝服务。由于每个机器人都是合法的互联网设备,因而可能很难区分攻击流量与正常流量。
常见的DDoS攻击包括以下几类:
为了防范DDoS攻击,可以采取以下措施:
增强网络基础设施:提升网络带宽、增加服务器的处理能力和承载能力,通过增强基础设施的能力来抵御攻击。
使用防火墙和入侵检测系统:配置防火墙规则,限制不必要的网络流量,阻止来自可疑IP地址的流量。入侵检测系统可以帮助及时发现并响应DDoS攻击。
流量清洗和负载均衡:使用专业的DDoS防护服务提供商,通过流量清洗技术过滤掉恶意流量,将合法流量转发给目标服务器。负载均衡可以将流量均匀地分发到多台服务器上,减轻单一服务器的压力。
配置访问控制策略:限制特定IP地址或IP段的访问,设置访问频率限制,防止过多请求集中在单个IP上。

Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。
匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。
那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。
与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。
可以通过 lsof 或者 netstate 命令查看,比如查看 80 端口。
lsof :
[root@xiaolin ~]# lsof -i :80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 929 root 6u IPv4 15249 0t0 TCP *:http (LISTEN)
nginx 929 root 7u IPv6 15250 0t0 TCP *:http (LISTEN)
nginx 934 nginx 6u IPv4 15249 0t0 TCP *:http (LISTEN)
nginx 934 nginx 7u IPv6 15250 0t0 TCP *:http (LISTEN)
AliYunDun 16507 root 10u IPv4 40212783 0t0 TCP xiaolin:41830->100.100.30.26:http (ESTABLISHED)
netstate:
[root@xiaolin ~]# netstat -napt | grep 80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 929/nginx: master p
可以使用sed命令。以下是一个示例:
sed -i 's/旧字符串/新字符串/g' 文件名
在上面的命令中,-i选项表示直接在原始文件中进行修改,而不是输出到标准输出。s/旧字符串/新字符串/g是替换操作的模式,其中旧字符串是要替换的字符串,新字符串是替换后的新字符串。g表示全局替换,即一行中出现多次的旧字符串都会被替换。
请注意,这将直接修改原始文件,如果需要备份原始文件,可以在-i选项后面指定一个备份文件的扩展名,例如-i.bak,这将在替换前备份原始文件。
例如,假设要将文件example.txt中的字符串Hello替换为Hi,可以运行以下命令:
sed -i 's/Hello/Hi/g' example.txt
要查找请求耗时最高的10条记录,可以使用以下Shell命令:
sort -k3 -nr 日志文件 | head -n 10
在上面的命令中,sort -k3 -nr用于按第三列(请求耗时)进行倒序排序。-k3表示按第三列排序,-n表示按数字排序,-r表示倒序排序。然后,使用head -n 10来获取排序后的前10行,即耗时最高的10条记录。
将命令中的“日志文件”替换为实际的日志文件路径,即可查找到请求耗时最高的10条记录。
思路如下:
ps -T -p<> <进程ID> 命令找到进程中占用比较高的线程
接下来,举个具体的例子来说明这四种隔离级别,有一张账户余额表,里面有一条账户余额为 100 万的记录。然后有两个并发的事务,事务 A 只负责查询余额,事务 B 则会将我的余额改成 200 万,下面是按照时间顺序执行两个事务的行为:

在不同隔离级别下,事务 A 执行过程中查询到的余额可能会不同:
这四种隔离级别具体是如何实现的呢?
确认慢查询:首先,通过MySQL的慢查询日志或性能监控工具,确认哪些SQL查询较慢,需要进行调优。
分析执行计划:通过使用EXPLAIN关键字,可以获取SQL查询的执行计划。执行计划可以告诉您MySQL是如何执行查询的,包括使用的索引、连接方式等。分析执行计划可帮助您理解查询的性能瓶颈所在。
优化查询语句:根据执行计划的分析结果,考虑对查询语句进行优化。例如,添加合适的索引、调整查询条件、避免全表扫描等。优化查询语句可以提高查询性能。
优化数据库结构:有时,慢查询的性能问题可能与数据库结构有关。考虑对表结构进行优化,例如拆分大表、规范化设计等。合理的数据库结构可以提高查询性能。
缓存和查询缓存:对于一些频繁查询的结果,可以考虑使用缓存机制,避免重复的查询操作。
explain 是查看 sql 的执行计划,主要用来分析 sql 语句的执行过程,比如有没有走索引,有没有外部排序,有没有索引覆盖等等。
如下图,就是一个没有使用索引,并且是一个全表扫描的查询语句。

对于执行计划,参数有:
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
在这些情况里,all 是最坏的情况,因为采用了全表扫描的方式。index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。所以,要尽量避免全表扫描和全索引扫描。
range 表示采用了索引范围扫描,一般在 where 子句中使用 < 、>、in、between 等关键词,只检索给定范围的行,属于范围查找。从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。
ref 类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀,返回数据返回可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
eq_ref 类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。比如,对两张表进行联查,关联条件是两张表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 进行执行计划查看的时候,type 就会显示 eq_ref。
const 类型表示使用了主键或者唯一索引与常量值进行比较,比如 select name from product where id=1。
需要说明的是 const 类型和 eq_ref 都使用了主键或唯一索引,不过这两个类型有所区别,const 是与常量进行比较,查询效率会更快,而 eq_ref 通常用于多表联查中。
extra 显示的结果,这里说几个重要的参考指标:
synchronized:是Java内置的关键字,用于实现互斥锁。在多线程环境下,通过对代码块或方法添加synchronized关键字,可以确保同一时刻只有一个线程执行该代码块或方法。适用于对共享资源的访问进行同步控制的场景。
ReentrantLock:是Java.util.concurrent包提供的可重入锁实现。与synchronized相比,ReentrantLock提供了更灵活的锁定方式,例如可以手动控制锁的获取和释放、支持公平锁等。适用于需要更高级别控制的场景。
ReadWriteLock:是Java.util.concurrent包提供的读写锁接口。读写锁允许多个线程同时读共享资源,但在写操作时需要独占锁。适用于读多写少的场景,可以提高并发性能。
StampedLock:是Java.util.concurrent包提供的乐观读写锁。与ReadWriteLock相比,StampedLock提供了更高的并发性能,但使用方式较为复杂。适用于读多写少、读操作频繁的场景。
AtomicInteger:是Java.util.concurrent.atomic包提供的原子整数类。通过使用AtomicInteger类,可以实现在多线程环境下对整数值的原子操作,避免线程安全问题。适用于对整数值进行原子操作的场景。
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
应用场景:
逆向代码,例如反编译
与注解相结合的框架,如 Retrofit
单纯的反射机制应用框架,例如 EventBus(事件总线)
动态生成类框架 例如Gson
方法:分治+hashmap
参考资料:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70024923/viewspace-2978579/ ,如需转载,请注明出处,否则将追究法律责任。