一、php 数组底层实现原理二、冒泡排序的时间复杂度和空间复杂度三、网络七层协议及 TCP 和 TCP四、TCP 和 UDP 的特点和区别五、TCP 的三次握手和四次挥手六、HTTP 状态码七、http 和 HTTPS 的区别八、redis 分布式锁及问题九、redis 为什么是单线程?为什么快?十、redis 的数据类型及应用场景十一、redis 实现持久化的方式及原理、特点十二、秒杀设计流程及难点十三、防 sql 注入十四、事务隔离级别十五、索引原理十六、分表 (分库) 的策略十七、select 和 update 的执行流程十八、binlog 的作用和三种格式十九、主从同步(主从复制)的原理和问题及读写分离二十、死锁二十一、Mysql 优化大分页查询 limit 100000 (offset),10 (page\_sie)二十二、redis 缓存和 mysql 数据一致性二十三、redis 中的 connect 和 pconnect二十四、redis zset 有序集合使用 skiplist 的原理二十五、redis 的过期删除和淘汰机制二十六、redis 常见问题及解决方案二十七、php-fpm 详解及生命周期二十八、Nginx 和 php 之间的通信二十九、web 漏洞及问题
本篇文章给大家整理分享28道PHP面试题(附答案分享),带你梳理基础知识,有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
相关推荐:2022年PHP面试题大汇总(收藏)
过完年之后打算寻找新的工作机会,发现之前自己对于很多基础的面试理解和学习不够深刻,为了鼓励自己持续前进所以最近开始在论坛和搜索引擎上开始学习和总结相关知识,其中有一些题目时论坛里面一些前辈分享过的题目或者答案,还有一部分时自己最近面试遇到的问题,基于自己的理解和前辈们的分享归档了一部分,所以分享出来,希望对其他的小伙伴们也有帮助,同时也希望能收到大佬们对于理解有误的地方的指导,最近一段时间会持续更新
一、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、基础知识
- 链表:队列、栈、双向链表、
- 链表 :元素 + 指向下一元素的指针
- 双向链表:指向上一元素的指针 + 元素 + 指向下一元素的指针
参考:
二、冒泡排序的时间复杂度和空间复杂度
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)
时间复制类型:
- 最好时间复杂度
- 最坏时间复杂度
- 平均时间复杂度
- 均摊时间复杂度
空间复杂度:全程渐进空间复杂度,估算对计算机内存的使用程度(描述算法占用的存储空间的趋势,不是实际占用空间,同上)
参考:
三、网络七层协议及 TCP 和 TCP
应用层、表示层、会话层、传输层、网络层、(数据)链路层、物理层
记忆套路:
首字:应表会传(物链网)
第一个字:应用层(出现次数多,易忆)
前四个正向:应表 - 会传
后三个反向:物联网谐音比网链物更好记
四、TCP 和 UDP 的特点和区别
1、都是属于传输层协议
2、TCP
- 面向连接,所以只能一对一
- 面向字节流传输
- 数据可靠,不丢失
- 全双工通信
3、UDP(根据TCP特点反记)
- 无连接,支持一对一,一对多,多对多
- 面向保温传输
- 首部开销小,数据不一定可靠但是速度更快
五、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之后不能立即关闭连接)
六、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
参考:
七、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.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 实现持久化的方式及原理、特点
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文件,并重命名。之后收到的新命令写入到新的文件中
参考:
十二、秒杀设计流程及难点
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的快照解决的是不可重复读问题
- 串行化
- 事务读取数据时加表级,当前读
- 事务更新数据时加表级排他锁
参考:
十五、索引原理
索引就是帮助数据库高效查找数据的存储结构,存储再磁盘中,需要消耗磁盘IO
1、存储引擎
- myisam 支持表锁,索引和数据分开存储适合跨服务器迁移
- innodb 支持行锁,索引和数据存储再一个文件
2、索引类型
- hash索引
- 适合精确查询且效率高
- 无法排序、不适合范围查询
- hash冲突的情况下需要遍历链表(php数组的实现原理、redis zset 的实现原理类似)
- b-tree、b+tree
- b-tree 和b+tree的去区别
- b+tree 的数据全部存储在叶子节点,内部节点只存key,一次磁盘IO能获取到更多的节点
- b-tree 的内部节点和叶子节点都存储key和数据,查找数据不需要找到叶子节点,内部节点可以直接返回数据
- b+tree 增加了叶子节点到相邻节点的指针,方便返回查询遍历
- 聚簇索引和非聚簇索引
- 概念
- 聚簇索引 :索引和数据存储在一个节点
- 非聚簇索引:索引和数据分开存储,通过索引找到数据实际存储的地址
- 详解:
- innodb 使用的聚簇索引,且默认主键索引为聚簇索引(没有主键索引的时候,选择一个非空索引,还没有则隐式的主键索引),辅助索引指向聚簇索引位置,然后在找到实际存储地址
- myisam 使用非聚簇索引,所有的索引都只需要查询一次就能找到数据
- 聚簇索引的优势和略势
1\. 索引和数据在一起,同一页的数据会被缓存到(buffer)内存中,所以查看同一页数据的时候只需要从内存中取出,
2\. 数据更新之后之只需要维护主键索引即可,辅助索引不受影响
3\. 辅助索引存的是主键索引的值,占用更多的物理空间。所以会受到影响
4\. 使用随机的UUID,数据分布不均匀,导致聚簇索引可能扫全表,降低效率,所以尽量使用自增主键id
十六、分表 (分库) 的策略
1、流程
评估容量和分表数量-> 根据业务选定分表key->分表规则(hash、取余、range)->执行->考虑扩容问题
2、水平拆分
- 根据字段水平拆分为多个表
- 每个表的结构相同
- 所有分表的合集是全量数量
3、垂直拆分
- 根据字段垂直拆分
- 表结构不一样,分表的同一个关联行是一条完整的数据
- 扩展表,热点字段和非热点字段的拆分(列表和详情的拆分)
- 获取数据时,尽量避免使用join,而是两次查询结果组合
4、问题
- 跨库join问题
- 全局表:需要关联部分系统表的场景
- 冗余法:常用字段进行冗余
- 组装法:多次查询的结果进行组装
- 跨节点的分页、排序、函数问题
- 事务一致性
- 全局主键id
- 使用uuid -> 会降低聚簇索引效率
- 使用分布式自增id
- 扩容问题
- 升级从库
- 从库升级为主库,数据一致,只需要删除冗余数据即可
- 成倍扩容:需要在加一倍从库
- 双写迁移:
- 新数据进行双写,同时写进新老数据库
- 旧数据复制到新数据库
- 以老数据库为准,验证数据一致性之后删除冗余数据
十七、select 和 update 的执行流程
1、mysql 构成
- server层:连接器->缓存器->分析器(预处理器)->优化器->执行器
- 引擎层 : 查询和存储数据
2、select 执行过程
- 客户端发送请求,建立连接
- server层查找缓存,命中直接返回,否则继续
- 分析七分析sql语句以及预处理(验证字段合法性及类型等)
- 优化器生成执行计划
- 执行器调用引擎API查询结果
- 返回查询结果
3、update执行过程
- 基础概念
- buffer pool(缓存池),在内存中,下次读取同一页的数据的时候可以直接从buffer pool中返回(innodb的聚簇索引)
- 更新数据的时候先更新buffer pool,然后在更新磁盘
- 脏页:内存中的缓存池更新了,但是没有更新磁盘
- 刷脏:inndb 中有一个专门的进程将buffer pool的数据写入磁盘,每隔一段时间将多个修改一次性写入磁盘
- redo log 和 binlog
- redo log(重做日志),innodb特有的日志,物理日志,记录修改
- redo log是重复写,空间固定且会用完,会覆盖老日志
- binlog 是server层共有的日志,逻辑日志,记录语句的原始逻辑
- binlog 是追加写到一定大小切换到下一个,不会覆盖以前的日志
- redo log主要是用来恢复崩溃,bin log是用来记录归档的二进制日志
- redo log只能恢复短时间内的数据,binlog可以通过设置恢复更大的数据
- WAL(write-ahead-logging)先写日志方案
- 记录日志是顺序IO
- 直接写入磁盘(刷盘)是随机IO,因为数据是随机的,可能分布在不同的扇区
- 顺序IO的效率更高,先写入修改日志,可以延迟刷盘时机,提高吞吐量
- redo log 刷盘机制,check point
- redo log大小固定,循环写入
- redo log 就像一个圆圈,前面是check point (到这个point就开始覆盖老的日志),后面是write point (当前写到的位置)
- write point 和check point 重叠的时候就证明redo log 满了,需要开始同步redo log 到磁盘中了
- 执行步骤(两阶段提交 - 分布式事务,保证两个日志的一致性)
- 分析更新条件,查找需要更新的数据(会用到缓存)
- server 调用引擎层的API,Innodb 更新数据到内存中,然后写入redo log,然后进入prepare
- 引擎通知server层开始提交数据
- server层写入binlog 日志,并且调用innodb 的接口发出commit请求
- 引擎层收到请求之后提交commit
- 宕机后数据崩溃恢复规则
- 如果redo log 状态为commit ,则直接提交
- 如果redo log 状态为prepare,则判断binlog 中的事务是否commit,是则提交,否则回滚
- 如果不使用两次提交的错误案例(update table\_x set value = 10 where value = 9)
- 先redo log 再写入binlog
- 先写binlog 再写redo log
1\. redo log 写完之后,binlog没写完,这时候宕机。
2\. 重启之后redo log 完整,所以恢复数据 value = 10
3\. bin log日志中没有记录,如果需要恢复数据的时候 value = 9
1\. binlog 写入完成,redo log 未完成
2\. 重启之后没有redo log ,所以value 还是9
3\. 当需要恢复数据的时候binlog 日志完整,value 更新成10
- undo log
- 在更新写入buffer pool之前记录
- 如果更新过程中出错,直接回滚到undo log 的状态
十八、binlog 的作用和三种格式
作用:
1\. 数据恢复
2\. 主从复制
格式(二进制文件):
1)statement
- 1\. 记录每次sql语句的原文
- 2\. 删除一个表只需要记录一条sql语句,不需要记录每一行的变化,节约IO,提高性能,减少日志量
- 3\. 可能出现主从不一致(存储过程、函数等)
- 4\. RC隔离级别(读提交),因为binlog 记录顺序是按照事务commit 顺序记录的,所以可能导致主从复制不一致。通过可重复读级别的间隙锁的引入,可以解决。
2)row
- 1\. 记录每条记录的修改情况,不需要记录sql语句的上下文记录
- 2\. 导致binlog日志量很大
- 3\. 删除一个表:记录每条记录都被删除的状况
3)mixed
- 1\. 前两个格式的混合版
- 2\. 根据语句自动选择使用哪一种:
- 一般的sql语句修改使用statement
- 修改表结构、函数、存储过程等操作选择row
- update 和delete 还是会记录全部记录的变化
十九、主从同步(主从复制)的原理和问题及读写分离
1、解决的问题
- 数据分布
- 负载均衡
- 数据备份,高可用,避免单点失败
- 实现读写分离,缓解数据库压力
- 升级测试(使用高版本mysql当从库)
2、支持的复制类型(binlog 的三种格式)
- 基于sql语句的复制
- 基于行的复制
- 混合型复制
3、原理
1)基础概念
- 从库生成两个线程
- I/O线程
- SQL线程
- 主库生成线程
- log dumo 线程
2)流程(主节点必须开启bin log功能,)
- 1\. 从节点开启start slave 命令之后,创建一个IO进程连接到主节点
- 2\. 连接成功之后,主节点创建一个 log dump线程(主节点会为每一个从节点创一个log dump线程)
- 3\. 当binlog发生变化时,主节点的dump log线程会读取bin-log内容并发送给从节点
- 4\. 主节点dump log 线程读取bin-log 的内容时会对主节点的bin-log加锁,读取完成在发送给从节点之前释放锁
- 5\. 从节点的IO线程接收主节点发送的binlog内容,并将其写入本地relay log 文件中
- 6\. 主从节点通过binlog文件+position偏移量定位主从同步的位置,从节点会保存接收到的position偏移量,如果从节点发生宕机重启,自动从postion位置发起同步
- 7\. 从节点的SQL线程复制读取本地relay log的内容,解析成具体的操作并执行,保证主从数据一致性
4、主从复制的模式
1)异步模式(默认方式)
- 1\. 可能导致主从不一致(主从延时)
- 2\. 主节点接收到客户端提交的事务之后直接提交事务并返回给客户端
- 3\. 如果主节点事务提交之后,log dump还没来得及写入就宕机就会导致主从数据不一致
- 4\. 不用关心主从的同步操作,性能最好
2)全同步模式
- 1\. 可靠更高,但是会影响主库相应时间
- 2\. 主节点接收到客户端提交的事务之后,必须等待binlog 发送给从库,并且所有从库全部执行完事务之后才返回给客户端
3)半同步模式
- 1\. 增加一部分可靠性,增加主库一部分相应时间
- 2\. 主节点接收到客户端提交的事务之后,等待binlog发送给至少一个从库并且成功保存到本地relay log中,此时主库提交事务并返回给客户端
4)server-id的配置和server-uuid
- 1\. server-id用于标识数据库实例,防止在链式主从、多主多从拓扑中导致SQL语句的无限循环
- 2\. server-id默认值为0,对于主机来说依然会记录二进制日志,但是会拒绝所有的从机连接。
- 2\. server-id = 0 对于从机来说会拒绝连接其他实例
- 3\. server-id是一个全局变量,修改之hi偶必须重启服务
- 4\. 主库和从库的server-id重复时
- 默认replicate-same-server-id = 0,从库会跳过所有主从同步的数据,导致主从数据不一致
- replicate-same-server-id = 1,可能导致无线循环执行sql
- 两个从库(B、C)server-id重复会导致主从连接异常,时断时连
- 主库(A)发现相同的server-id会断开之前的连接,重新注册新的连接
- B、C从库的连接会周而复始的重连
- MySQL服务会自动创建并生成server-uuid配置
- 当主从同步时如果主从实例的server-uuid相同会报错退出,不过我们可以通过设置replicate-same-server-id=1来避免报错(不推荐)
5、读写分离
1)基于代码实现,减少硬件开支
2)基于中间代理实现
3)主从延时
- 从库性能比主库差
- 大量查询导致从库压力大,消耗大量CPU资源,影响同步速度:一主多从
- 大事务执行:事务执行完之后才会写入binlog,从库读取延时
- 主库ddl(alter、drop、create)
二十、死锁
1、产生的四个必要条件
- 1\. 互斥条件
- 2\. 请求与保持条件:一次性分配全部资源,否则一个都不分配
- 3\. 非剥夺条件:当进程获得一部分资源等待其他资源的时候释放占有的资源
- 4\. 循环等待条件:
理解:一个资源只能被一个进程占用,进程获取资源资源还能申请新的资源,并且已经获得的资源不能被剥夺,同时多个进程相互等待其他进程被占用的资源
2、解除死锁
- 1\. 终止进程(全部干掉)
- 2\. 逐个种植(杀一个看一下有没有解除)
二十一、Mysql 优化大分页查询 limit 100000 (offset),10 (page\_sie)
1、原因
mysql查询分页数据时不是直接跳过offset(100000),而是取offset + page\_size = 100000 + 10 = 100010条数据,然后放弃其掉前面的100000条数据,所以效率地下
2、优化方案
- 延时关联:使用覆盖索引
- 主键阈值法:主键是自增的情况下,通过条件推算出符合条件的主键最大值&最小值(使用覆盖索引)
- 记录上一页的结果位置,避免使用 OFFSET
二十二、redis 缓存和 mysql 数据一致性
方式:
1、先更新redis 再更新数据库
场景:update set value = 10 where value = 9
1) redis更新成功:redis value = 10
2)数据库更新失败:mysql value = 9
3)数据不一致
2、先更新数据库,再更新redis
场景: A进程update set value = 10 where value = 9 ;B进程 update set value = 11 where value = 9;
1)A 进程先更新数据库,还未写入缓存:mysql value = 10 ;redis value = 9
2)B 进程更新数据库并且提交事务,写入缓存:mysql value = 11;redis value = 11;
3)A 进程处理完请求提交事务,写入缓存:redis value = 10;
4)最终 mysql value = 11; redis value = 10
3、先删除缓存再更新数据库
场景:A进程update set value = 10 where value = 9 ;B进程查询value;
1)A 进程先删除缓存 还没来得及修改数据或者事务未提交
2)B 进程开始查询,没有命中缓存,所以查库并写入缓存 redis value = 9
3)A 进程更新数据库完成 mysql value = 10
4)最终 mysql value = 10;redis value = 9
解决方案:
1、延时双删除
场景:A进程update set value = 10 where value = 9 ;B进程查询value;
1)A 进程先删除缓存 还没来得及修改数据或者事务未提交
2)B 进程开始查询,没有命中缓存,所以查库并写入缓存 redis value = 9
3)A 进程更新数据库完成 mysql value = 10
4)A 进程估算延时时间,sleep之后再次删除缓存
5)最终mysql value = 10;redis value 为空(下次查询直接查库)
6)延时的原因时防止B进程在A进程更新完之后B进程还没来得及写入缓存
2、请求串行化
1)创建两个队列 :更新队列和查询队列
2)当缓存不存在需要查库的时候将key存入更新队列
3)如果查询未完成之前有新的请求进来,并且发现更新队列中还存在key则将key放入查询队列,则等待;不存在则重复第二步
4)如果查询的数据发现查询队列已经存在则不需要再次写入队列
5)数据更新完成之后rpop更新队列,同时rpop查询队列,释放查询请求
6)查询请求可以使用while + sleep 查询缓存并且设置最大延迟时间,还没有完成则返回空
二十三、redis 中的 connect 和 pconnect
1、connect :脚本结束之后释放连接
1\. close :释放连接
2、pconnect(长连接) :脚本结束连接不释放,连接保持在php-fpm进程中,生命周期随着php-fpm进程的生命周期
- 1\. close不释放连接
- 只是当前php-cgi进程中不能再次请求redis
- 当前php-cgi中的后续连接仍然可以复用,直到php-fpm结束生命周期
- 2\. 减少建立redis连接的消耗
- 3\. 减少一个php-fpm多次建立连接
- 4\. 消耗更多的内存,并且连接数持续增加
- 5\. 同一个php-fpm的woker子进程(php-cgi)的上一个请求可能会影响到下一个请求
3、pconnect 的连接复用问题
- 变量A select db 1 ;变量B select db 2;会影响到变量A的db
- 解决:每一个db创建一个连接实例
二十四、redis zset 有序集合使用 skiplist 的原理
1、基本概念
1\. skiplist是一个随机的数据,以有序的方式在层次化的链表中保存元素(只能用于元素有序的情况)
2\. skiplist实在有序链表和多层链表的基础上演变的
3\. 允许重复值,所以对比检查除了要对比key 还要对比value
4\. 每个节点都带有一个高度为1的后退指针,用于表头方向到表尾方向的迭代
5\. 时间复杂度O(logn)、空间复杂度O(n)
2、跳跃表和平衡树的对比
1)范围查询效率
- 跳跃表范围查询效率更高,因为找到最小值之后只需要对第一层的链表进行遍历直到小于最大值即可
- 平衡树范围查询找到最小值之后还要进行中序遍历找到其他不超过最大值的节点
2)内存占用
- skiplist 每个节点的指针数量为1/(1-p)
- 平衡树的每个节点指针数都为2
3)插入和删除操作
- skiplist只需要修改相邻节点的指针
- 平衡树变更会引起子树的调整
二十五、redis 的过期删除和淘汰机制
1、常规过期删除策略
1)定时删除
- 通过定时器在过期的时候立即删除
- 内存释放及时但是消耗更多的CPU,大并发的时候需要消耗CPU资源影响处理请求的速度
- 内存友好,CPU不友好
2)惰性删除
- 放任键过期不管,到下次需要去取出的时候检查是否过期并删除
- 可能存在大量过期键,且不会使用,导致内存溢出
- 内存不友好,CPU友好
3)定期删除
- 每隔一段时间检查,删除过期的键
- 删除多少和检查多少有算法决定
2、redis采用的 惰性删除 + 定期删除
- 周期性随机测试一些设置了过期时间的键进行检查,到期则删除
- 每次清理的时间不超过CPU的25%,达到时间则退出检查
- 定期没有删除到的键,且以后不会使用的键还是会存在内存中,所以需要配合淘汰策略
3、淘汰策略(内存不足以写入新数据的时候执行)
- volatile-lru :设置了过期时间且最近使用越少越优先淘汰
- volatile-ttl :设置了过期时间且过期时间越早越优先淘汰
- volatile-random :设置了过期时间中随机删除
- allkeys-lru :所有键中过期时间越早越优先淘汰
- allkeys-random :所有键中过期随机淘汰
- no-enviction :不允许淘汰,内存不足报错
二十六、redis 常见问题及解决方案
1、缓存雪崩:同一时间大量缓存失效,导致请求直接查询数据库,数据库内存和CPU压力增加甚至宕机
解决:
- 热点数据永不过期或者分布到不同实例,降低单机故障问题
- 缓存时间添加随机数,防止大量缓存同时失效
- 做二级缓存或者双缓存,A为原始缓存 短时效,B为备份缓存 ,长期有效。更新时候双写缓存
2、缓存穿透:缓存和数据库都没有数据,大量请求下,所有请求直接击穿到数据库,导致宕机。
解决:
- 布隆过滤器:长度为m的位向量或者位列表组成(仅包含0或1位值的列表)
- 使用多个不用的hash函数,产生多个索引值,填充对应多个位置的值为1
- 布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”
- 可能误判但是基础过滤效率高
- 极端情况,当布隆过滤器没有空闲位置的时候每次查询返回true
- 空缓存(短时效)
- 业务层参数过滤
3、缓存击穿:数据库中有数据,但是缓存突然失效之后发生大量请求导致数据库压力增加甚至打垮宕机
解决:
- 热点数据永不过期
- 互斥锁:获取锁之后不管成功还是失败都要释放锁
二十七、php-fpm 详解及生命周期
1、基础知识
1)CGI协议
- 动态语言的代码文件需要通过对应的解析器才能被服务器识别
- CGI协议就是用来使服务器和解释器相互通信的
- 服务器解析PHP文件需要PHP解释器加上对应的CGI协议
2)CGI程序 = php-cgi
- php-cgi就是一个遵守CGI协议的CGI程序
- 同时也就是PHP解释器
- 标准的CGI每个请求都会解析php.ini,初始化执行环境等,降低性能
- 每次修改配置之后需要重新php-cgi才能让php.ini生效
- 不能动态worker调度,只能一开始指定数量的worker
3)FastCGI协议
- 和CGI一样也是一个协议/规范,不过是再CGI的基础上优化,效率更高
- 用来提高CGI程序性能的
- 实现了CGI进程的管理
4)FastCGI程序 = php-fpm
- php-fpm就是一个遵守FastCGI协议的FastCGI程序
- FastCGI程序对CGI程序的管理模式
- 启动一个master进程,解析配置文件,初始化环境
- 启动多个worker子进程
- 接受到请求之后,传递给woker进程去执行
- 解决修改php.ini之后平滑重启问题
- process\_control\_timeout:子进程接受主进程复用信号的超时时间(在规定时间内处理完请求,完成不了就不管了)
- 设定php-fpm留给fastcgi进程响应重启信号的时间
- process\_control\_timeout = 0,也就是不生效,无法保证平滑重启
- process\_control\_timeout设置过大可能导致系统请求堵塞
- process\_control\_timeout =10的情况下,如果代码逻辑需要11s,重启旧可能导致代码执行部分退出
- 建议值:request\_terminate\_timeout
- 重启类型
- 优雅重启
- 强制重启
2、php-fpm生命周期:待更新
PHP-FPM 生命周期:https://www.abelzhou.com/php/php-fpm-lifespan/#
参考:
二十八、Nginx 和 php 之间的通信
1、通信方式:fastcgi\_pass
1)tcp socket
- 跨服务器,nginx和php不在一个机器时,只能用这个方式
- 面向连接的协议,更好的保证通信的正确性和完整性
2)unix socket
- 不需要网络协议栈、打包拆包等
- 减少tcp 开销,效率比tcp socket 更高
- 高并发时候不稳定,连接数暴增产生大量的长时缓存,大数据包可能直接返回异常
参考:
二十九、web 漏洞及问题
1、sql注入
2、XSS攻击
推荐阅读(很详细的案例来分析XSS攻击的不同类型及解决方案):\前端安全系列(一):如何防止XSS攻击?\
3、CSRF攻击:
推荐阅读 :\前端安全系列(二):如何防止CSRF攻击?\
4、文件上传漏洞
推荐阅读 :\浅析文件上传漏洞\
5、跨域问题:
1)jsonp
2)cors
3)nginx代理
推荐学习:《PHP视频教程》