PHP
PHP的运行原理?有几层?
从图上可以看出,PHP从上到下是一个4层体系:
-
Application:就是我们平时编写的 PHP 程序,通过不同的 SAPI 方式得到各种各样的应用模式,如通过 Web Server 实现 WEB 应用、在命令行下以脚本方式运行等等。
-
SAPI:SAPI 全称是 Server Application Programming Interface,也就是服务端应用编程接口,SAPI 通过一系列钩子函数,使得 PHP 可以和外围交互数据,这是 PHP 非常优雅和成功的一个设计,通过 SAPI 成功的将 PHP 本身和上层应用解耦隔离,PHP 可以不再考虑如何针对不同应用进行兼容,而应用本身也可以针对自己的特点实现不同的处理方式。
-
Extensions:围绕着 Zend 引擎,Extensions 通过组件式的方式提供各种基础服务,我们常见的各种内置函数(如array系列)、标准库等都是通过 Extensions 来实现,用户也可以根据需要实现自己的 Extension 以达到功能扩展、性能优化等目的(如贴吧正在使用的PHP中间层、富文本解析就是 Extensions 的典型应用)。
-
Zend:Zend 引擎整体用纯 C 实现,是 PHP 的内核部分,它将 PHP 代码翻译(词法、语法解析等一系列编译过程)为可执行 opcode 处理,并实现相应的处理方法,实现了基本的数据结构(如 HashTable、OO)、内存分配及管理、提供了相应的 API 方法供外部调用,是一切的核心,所有的外围功能均围绕 Zend 实现。
-
PHP 代码的执行流程
参考文档:https://www.awaimai.com/509.html
PHP垃圾回收机制(GC)?
通过引用计数实现,原理如下:
因为 PHP 是由C语言实现的,所以底层会用如下结构来表示一个变量容器(即 变量值,来源于https://www.php.net/manual/zh/features.gc.refcounting-basics.php):
struct _zval_struct {
union _zvalue_value {
long lval;
double dval;
struct {
char *val;
int len;
} str;
...
} value;
zend_uchar is_ref;
zend_ushort refcount; // 引用计数器
...
};
每次变量容器被使用,refcount 就会+1,每当关联到这个变量容器的变量退出作用域(如:函数执行结束)或变量被 unset 时,refcount 就会-1。当 refcount=0 时,变量容器将被销毁
可以从下面例子中理解引用计数的原理:
$a = 'a' . rand(0, 9);
echo xdebug_debug_zval('a');
$b = $a;
echo xdebug_debug_zval('a');
echo xdebug_debug_zval('b');
$a = 'aa';
echo xdebug_debug_zval('a');
echo xdebug_debug_zval('b');
运行结果:
a: (refcount=1, is_ref=0)='a7' // 这里的“a7”即代表一个“变量容器”,这里是初次赋值,引用计数为1
a: (refcount=2, is_ref=0)='a7' // 被使用,引用计数+1
b: (refcount=2, is_ref=0)='a7' // 变量$a和$b指向同一个变量容器
a: (refcount=1, is_ref=0)='aa' // 变量$a指向了一个新的变量容器
b: (refcount=1, is_ref=0)='a7' // 变量$b仍然指向了原来的变量容器
PHP7有哪些新特性?
- NULL 合并运算符($_GET[‘a’] ?? ‘aa’; 等价于 isset($_GET[‘a’]) ? $_GET[‘a’] : ‘aa’;)
- 返回值类型声明(function arraysSum(array …$arrays): array)
- 匿名类
- 参见:https://www.php.net/manual/zh/migration70.new-features.php
PSR
- PSR 是 PHP Standard Recommendations (PHP 推荐标准)的简写,由 PHP FIG 组织制定的 PHP规范,是PHP开发的实践标准。其中 PSR-1、PSR-2和PSR-12 规定了编码规范。
- 中文文档:https://learnku.com/docs/psr/about-psr/1613
如何理解Laravel中服务容器和服务提供者?
PHP-FPM
CGI、FastCGI、PHP-CGI、PHP-FPM的区别?
-
CGI历史:早期 Web Server(Nginx)只能处理 html 等静态文件,为了支持 Web Server 处理动态内容,便出现了 CGI协议,PHP-CGI 就是 CGI协议的 PHP 实现,通过 PHP-CGI 就能让 Web Server 和 PHP 进行通信
-
CGI:Web Server 与 Web Application 之间数据交换的一种协议
-
FastCGI:同CGI,是一种通信协议,但比 CGI 性能上做了一些优化( CGI 规定每个请求创建一个 CGI 进程来处理。而FastCGI会创建进程池来处理请求,而且进程池中的进程数会根据请求数量自动扩缩容,所以性能比 CGI 好很多)
-
PHP-CGI:PHP(Web Application)与 Web Server 通信的CGI实现(Web Server 与 PHP-CGI 通过标准输入和标准输出通信,Web Server 收到请求后,会将请求体通过标准输入传递给 PHP-CGI,请求参数(如:REMOTE_HOST、HTTP_USER_AGENT等) 则通过设置环境变量的方式传递,然后 PHP-CGI 处理后通过标准输出传回给 Web Server)
-
PHP-FPM:PHP 与 Web Server 通信的 FastCGI 实现,FPM 是 FastCGI Process Manage,即 FastCGI进程管理器
PHP-FPM运行原理?
FPM 是 FastCGI Process Manage,即 FastCGI进程管理器,FastCGI协议的PHP实现。包含一个 master进程和多个 worker进程。
- master进程
负责监听端口,接收来自 Web Server 的请求。 - worker进程
一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方。
MySQL
MySQL有哪些存储引擎,有什么区别?
区别 | InnoDB | MyISAM |
---|---|---|
事务 | 是 | 否 |
外键 | 是 | 否 |
锁粒度 | 行锁 | 表锁 |
数据存放 | 索引文件叶子节点 | 单独的数据文件 |
MySQL主从复制原理?
- 开启三个并行工作的线程,一个在主服务器,剩下的两个(IO线程和SQL线程)在从服务器。
- 主服务器上的任何修改都会通过自己的IO线程写入到二进制日志(Binary log)。
- 从服务器通过自己的 IO线程以及配置好的用户名密码连接到主服务器上,然后读取二进制日志并把读取到的差异数据写到本地的中继日志(Relay log)。
- 从服务器通过自己的SQL线程定时检查中继日志,并把差异数据写入到本地的数据库中。
MySQL锁机制?
- InnoDB 支持行锁+表锁,MyISAM 只支持表锁
- 共享锁和排它锁是互斥的,所以一旦上了共享锁就不能上排它锁,一旦上了排它锁就不能上共享锁。
- 共享锁(读锁/S锁):
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
通过上面语句,MySQL 会为查询结果集中的每行都添加共享锁,但如果结果集中有一行已经添加了排它锁,那本次查询操作将被阻塞,直到排它锁被释放。
同一行记录可被多个事务添加多个共享锁,只有当所有共享锁都被释放,才能修改数据。 - 排他锁(写锁/X锁):
SELECT * FROM table_name WHERE ... FOR UPDATE
通过上面语句,MySQL 会为查询结果集中的每行都添加排他锁,但如果结果集中有一行已经添加了共享锁或排它锁,那本次查询操作将被阻塞,直到相应锁被全部释放。 - 死锁:
1.开启事务A,得到id为1和2的S锁
BEGIN; SELECT * FROM table_name WHERE id IN (1,2) LOCK IN SHARE MODE;
2.开启事务B,得到id为1和3的S锁
BEGIN; SELECT * FROM table_name WHERE id IN (1,3) LOCK IN SHARE MODE;
3.因为id=2的记录已经被事务A加了S锁,所以此处会阻塞
UPDATE table_name SET name='xx' WHERE id=2
4.此时在事务A中执行如下语句就会发生死锁,因为id=3的记录已经被事务B加了S锁
UPDATE table_name SET name='yy' WHERE id=3
MySQL事务的隔离级别有哪些?
- 读未提交(Read Uncommitted):一个事务还没提交,其修改的内容就可以被其他事务看到
- 读提交(Read Committed):一个事务提交后,其修改的内容才可以被其他事务看到
- 可重复读(Repeatable Read):一个事务启动后,总是无法看到其他事务修改的内容
- 串行化 (Serializable):对于同一行记录,写会加写锁,读会加读锁,当事务之间读写冲突时,必须等待上一个事务执行完后才能继续执行,使得事务串形化执行
- 查询当前隔离级别:
SELECT @@transaction_isolation
- 修改当前隔离级别:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL READ COMMITTED
MySQL如何分库分表?
- 垂直分库:
按功能将表划分进不同的数据库中,如用户库、订单库等,拆分后一般分散到不同的数据库服务器中。优点:缓解单库带来的并发压力,缺点:不能 JOIN 和分布式事务会带来更多的复杂性。 - 垂直分表:
按表的字段来拆分表,如用户表+用户附加表,拆分后的表仍在一个库中。优点:缓解单表过大的问题。 - 水平分表:
按照某种规则(RANGE / HASH取模等)将一个表拆分成多个表,此时每个表的结构完全一致,拆分后的表仍在一个库中。优点:缓解单表记录数过多的问题(MySQL 单表记录数如果超过2000w,即使有索引也会很慢)。 - 分表示例:
MySQL是否支持中文全文检索?
从MySQL 5.7.6开始,MySQL 内置了 ngram 全文解析器,用来支持中文、日文、韩文分词
MySQL如何实现无限极分类?
[id, type, parent_id],用 parent_id 可以指向父级,实现无限极分类
Redis
Redis有哪些数据类型?
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
- string(字符串): Redis 最基本的数据类型,string 类型的值最大能存储 512MB
redis 127.0.0.1:6379> SET c1 "Hello" OK redis 127.0.0.1:6379> GET c1 "Hello"
- hash(哈希):string 类型的键值对映射表,特别适合用于存储对象
# HMSET = Multiply HSET (v4.0.0后HMSET被弃用了,可直接用HSET) redis 127.0.0.1:6379> HMSET c2 k1 "Hello" k2 "World" OK redis 127.0.0.1:6379> HGET c2 k1 "Hello" redis 127.0.0.1:6379> HGET c2 k2 "World"
- list(列表):string 类型的列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)
redis 127.0.0.1:6379> LPUSH c3 "Hello" (integer) 1 redis 127.0.0.1:6379> LPUSH c3 "World" (integer) 2 redis 127.0.0.1:6379> LRANGE c3 0 10 1) "Hello" 2) "World"
- set(集合):string 类型的无序集合。set 是通过哈希表实现的,所以添加、删除和查找的复杂度都是 O(1)。
redis 127.0.0.1:6379> SADD c4 "Hello" (integer) 1 redis 127.0.0.1:6379> SADD c4 "World" (integer) 1 redis 127.0.0.1:6379> SADD c4 "World" (integer) 0 redis 127.0.0.1:6379> SMEMBERS c4 1) "World" 2) "Hello"
- zset(sorted set:有序集合):将 set 中的元素增加一个权重参数score,元素按score有序排列。
redis 127.0.0.1:6379> ZADD c5 2 "two" (integer) 1 redis 127.0.0.1:6379> ZADD c5 1 "one" (integer) 1 redis 127.0.0.1:6379> ZADD c5 3 "three" (integer) 0 redis 127.0.0.1:6379> > ZRANGEBYSCORE c5 0 5 1) "one" 2) "two" 3) "three"
Redis持久化机制?
-
RDB 是 Redis 默认的持久化方案。AOF 在 Redis 中默认不开启,它的出现是为了弥补 RDB 的不足(数据的不一致性)。
-
RDB(Redis Database File):可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。
-
优点:
- RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑。
- RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
-
缺点:
- 容易丢失数据。因为 RDB 文件需要保存整个数据集的状态,所以它并不是一个轻松的操作。 因此可能会至少 5 分钟才保存一次 RDB 文件。一旦发生故障停机,就可能会丢失好几分钟的数据。
-
原理图:
-
AOF(Append Only File): 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集,默认处于关闭状态。
-
如何开启 AOF:
打开 redis.conf 文件,找到 APPEND ONLY MODE 对应内容,开启需要手动把 no 改为 yes
appendonly yes
-
优点:
- AOF 文件是一个只进行追加操作的日志文件(append only log),因此对 AOF 文件的写入不需要进行 seek ,即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等),redis-check-aof 工具也可以轻易地修复这种问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。
- AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存,因此 AOF 文件的内容非常容易被人读懂,对文件进行分析(parse)也很轻松。
-
缺点:
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
-
原理图:
Redis过期策略?
-
Redis采用的是 惰性删除+定期删除 策略
-
定时删除:在设置 key 的过期时间的同时,为该 key 创建1个定时器,让定时器在 key 的过期时主动删除。
- 优缺点:时间换空间做法,虽然保证了内存能被尽快释放,但是浪费掉了很多CPU时间,如果定时器过多,将会严重影响写入性能。
-
惰性删除:key 过期的时候不删除,每次从数据库获取 key 的时候去检查是否过期,若过期,则删除并返回 null。
- 优缺点:空间换时间做法,虽然不会浪费CPU时间,但是已过期的内存不能被及时释放,如果 key 长期不被使用,将会造成严重的内存浪费,甚至内存泄露。
-
定期删除:只创建1个定时器,每隔一段时间清理过期的 key
- 优缺点:折中方案,平衡了内存浪费和CPU浪费。
Redis事务原理?
- Redis 事务的本质是一组命令的集合。在事务执行过程中,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到当前事务中。
- Redis 事务不保证原子性,如果事务执行失败,不会回滚,会造成部分命令执行成功而部分失败的情况。
- 一个事务从开始到执行会经历以下三个阶段:
- 开始事务(MULTI)
- 命令入队
- 执行事务(EXEC)
- 示例:
redis 127.0.0.1:6379> MULTI OK redis 127.0.0.1:6379> SET a "Hello" QUEUED redis 127.0.0.1:6379> GET a QUEUED redis 127.0.0.1:6379> SET b "World" QUEUED redis 127.0.0.1:6379> EXEC 1) OK 2) "Hello" 3) OK
如何解决缓存穿透、缓存击穿、缓存雪崩问题?
-
缓存穿透
- 原因:
如果某个数据在缓存中不存在,查询 DB 后也不存在,这个数据就穿透缓存了,此时缓存已经失效。如果这个数据被频繁访问,会对 DB 造成极大压力。 - 理解:
缓存和 DB 均没有数据+大量请求 - 解决方案:
可以把查询 DB 后的空结果也放入缓存,但是设置很短的过期时间,一般不超过5分钟。
- 原因:
-
缓存击穿
- 原因
一般指访问热点数据的时候,可能会有大量的并发请求同时到达,此时如果缓存中没有数据(一般是数据刚好过期),那么大量的请求将会同时访问 DB,给 DB 带来瞬时的巨大压力。
理解:
缓存没数据+超大量并发请求 - 解决方案:
- 设置热点数据永远不过期。
- 加互斥锁,防止请求并发访问。
- 原因
-
缓存雪崩
- 原因:
由于设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重,导致雪崩。 - 理解:
大量缓存同时过期+大量请求 - 解决方案:
可以在设置缓存过期时间的时候加一个随机值(例如:1-5分钟随机),这样缓存过期时间重复率就会大大降低。
- 原因:
高并发
如何解决高并发问题?
服务器层面
- 做负载均衡:分硬件负载均衡和软件负载均衡,硬件负载设备如F5,可支持百万级别的 QPS。软件负载可用 Nginx,可支持5万的 QPS。然后用 ip_hash 做为负载均衡算法,这样服务器就能水平扩容了,可解决服务器层面的并发问题。
数据库层面
- 分库分表+读写分离:把原本的一个库分成多个库部署在不同的数据库服务器上,然后数据库做读写分离,如果数据库写需求多,可以继续分库或做多主库。如果读需求多,可以水平扩展从库。这样就能基本解决数据库的并发问题。
- 缓存:应对数据库读取高并发情况,将高频数据写入缓存,这样能极大缓解数据库的读压力。
- 消息中间件:应对数据库写入高并发情况,将请求先写入消息中间件,通过消息中间件削峰填谷(假设每秒有1w请求,其中5k请求必须立刻写入数据库,那就立刻写,还有5k并不着急,就可以通过消息中间件在几分钟后过了高峰期再写入数据库),从而缓解数据库高峰时期的写压力。
流控(流量控制)
- 可用Redis来做流控,来应对短时间内流量爆发。
QPS
Laravel | 原生PHP | Golang | 数据库 | redis |
---|---|---|---|---|
几百 | 几千 | 几万 | 几千 | 10w |
消息队列
Elasticsearch
基本概念
- 全文搜索(Full-text Search)
- 全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。
- 在全文搜索的世界中,存在着几个庞大的帝国,也就是主流工具,主要有:
Apache Lucene
Elasticsearch
Solr
Ferret
- 倒排索引(Inverted Index)
- 该索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。Elasticsearch 能够实现快速、高效的搜索功能,正是基于倒排索引原理。
- 节点 & 集群(Node & Cluster)
- Elasticsearch 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elasticsearch 实例。单个Elasticsearch实例称为一个节点(Node),一组节点构成一个集群(Cluster)。
- 索引(Index)
- Elasticsearch 数据管理的顶层单位就叫做 Index(索引),相当于关系型数据库里的数据库的概念。另外,每个 Index 的名字必须是小写。
- 文档(Document)
- Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。Document 使用 JSON 格式表示。同一个 Index 里面的 Document,不要求有相同的结构(Schema),但是最好保持相同,这样有利于提高搜索效率。
- 类型(Type)
- Document 可以分组,比如 employee 这个 Index 里面,可以按部门分组,也可以按职级分组。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document,类似关系型数据库中的数据表。
- 不同的 Type 应该有相似的结构(Schema),性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。
- 文档元数据(Document metadata)
- 文档元数据为 _index, _type, _id,这三者可以唯一表示一个文档,_index 表示文档在哪存放,_type 表示文档的对象类别,_id 为文档的唯一标识。
- 字段(Fields)
- 每个 Document 都类似一个 JSON 结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,可以类比关系型数据库数据表中的字段。
- 在 Elasticsearch 中,文档(Document)归属于一种类型(Type),而这些类型存在于索引(Index)中,下图展示了Elasticsearch与传统关系型数据库的类比:
- 参考文档:https://www.jianshu.com/p/d48c32423789
实践指引
微服务
对微服务架构是否有了解?
Hyperf,PHP的微服务框架
操作系统
网络安全
参考博客:https://medium.com/iocscan
SQL注入
XSS攻击原理,如何防范?
- XSS:Cross-Site Scripting(跨站脚本攻击),是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
- 防范:对用户的输入过滤。
CSRF攻击原理,如何防范?
算法与数据结构
10亿个数字中找出最大的10000个数?
分治+hash表