本篇文章给大家整理分享28道PHP面试题(附答案分享),带你梳理基础知识,有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
过完年之后打算寻找新的工作机会,发现之前自己对于很多基础的面试理解和学习不够深刻,为了鼓励自己持续前进所以最近开始在论坛和搜索引擎上开始学习和总结相关知识,其中有一些题目时论坛里面一些前辈分享过的题目或者答案,还有一部分时自己最近面试遇到的问题,基于自己的理解和前辈们的分享归档了一部分,所以分享出来,希望对其他的小伙伴们也有帮助,同时也希望能收到大佬们对于理解有误的地方的指导,最近一段时间会持续更新
本论坛的文章参考的比较多,优先列出来大家参考,如下: https://learnku.com/articles/63520 https://learnku.com/php/t/47623 https://learnku.com/articles/28772
一、php 数组底层实现原理
1、底层实现是通过散列表(hash table) + 双向链表(解决hash冲突)
-
hashtable:将不同的关键字(key)通过映射函数计算得到散列值(Bucket->h) 从而直接索引到对应的Bucket
-
hash表保存当前循环的指针,所以foreach 比for更快
-
Bucket:保存数组元素的key和value,以及散列值h
2、如何保证有序性
-
1. 散列函数和元素数组(Bucket)中间添加一层大小和存储元素数组相同的映射表。
-
2. 用于存储元素在实际存储数组中的下标
-
3. 元素按照映射表的先后顺序插入实际存储数组中
-
4. 映射表只是原理上的思路,实际上并不会有实际的映射表,而是初始化的时候分配Bucket内存的同时,还会分配相同数量的 uint32_t 大小的空间,然后将 arData 偏移到存储元素数组的位置。
3、解决hash重复(php使用的链表法):
-
1. 链表法:不同关键字指向同一个单元时,使用链表保存关键字(遍历链表匹配key)
-
2. 开放寻址法:当关键字指向已经存在数据的单元的时候,继续寻找其他单元,直到找到可用单元(占用其他单元位置,更容易出现hash冲突,性能下降)
4、基础知识
-
链表:队列、栈、双向链表、
-
链表 :元素 + 指向下一元素的指针
-
双向链表:指向上一元素的指针 + 元素 + 指向下一元素的指针
参考:
算法的时间与空间复杂度(一看就懂)
https://zhuanlan.zhihu.com/p/50479555
LeetCode0:学习算法必备知识:时间复杂度与空间复杂度的计算
https://cloud.tencent.com/developer/article/1769988
二、冒泡排序的时间复杂度和空间复杂度
1、代码实现
$arr = [2, 4, 1, 5, 3, 6]; for ($i = 0; $i < (count($arr)); $i++) { for ($j = $i + 1; $j < (count($arr)); $j++) { if ($arr[$i] <= $arr[$j]) { $temp = $arr[$i]; $arr[$i] = $arr[$j]; $arr[$j] = $temp; } } } result : [6,5,4,3,2,1]
2、计算原理
-
第一轮:将数组的第一个元素和其他所有的元素进行比较,哪个元素更大,就换顺序,从而冒泡出第一大(最大)的元素
-
第一轮:将数组的第二个元素和其他所有的元素进行比较(第一大已经筛选出来不用继续比较了),哪个元素更大,就换顺序,从而冒泡出第二大的元素
-
… 依次类推,冒泡从大到小排序的数组
平均时间复杂度:O(n^2)
;
最优时间复杂度:O(n)
,需要加判断,第一次循环如果一次都没有交换就直接跳出循环
空间复杂度:O(1)
,交换元素的时候的临时变量占用的空间
最优空间复杂度:O(1)
,排好序,不需要交换位置
3、时间复杂度和空间复杂度
时间复杂度:全程为渐进时间复杂度,估算对处理器的使用效率(描述算法的效率趋势,并不是指算法具体使用的时间,因为不同机器的性能不一致,只是一种效率计算的通用方法)
表示方法:大O符号表示法
复杂度量级:
-
常数阶O(1)
-
线性阶O(n)
-
平方阶O(n²)
-
立方阶O(n³)
-
K次方阶O(n^k)
-
指数阶(2^n)
-
对数阶O(logN)
-
线性对数阶O(nlogN)
时间复制类型:
-
最好时间复杂度
-
最坏时间复杂度
-
平均时间复杂度
-
均摊时间复杂度
空间复杂度:全程渐进空间复杂度,估算对计算机内存的使用程度(描述算法占用的存储空间的趋势,不是实际占用空间,同上)
参考:
算法的时间与空间复杂度(一看就懂)
https://zhuanlan.zhihu.com/p/50479555
LeetCode0:学习算法必备知识:时间复杂度与空间复杂度的计算
https://cloud.tencent.com/developer/article/1769988
三、网络七层协议及 TCP 和 TCP
应用层、表示层、会话层、传输层、网络层、(数据)链路层、物理层
记忆套路:
首字:应表会传(物链网)
第一个字:应用层(出现次数多,易忆)
前四个正向:应表 – 会传
后三个反向:物联网谐音比网链物更好记
四、TCP 和 UDP 的特点和区别
1、都是属于传输层协议
2、TCP
-
面向连接,所以只能一对一
-
面向字节流传输
-
数据可靠,不丢失
-
全双工通信
3、UDP(根据TCP特点反记)
-
无连接,支持一对一,一对多,多对多
-
面向保温传输
-
首部开销小,数据不一定可靠但是速度更快
参考:
github:colinlet/PHP-Interview-QA =>PHP 面试问答 网络篇
https://github.com/colinlet/PHP-Interview-QA/blob/master/docs/01.网络.md#1-计算机网络体系结构
五、TCP 的三次握手和四次挥手
1、三次握手:
-
1)第一次:客户端发送SYN = 1,seq = client_isn
作用:
客户端:无
服务端:确认自己的接收功能和客户端的发送功能
-
2)第二次:服务端发送SYN = 1,seq = server_isn,ACK =client_isn +1
作用:
客户端:确认自己发送和接收都正常,确认服务端的接收和发送正常
服务端:确认自己的接收正常,确认服务端的发送正常(这时候服务端还不能确认客户端接收是否正常)
-
3)第三次:客户端发送SYN = 0, ACK = server_isn+1,seq =client_isn+1
作用:双方确认互相的接收和发送正常,建立连接
2、四次挥手
-
1)第一次:客户端发送FIN
作用:告诉服务端我没有数据发送了(但是还能接收数据)
-
2)第二次:服务端发送ACK
作用:告诉客户端收到请求了,可能服务端可能还有数据需要发送,所以客户端收到进入FIN_WAIT状态,等服务端数据传输完之后发送FIN
-
3)第三次:服务端发送FIN
作用:服务端告诉客户端我发送完了,可以关闭连接了。
-
4)第四次:客户端发送ACK
作用:客户端收到FIN之后,担心服务端不知道要关闭,所以发送一个ACK,进入TIME_WAIT,等待2MSL之后如果没有收到回复,证明服务端已经关闭了,这时候客户端也关闭连接。
注意:
-
当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据
-
最后需要等待2MSL是因为网络是不可靠的,如果服务端没有收到最后一次ACK,服务端会重新放FIN包然后等客户端再次发送ACK包然后关闭(所以客户端最后发送ACK之后不能立即关闭连接)
参考:
github:colinlet/PHP-Interview-QA =>PHP 面试问答 网络篇
https://github.com/colinlet/PHP-Interview-QA/blob/master/docs/01.网络.md#1-计算机网络体系结构
TCP 协议中的三次握手和四次挥手 (图解)
https://blog.csdn.net/whuslei/article/details/6667471
“三次握手,四次挥手” 你真的懂吗?
https://zhuanlan.zhihu.com/p/53374516
TCP 的特性
https://hit-alibaba.github.io/interview/basic/network/TCP.html
六、HTTP 状态码
1、状态码分类
-
– 1xx:信息,服务器收到请求,需要请求者继续操作
-
– 2xx:成功
-
– 3xx:重定向
-
– 4xx:客户端错误
-
– 5xx:服务端错误
2、常用状态码
-
200:请求成功
-
301:永久重定向
-
302:临时移动
-
400 bad request:客户端请求语法错误
-
401 unauthorized:客户端没有权限
-
403 forbidden:服务器拒绝客户端请求
-
404 not found:客户端请求资源不存在
-
500 Internal Server Eerro:服务器内部错误
-
502 bad gateway:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
-
503 Service Unavailable 超载或系统维护
-
504 Gateway timeout:网关超时
3、502 的原因及解决方法
原因:nginx将请求提交给网关(php-fpm)处理异常导致
1)fastcgi 缓冲区设置过小
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
2)php-cgi的进程数设置过少
查看FastCgi进程数:netstat -anpo | grep "php-cgi"| wc -l
调整参数最大子进程数:max_children
一般按照单个进程20M计算需要需要设置的子进程数
3)max_requests(内存溢出或频繁重启)
参数指明每个children最多能处理的请求数量,到达最大值之后会重启children。
设置过小可能导致频繁重启children:
php将请求轮询给每个children,在大流量的场景下,每一个children 到达最大值的时间差不多,如果设置过小可能多个children 在同一时间关闭,nginx无法将请求转发给php-fpm,cpu降低,负载变高。
设置过大可能导致内存泄露
4)php执行时间超过nginx等待时间
fastcgi_connect_timeout
fastcgi_send_timeout
fastcgi_read_timeout
5)fastcgi执行时间
max_execution_time
参考:
php+php-fom+nginx 配置参数调优详解
http://www.voycn.com/article/phpphp-fomnginxpeizhicanshudiaoyouxiangjie
nginx 报错 502
https://www.cnblogs.com/fengzhongzhuzu/p/9193355.html
七、http 和 HTTPS 的区别
1、端口:http 80; https :443
2、http无状态,https是有http + ssl构建的可进行加密传输的协议
3、http明文传输,https加密传输
4、http更快,三次握手三个包,https 需要12个包(3个tcp包+9个ssl握手包)
八、redis 分布式锁及问题
1、实现:
加锁:setnx
解锁:del
锁超时:expire
2、可能出现的问题
-
1)setnx 和expire非原子性问题(加锁之后还没来得及设置超时就挂了)
解决方案:
Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令
-
2)超时误删其他进程锁。(A进程执行超时,导致锁释放,这时候B进程获取锁开始处理请求,这时候A进程处理完成,会误删B进程的锁)
解决方案:只能删除自己进程的锁 (lua脚本防止B进程获取过期锁之后误删A进程的锁)
-
3)并发场景,A进程执行超时导致锁释放,这时候B进程获取到锁。
解决方案:开启守护进程,给当前进程要过期的锁延时。
-
4)单点实例安全问题
单机宕机之后导致所有客户端无法获取锁
解决:
主从复制,因为是异步完成的所以无法完全实现解决
参考:
使用 Redis 作为分布式锁的一些注意点
https://www.cnblogs.com/gxyandwmm/p/9588383.html
面试官:你真的了解 Redis 分布式锁吗?
https://segmentfault.com/a/1190000038988087
九、redis 为什么是单线程?为什么快?
推荐阅读:https://www.php.cn/redis/475918.html
十、redis 的数据类型及应用场景
1、string :
普通的key/value存储
2、hash:
hashmap:键值队集合,存储对象信息
3、list:
双向链表:消息队列
4、set:
value永远为null的hashMap:无序集合且不重复:计算交集、并集、差集、去重值
5、zset:
有序集合且不重复:hashMap(去重) + skiplist跳跃表(保证有序):排行榜
参考:
Redis 五种数据类型及应用场景
https://www.cnblogs.com/jasonZh/p/9513948.html
十一、redis 实现持久化的方式及原理、特点
1、RDB持久化(快照):指定时间间隔内的内存数据集快照写入磁盘
1)fork一个子进程,将快照内容写入临时RDB文件中(dump.rdb),当子进程写完快照内容之后新的文件替换老的文件
2)整个redis数据库只包含一个备份文件
3)性能最大化,只需要fork子进程完成持久化工作,减少磁盘IO
4)持久化之前宕机可能会导致数据丢失
2、AOF持久化 :以日志的形式记录服务器的所有的写、删除操作
1)每接收到一个写的命令用write函数追加到文件appendonly.aof
2)持久化的文件会越来越大,存在大量多余的日志(0 自增100次到100,会产生100条日志记录)
3)可以设置不同的fsync策略
-
appendfsync everysec :1s一次,最多丢失1s的数据(默认)
-
appendfsync always :每次变动都会执行一次
-
appendfsync no :不处理
4)AOF文件太大之后会进行重写:压缩AOF文件大小
-
fork一个子进程,将redis内地数据对象的最新状态写入AOF临时文件(类似rdb快照)
-
主进程收到的变动会先写入内存中,然后同步到老的AOF文件中(重写失败之后也能保证数据完整性)
-
子进程完成重写之后会将内存中的新变动同步追加到AOF的临时文件中
-
父进程将临时AOF文件替换成新的AOF文件,并重命名。之后收到的新命令写入到新的文件中
参考:
Redis 专题:万字长文详解持久化原理
https://segmentfault.com/a/1190000039208726
Redis 持久化
https://segmentfault.com/a/1190000002906345
RDB 和 AOF 持久化的原理是什么?我应该用哪一个?它们的优缺点?
https://segmentfault.com/a/1190000018388385
十二、秒杀设计流程及难点
1、静态缓存
2、nginx 负载均衡
三种方式:DNS轮询、IP负债均衡、CDN
3、限流机制
方式:ip限流、接口令牌限流、用户限流、header动态token(前端加密,后端解密)
4、分布式锁
方式:
-
setnx + expire (非原子性,redis2.6 之后set保证原子性)
-
释放锁超时 (开启守护进程自动续时间)
-
过期锁误删其他线程(requestId验证或者lua脚本保证查 + 删的原子性)
5、缓存数据
方式:
-
缓存击穿:缓存数据预热 + 布隆过滤器/空缓存
-
缓存雪崩:缓存设置随机过期时间,防止同一时间过期
6、库存及订单
-
扣库存
-
redis 自减库存,并发场景下可能导致负数,影响库存回仓:使用lua脚本保证原子性
-
redis预扣库存之后,然后使用异步消息创建订单并更新库存变动
-
数据库更新库存使用乐观锁:where stock_num – sell_num > 0
-
添加消息发送记录表及重试机制,防止异步消息丢失
-
-
创建订单
-
前端建立websocket连接或者轮询监听订单状态
-
消费验证记录状态,防止重复消费
-
-
回仓
-
创建订单之后发送延时消息,验证订单支付状态及库存是否需要回仓
-
十三、防 sql 注入
1、过滤特殊字符
2、过滤数据库关键字
3、验证数据类型及格式
4、使用预编译模式,绑定变量
十四、事务隔离级别
1、标准的sql隔离级别实现原理
-
未提交读:其他事务可以直接读到没有提交的:脏读
-
事务对当前被读取的数据不加锁
-
在更新的瞬间加行级共享锁到事务结束释放
-
-
提交读:事务开始和结束之间读取的数据可能不一致,事务中其他事务修改了数据:不可重复度
-
事务对当前读取的数据(被读到的时候)行级共享锁,读完释放
-
在更新的瞬间加行级排他锁到事务结束释放
-
-
可重复读:事务开始和结束之前读取的数据保持一致,事务中其他事务不能修改数据:可重复读
-
事务对当前读到的数据从事务一开始就加一个行级共享锁
-
在更新的瞬间加行级排他锁到事务结束释放
-
其他事务再事务过程中可能会新增数据导致幻读
-
-
串行化
-
事务读取数据时加表级共享锁
-
事务更新数据时加表级排他锁
-
2、innodb的事务隔离级别及实现原理(!!和上面的不一样,区分理解一个是隔离级别 一个是!!事务!!隔离级别)
1)基本概念
-
mvcc:多版本并发控制:依赖于undo log 和read view
-
让数据都读不会对数据加锁,提高数据库并发处理能力
-
写操作才会加锁
-
一条数据有多个版本,每次事务更新数据的时候会生成一个新的数据版本,旧的数据保留在undo log
-
一个事务启动的时候只能看到所有已经提交的事务结果
-
-
当前读:读取的是最新版本
-
快照读:读取的是历史版本
-
间隙锁:间隙锁会锁住一个范围内的索引
-
update id between 10 and 20
-
无论是否范围内是否存在数据,都会锁住整个范围:insert id = 15,将被防止
-
只有可重复读隔离级别才有间隙锁
-
-
next-key lock:
-
索引记录上的记录锁+ 间隙锁(索引值到前一个索引值之间的间隙锁)
-
前开后闭
-
阻止幻读
-
2)事务隔离级别
-
未提交读
-
事务对当前读取的数据不加锁,都是当前读
-
在更新的瞬间加行级共享锁到事务结束释放
-
-
提交读
-
事务对当前读取的数据不加锁,都是快照读
-
在更新的瞬间加行级排他锁到事务结束释放
-
-
可重复读
-
事务对当前读取的数据不加锁,都是快照读
-
事务再更新某数据的瞬间,必须加行级排他锁(Record 记录锁、GAP间隙锁、next-key 锁),事务结束释放
-
间隙锁解决的是幻读问题
-
主从复制的情况下 ,如果没有间隙锁,master库的A、B进程
-
A进程 delete id < 6 ;然后还没有commit
-
B进程insert id = 3,commit
-
A进程提交commit
-
该场景下,主库会存在一条id =3 的记录,但是binlog里面是先删除再新增就会导致从库没有数据,导致主从的数据不一致
-
-
MVCC的快照解决的是不可重复读问题
-
-
串行化
-
事务读取数据时加表级,当前读
-
事务更新数据时加表级排他锁
-
参考:
深入理解 MySQL 中事务隔离级别的实现原理
https://segmentfault.com/a/1190000025156465
小胖问我:MySQL 事务与 MVCC 原理
https://segmentfault.com/a/1190000039809030
快照在 MVCC 中是怎么工作的?
https://www.jianshu.com/p/6c4454ffecc3
MVCC 我知道,但是为什么要设计间隙锁?
https://www.jianshu.com/p/fbec6d1fa16c
间隙锁和 next-key lock
https://www.jianshu.com/p/d1aba64b5c03
十五、索引原理
索引就是帮助数据库高效查找数据的存储结构,存储再磁盘中,需要消耗磁盘IO
1、存储引擎
-
myisam 支持表锁,索引和数据分开存储适合跨服务器迁移
-
innodb 支持行锁,索引和数据存储再一个文件
2、索引类型
-
hash索引
-
适合精确查询且效率高
-
无法排序、不适合范围查询
-
hash冲突的情况下需要遍历链表(php数组的实现原理、redis zset 的实现原理类似)
-
-
b-tree、b+tree
-
b-tree 和b+tree的去区别
-
b+tree 的数据全部存储在叶子节点,内部节点只存key,一次磁盘IO能获取到
-
-