今年遇到的PHP面试题汇总

1,333 阅读45分钟

前言

最近一直在面试,大概面了十来家公司,有大公司也有小公司,用这篇文章记录一下遇到过的面试题,顺便做一下自己的知识梳理。

面试面多了有时会产生怀疑人生的感觉,面试官里有觉得我还不错的,也有觉得我基础不扎实的,反正我觉得自己是挺帅的,帅就完事了。

毕竟每个面试官的知识面都不一样,判断标准也不一样,最重要的还是摆平心态,就当做平时和别人做技术交流。

个人觉得面试除了找工作以外,也是检验自己能力的一个途径,至少我自己在这段时间的面试中的确学到了不少新东西。

面试时我还会感受一下面试官的性格,毕竟工作最后还是得和人相处,找到一个和自己合得来的Leader还是挺重要的。

PHP

PHP7和PHP5有什么区别

  • 性能提升:PHP7比PHP5.0性能提升了两倍。
  • 以前的许多致命错误,现在改成抛出异常。
  • 移除了一些老的不在支持的SAPI(服务器端应用编程端口)和扩展。
  • 新增加了空合并操作符。
  • 新增加了结合比较运算符。
  • 新增加了函数的返回类型声明。
  • 新增加了标量类型声明。
  • 新增加匿名类。

trait和interface的区别

trait

是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。

interface

可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。接口是通过interface关键字来定义的,就像定义一个标准的类一样,但其中定义所有的方法都是空的。

相关链接:Traitinterface

PHP有哪些魔术方法

__construct()__destruct()__call()__callStatic()__get()__set()__isset()__unset()__sleep()__wakeup()__serialize(), __unserialize(), __toString()__invoke()__set_state()__clone()__debugInfo()等方法在 PHP 中被称为魔术方法(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。

PHP的数组有哪些常用函数

array(),array_keys(),in_array(),array_column(),array_pop(),array_first(),array_flip(),array_walk(),array_slice(),array_sum()

相关链接:PHP数组

__autoload和spl_autoload_register有什么关系

当我们使用PHP新建一个类对象时,需要引入这个类的代码文件。如果需要新建100个类,则需要引入100个类文件,那么业务代码就要有100条include语句,这样就很痛苦了,所以需要使用PHP的自动加载机制。

但是PHP怎么知道类文件的路径呢?

这就需要我们自己实现一个加载类文件的方法,可以通过实现__autoload方法,或者通过spl_autoload_register注册自动加载方法。

__autoload —— 尝试加载未定义的类(警告:本函数已自 PHP 7.2.0 起被废弃,并自 PHP 8.0.0 起被移除。 强烈建议不要依赖本函数。)

spl_autoload_register —— 将函数注册到SPL __autoload函数队列中。

那么为什么__autoload不推荐使用呢?

他有一个很大的缺陷:只能定义一次。当我们项目中有多种不同的目录结构时,就需要在__autoload中用各种判断来加载不同目录的类文件。

而spl_autoload_register可以注册多个加载方法到队列中,比__autoload更加灵活。

相关链接:spl_autoload_register

CGI和FastCGI有什么区别

CGI(Common Gateway Interface)

全称是“通用网关接口”,是一种让客户端(web浏览器)与Web服务器(nginx等)程序进行通信(数据传输)的协议。

FastCGI(Fast Common Gateway Interface)

全称是“快速通用网关接口”。是通用网关接口(CGI)的增强版本,由CGI发展改进而来,主要用来提高CGI程序性能,类似于CGI,FastCGI也是一种让交互程序与Web服务器通信的协议。

两者区别

CGI程序运行在独立的进程中,并对每个Web请求建立一个进程,这种方法非常容易实现,但效率很差,难以扩展。面对大量请求,进程的大量建立和消亡使操作系统性能大大下降。此外,由于地址空间无法共享,也限制了资源重用。

与CGI程序为每个请求创建一个新的进程不同,FastCGI使用持续的进程(master)来处理一连串的请求。这些进程由FastCGI服务器管理,而不是web服务器。 当进来一个请求时,web服务器把环境变量和这个页面请求通过一个socket或者一个TCP connection传递给FastCGI进程。

相关链接:关于CGI和FastCGI的理解

什么是PHP-FPM

PHP-FPM(PHP FastCGI Process Manager)意:PHP FastCGI 进程管理器,它是 FastCGI 的实现,用于管理PHP进程池的软件,用于接受web服务器的请求。

相关链接:深入了解下PHP-FPM

PHP脚本的执行流程

首先,Zend Engine(ZE),调用词法分析器(Lex生成的,源文件在 Zend/zend_language_sanner.l), 将我们要执行的PHP源文件,去掉空格 ,注释,分割成一个一个的token。

然后,ZE会将得到的token forward给语法分析器(yacc生成, 源文件在 Zend/zend_language_parser.y),生成一个一个的opcode,opcode一般会以op array的形式存在,它是PHP执行的中间语言。

最后,ZE调用zend_executor来执行op array,输出结果。

相关链接:深入浅出PHP(Exploring PHP)

用PHP实现单例模式

<?php
class Singleton {
    private static $instance = null;

    public static function getInstance()
    {
        if (!isset(self::$instance)) {
            self::$instance = new self;
        }
        return self::$instance;
    }
    private function __construct(){}
    private function __clone(){}
}

面试时我写的也是这个版本的单例模式代码,但是面试官和我说会有线程安全问题?查了很久都没找到PHP实现的线程安全的单例模式,欢迎小伙伴们给我提供一下。

PHP框架

使用过哪些框架

ThinkPHP、Laravel、Lumen、Swoole

Laravel框架里有哪些设计模式

SoC 关注点分离Facade 外观模式Container 容器Contract 契约模式,Pipeline 管道模式,Observer 观察者模式,Singleton 单例模式。

以上几个链接都是以前自己写的,关于Laravel中用到了哪些设计模式查了一圈也没找到说得特别详细的文章,在掘金上面有一本有明写的小册,介绍了Laravel中比较多的设计模式,有兴趣的同学可以自行去查看。

Laravel的容器机制怎么实现

通过bind或者singleton等方法进行名称和实例的注册绑定。

调用容器的make方法或者通过依赖注入,容器会通过PHP的反射机制来实例化具体的对象进行返回。

相关链接:Container 容器Laravel 服务容器必知必会

Laravel的路由机制怎么实现

  1. 加载用户路由配置。
  2. 框架HTTP内核获取请求对象,发送到路由组件。
  3. 路由组件解析URL匹配对应的Controller并调用匹配的方法。
  4. 生成HTTP响应返回。

相关链接:深入浅出 Laravel 路由执行原理

Swoole主要进程和他们的关系

Master 进程、Reactor 线程、Worker 进程、Task 进程、Manager 进程。

他们之间的关系可以理解为 Reactor 就是 nginxWorker 就是 PHP-FPMReactor 线程异步并行地处理网络请求,然后再转发给 Worker 进程中去处理。ReactorWorker 间通过 unixSocket 进行通信。

PHP-FPM 的应用中,经常会将一个任务异步投递到 Redis 等队列中,并在后台启动一些 PHP 进程异步地处理这些任务。Swoole 提供的 TaskWorker 是一套更完整的方案,将任务的投递、队列、PHP 任务处理进程管理合为一体。通过底层提供的 API 可以非常简单地实现异步任务的处理。另外 TaskWorker 还可以在任务执行完成后,再返回一个结果反馈到 Worker

SwooleReactorWorkerTaskWorker 之间可以紧密的结合起来,提供更高级的使用方式。

一个更通俗的比喻,假设 Server 就是一个工厂,那 Reactor 就是销售,接受客户订单。而 Worker 就是工人,当销售接到订单后,Worker 去工作生产出客户要的东西。而 TaskWorker 可以理解为行政人员,可以帮助 Worker 干些杂事,让 Worker 专心工作。

相关链接:Swoole不同进程的区别与联系

Swoole和Laravel有什么区别

Swoole

是一个用C语言编写的PHP扩展,是PHP的协程高性能网络通信引擎。

Laravel

是一个用PHP编写的Web开发框架,类似的还有ThinkPHP、Yii框架等。

相关链接:Swoole、Laravel的区别

Swoole为什么性能这么高

  • 常驻内存,避免重复加载带来的性能损耗,提升海量性能。

  • Reactor基于epoll,轻松支持高并发。

  • 协程异步,提高对 I/O 密集型场景并发处理能力。

  • 支持多种通信协议,方便地开发 Http、WebSocket、TCP、UDP 等应用。

Swoole和OPcache有什么区别

OPcache

通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码的好处就是省去了每次加载和解析 PHP 脚本的开销。

Swoole

除了常驻内存以外,还有协程异步I/O等操作,比OPcache对性能的提升更大。

相关链接:PHP Opcache工作原理 - 王志强的文章 - 知乎

Swoole的协程和Go的协程有什么区别

Swoole

  • Swoole 的协程客户端必须在协程的上下文环境中使用。
  • Swoole 的协程是基于单线程的,无法利用多核CPU,同一时间只有一个在调度。

Go

  • Go 语言原生层面就支持协层,不需要声明协程环境。
  • Go 协程是基于多线程的,可以利用多核 CPU,同一时间可能会有多个协程在执行。

相关链接:Swoole协程与Go协程的区别

MySQL

MySQL有哪些数据库引擎及其区别

InnoDB、MyISAM、Archive、Blackhole、CSV

相关链接:MySQL引擎之间的区别和优劣之分

MySQL有哪些索引

普通索引

是最基本的索引,它没有任何限制

CREATE INDEX index_name ON table(column)

唯一索引

与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。

CREATE UNIQUE INDEX indexName ON table(column)

主键索引

是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引

CREATE TABLE `table` (
    `id` int(11) NOT NULL AUTO_INCREMENT ,
    `title` char(255) NOT NULL ,
    PRIMARY KEY (`id`)
);

组合索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合,即支持 a|a,b|a,b,c 3种组合进行查找。

CREATE INDEX index_name ON table(column1,column2,column3)

全文索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。

CREATE FULLTEXT INDEX index_name ON table(column)

相关链接:MySQL索引类型

InnoDB索引有哪些常见数据结构

InnoDB索引常见的数据存储结构有Hash结构,B+树结构。

Hash结构

优点:

  • 存储时只需要存储对应的哈希值,存储速度很快。
  • 进行检索效率非常高,基本上一次检索就可以找到数据。

缺点:

  • Hash 索引不能进行范围查询。
  • Hash 索引不支持联合索引的最左侧原则。
  • Hash 索引不支持 ORDER BY 排序。
  • 存在Hash冲突。

B+树结构

优点:

  • Btree索引可以加快数据的查询速度。
  • Btree索引更适合范围查找。

缺点:

  • 命中的数据占用了表中大部分数据,那么mysql查询优化器会认为全表扫描方式更好。
  • 不是按照最左原则,无法使用索引。
  • not in 和 <>也无法使用索引。
  • 如果查询中有某个列的范围查询,则其右边所有的列都无法使用索引。

相关链接:Mysql索引结构与索引原理及hash索引介绍

为什么使用B+树而不使用B树

  1. B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。
  2. 因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。

相关链接:Mysql索引结构与索引原理及hash索引介绍b树和b+树的区别

什么是回表

MySQL索引分为两类:聚簇索引和非聚簇索引。

一般主键索引就是聚簇索引,索引里会保存这一行记录的所有的数据。

而普通索引就是非聚簇索引,只会保存这一行记录的主键值。

当查询的字段中存在非索引字段时,就需要依靠普通索引查询出来的主键值,再去查询一次才能获取这一行记录的其他数据,即需要扫描两次索引表,这个操作就叫做回表。

想要避免回表操作可以使用索引覆盖。

相关链接:MySQL 的覆盖索引与回表

MySQL有哪些架构模式

MHA(Master High Availability)

优点:

  • 可以支持基于GTID的复制模式
  • MHA在进行故障转移时更不易产生数据丢失
  • 同一个监控节点可以监控多个集群

缺点:

  • 需要编写脚本或利用第三方工具来实现Vip的配置
  • MHA启动后只会对主数据库进行监控
  • 需要基于SSH免认证配置,存在一定的安全隐患

MMM(Master-Master replication manager for MySQL)

优点:

  • 使用Perl脚本语言开发及完全开源
  • 提供了读写VIP(虚拟IP),使服务器角色的变更对前端应用透明
  • MMM提供了从服务器的延迟监控
  • MMM提供了主数据库故障转移后从服务器对新主的重新同步功能
  • 很容易对发生故障的主数据库重新上线

缺点:

  • 发布时间比较早不支持MySQL新的复制功能(基于GTID的复制)
  • 没有读负载均衡的功能
  • 在进行主从切换时,容易造成数据丢失
  • MMM监控服务存在单点故障

相关链接:MySQL高可用集群方案

如何优化SQL性能

这个问题几乎是可以写出来一本书了,这里只说一些简单的优化方法。

  1. 在表中建立索引,优先考虑where、group by使用到的字段。
  2. 尽量避免使用select *,返回无用的字段会降低查询效率。
  3. 尽量避免使用in 和not in,会导致数据库引擎放弃索引进行全表扫描。
  4. 尽量避免使用or,会导致数据库引擎放弃索引进行全表扫描。
  5. 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。
  6. 尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。
  7. 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。
  8. 更进一步可以使用Redis进行数据的缓存。
  9. 数据量过大的话还可以考虑分区分表。

相关链接:SQL优化(面试题)

MySQL事务的四大特性以及四种隔离级别

四大特性ACID

  • 原子性(Atomicity):即不可分割,事务要么全部被执行,要么全部不执行。
  • 一致性(Consistency):事务的执行使得数据库从一种正确状态转换成另外一种正确状态。
  • 隔离性(Isolation):在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务。
  • 持久性(Durability):事务正确提交之后,其结果将永远保存在数据库之中,即使在事务提交之后有了其他故障,事务的处理结果也会得到保存。

四种隔离级别

MySQL默认的事务隔离级别为repeatable-read。

  • 读未提交(read-uncommitted):事务对当前被读取的数据不加锁; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。
  • 不可重复读(read-committed):事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。
  • 可重复读(repeatable-read):事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。
  • 串行化(serializable):事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放; 事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放。
事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

相关链接:MySQL的四种事务隔离级别深入理解MySQL中事务隔离级别的实现原理

什么是悲观锁、乐观锁、MVCC机制

悲观锁

如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

数据库悲观锁的加锁流程大致如下:

  • 开始事务后,按照操作类型给需要加锁的数据申请加某一类锁:例如共享行锁等
  • 加锁成功则继续后面的操作,如果数据已经被加了其他的锁,而且和现在要加的锁冲突,则会加锁失败(例如已经加了排他锁),此时需等待其他的锁释放(可能出现死锁)
  • 完成事务后释放所加的锁

优点:悲观并发控制采取的是保守策略:“先取锁,成功了才访问数据”,这保证了数据获取和修改都是有序进行的,因此适合在写多读少的环境中使用。当然使用悲观锁无法维持非常高的性能,但是在乐观锁也无法提供更好的性能前提下,悲观锁却可以做到保证数据的安全性。

缺点:由于需要加锁,而且可能面临锁冲突甚至死锁的问题,悲观并发控制增加了系统的额外开销,降低了系统的效率,同时也会降低了系统的并行性。

乐观锁

而是在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,则让返回冲突信息,让用户决定如何去做下一步,比如说重试或者回滚。

乐观锁的实现主要有以下几种方式:

  • 版本号标记:在表中新增一个字段:version,用于保存版本号。
  • 时间戳标记:和版本号一样,只是通过时间戳来判断。
  • 待更新字段:如果没有时间戳字段,而且不想新增字段,那可以考虑用待更新字段来判断。
  • 所有字段标记:数据表所有字段都用来判断。

优点:乐观并发控制没有实际加锁,所以没有额外开销,也不会出现死锁问题,适用于读多写少的并发场景,因为没有额外开销,所以能极大提高数据库的性能。

缺点:乐观并发控制不适合于写多读少的并发场景下,因为会出现很多的写冲突,导致数据写入要多次等待重试,在这种情况下,其开销实际上是比悲观锁更高的。而且乐观锁的业务逻辑比悲观锁要更为复杂,业务逻辑上要考虑到失败,等待重试的情况,而且也无法避免其他第三方系统对数据库的直接修改的情况。

MVCC机制(多版本并发控制(Multiversion concurrency control))

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。每个事务读到的数据项都是一个历史快照,被称为快照读,不同于当前读的是快照读读到的数据可能不是最新的,但是快照隔离能使得在整个事务看到的数据都是它启动时的数据状态。而写操作不覆盖已有数据项,而是创建一个新的版本,直至所在事务提交时才变为可见。

优点:MVCC 使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。

缺点:不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

相关链接:一文读懂数据库中的乐观锁和悲观锁和MVCC

MySQL事务是怎么实现的

  • 事务的原子性是通过 undo log 来实现的
  • 事务的持久性性是通过 redo log 来实现的
  • 事务的隔离性是通过 (读写锁+MVCC)来实现的
  • 而事务的一致性是通过原子性,持久性,隔离性来实现的。

相关链接:Mysql的事务实现原理

MySQL主从数据库怎么同步数据

主从同步简单可以分为三步:

  1. master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events)。
  2. slave将master的binary log events拷贝到它的中继日志(relay log)。
  3. slave重做中继日志中的事件,同步数据。

相关链接:MySQL数据库主从同步原理

Redis

Redis有哪些常用数据类型

String、Hash、List、Set、SortedSet、Pub/Sub。

相关链接:Redis 命令参考

怎么用Redis统计上亿访问量的日活跃用户

可以使用Bitmap或者HyperLogLog来进行统计。

Bitmap

因为bitmap的每一位只占据1bit的空间 ,所以利用这个特性我们可以把每一天作为key,value为1亿用户的活跃度状态。假设一个用户一天内只要登录了一次就算活跃。活跃我们就记为1,不活跃我们就记为0。把用户Id作为偏移量(offset)。这样我们一个key就可以存储1亿用户的活跃状态。

HyperLogLog

HyperLogLog 是用来做基数统计的算法,直接把用户ID插入到一个key中,

两者区别

如果需要精确结果的话,可以使用bitmap,1亿数据大约占用12M内存。

如果只需要一个大概结果的话,可以使用HyperLogLog,1亿数据大约占用14k内存。

相关链接:Redis如何存储和计算一亿用户的活跃度

Redis怎么实现分布式锁

将用户ID作为key,在Redis中设置一个value。

需要注意的点是要用到 NX 参数,保证key是不存在才设置成功。

还需要用到 EX 参数设置过期时间,防止出现异常时锁无法释放。

set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)

相关链接:redis分布式锁?坑那么多

Redis Cluster集群获取key对应的value的流程

  1. redis cluster模式采用了无中心节点的方式来实现,每个主节点都会与其它主节点保持连接。节点间通过gossip协议交换彼此的信息,同时每个主节点又有一个或多个从节点。
  2. 客户端连接集群时,直接与redis集群的每个主节点连接,根据hash算法取模将key存储在不同的哈希槽上。
  3. 在集群中采用数据分片的方式,将redis集群分为16384个哈希槽。
  4. 每个节点会保存一份数据分布表,节点会将自己的slot信息发送给其他节点,节点间不停的传递数据分布表。
  5. 客户端连接集群时,通过集群中某个节点地址进行连接。客户端尝试向这个节点执行命令时,比如获取某个key值,如果key所在的slot刚好在该节点上,则能够直接执行成功。如果slot不在该节点,则节点会返回MOVED错误,同时把该slot对应的节点告诉客户端,客户端可以去该节点执行命令。

相关链接:想进大厂,面试却挂在了Redis集群上

Redis Cluster集群中某个节点故障了该怎么获取数据

redis cluster中主节点故障处理方式与哨兵模式较为相像,当约定时间内某节点无法与集群中的另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态,同时将这个信息向整个集群广播。

如果一个节点收到某个节点失联的数量达到了集群的大多数时,那么将该节点标记为客观下线状态,并向集群广播下线节点的fail消息。然后立即对该故障节点进行主从切换。等到原来的主节点恢复后,会自动成为新主节点的从节点。如果主节点没有从节点,那么当它发生故障时,集群就将处于不可用状态。

相关链接:想进大厂,面试却挂在了Redis集群上

Redis Cluster集群怎么扩容

当集群中加入某个节点时,哈希槽又是如何来进行分配的?

当集群中加入新节点时,会与集群中的某个节点进行握手,该节点会把集群内的其它节点信息通过gossip协议发送给新节点,新节点与这些节点完成握手后加入到集群中。

然后集群中的节点会各取一部分哈希槽分配给新节点。

当集群中要删除节点时,只需要将节点中的所有哈希槽移动到其它节点,然后再移除空白(不包含任何哈希槽)的节点就可以了。

相关链接:想进大厂,面试却挂在了Redis集群上

Redis主从数据库怎么同步数据

全量同步

  1. 当从节点启动时,会向主节点发送SYNC命令。
  2. 主节点接收到SYNC命令后,开始在后台执行保存快照的命令生成RDB文件,并使用缓冲区记录此后执行的所有写命令。
  3. 主节点快照完成后,将快照文件和所有缓存命令发送给集群内的从节点,并在发送期间继续记录被执行的写命令。
  4. 主节点快照发送完毕后开始向从节点发送缓冲区中的写命令。
  5. 从节点载入快照文件后,开始接收命令请求,执行接收到的主节点缓冲区的写命令。

增量同步

主从复制中因网络等原因造成数据丢失场景,当从节点再次连上主节点。如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。

相关链接:想进大厂,面试却挂在了Redis集群上

Redis的数据持久化有哪些方式

Redis的数据持久化有两种方式,一种是RDB,一种是AOF。

RDB

RDB:把当前数据生成快照保存在硬盘上。

触发方式有两种

  • 手动触发:save、bgsave命令
  • 自动触发:在Redis的配置文件中配置触发条件

优点

  • RDB文件是一个紧凑的二进制压缩文件,是Redis在某个时间点的全部数据快照。所以使用RDB恢复数据的速度远远比AOF的快,非常适合备份、全量复制、灾难恢复等场景。

缺点

  • 每次进行bgsave操作都要执行fork操作创建子经常,属于重量级操作,频繁执行成本过高,所以无法做到实时持久化,或者秒级持久化。

AOF

AOF:记录每次对数据的操作到硬盘上。

触发方式有两种

  • 手动触发:使用bgrewriteaof命令。
  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage配置确定自动触发的时机。

主要步骤

  1. 命令追加(append):所有写命令都会被追加到AOF缓存区(aof_buf)中。
  2. 文件同步(sync):根据不同策略将AOF缓存区同步到AOF文件中。
  3. 文件重写(rewrite):定期对AOF文件进行重写,以达到压缩的目的。
  4. 数据加载(load):当需要恢复数据时,重新执行AOF文件中的命令。

三种同步策略

  1. always:每次写入缓存区都要同步到AOF文件中,硬盘的操作比较慢,限制了Redis高并发,不建议配置。
  2. no:每次写入缓存区后不进行同步,同步到AOF文件的操作由操作系统负责,每次同步AOF文件的周期不可控,而且增大了每次同步的硬盘的数据量。
  3. eversec:每次写入缓存区后,由专门的线程每秒钟同步一次,做到了兼顾性能和数据安全。是建议的同步策略,也是默认的策略。

优点

  • 持久化的速度快,因为每次都只是追加,rdb每次都全量持久化。
  • 数据相对更可靠,丢失少,因可以配置每秒持久化、每个命令执行完就持久化

缺点

  • 灾难性恢复的时候过慢,因为aof每次都只追加原命令,导致aof文件过大,但是后面会rewrite,但是相对于rdb也是慢的。
  • 会对主进程对外提供请求的效率造成影响,接收请求、处理请求、写aof文件这三步是串行原子执行的。而非异步多线程执行的。Redis单线程!

相关链接:一篇文章彻底理解Redis持久化:RDB和AOF

Redis过期键回收策略

Redis 过期策略有两种

  1. 定期删除
  2. 惰性删除

定期删除

Redis每隔100ms就会随机抽取一些设置了过期时间的key来进行检查和删除。

需要注意的是,这里是随机抽取,而不是全量遍历,全量遍历会大幅影响Redis性能。

这样造成的后果是部分已过期的key并不会被实时删除。

惰性删除

为了弥补定期删除的缺陷,就需要引入惰性删除机制。

当获取某个key的时候,Redis会检查是否设置了过期时间,key是否已过期,如果已过期则进行删除处理。

相关链接:Redis的过期策略以及内存淘汰机制

Redis内存淘汰策略

当Redis的内存被占满后,如果继续写入数据就会触发淘汰策略。

淘汰策略

  • noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  • volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  • allkeys-random:加入键的时候如果过限,从所有key随机删除
  • volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  • volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-lfu:从所有键中驱逐使用频率最少的键

比较常用的是allkeys-lru策略。

LRU算法

LRU 即最近最久未使用。

LRU算法需要一个双向链表来记录数据的最近被访问顺序,但是出于节省内存的考虑,RedisLRU算法并非完整的实现。Redis并不会选择最久未被访问的键进行回收,相反它会尝试运行一个近似LRU的算法,通过对少量键进行取样,然后回收其中的最久未被访问的键。通过调整每次回收时的采样数量maxmemory-samples,可以实现调整算法的精度

相关链接:Redis的过期策略以及内存淘汰机制

Nginx

Nginx和PHP-FPM怎么通信

一般有两种方式:TCP Socket、Unix Socket。

通过修改Nginx配置中的 fastcgi_pass 参数来进行切换协议。

TCP Socket

优点:可以跨服务器通信,高并发时也稳定。

缺点:增加了一层网络传输,效率较低。

Unix Socket

优点:不需要经过网络协议栈,开销较小,效率较高。

缺点:高并发时不稳定,连接数爆发时,会产生大量的长时缓存。

相关链接:PHP-FPM和Nginx的通信机制

Nginx负载均衡有哪些策略

  • round robin(轮询)
  • random(随机)
  • weight(权重)
  • fair(按响应时长,三方插件)
  • url_hash(url的hash值)
  • ip_hash(ip的hash值)
  • least_conn(最少连接数)

相关链接:Nginx的负载均衡策略及其配置

Nginx和Apache有什么区别

Nginx

  • Nginx处理请求是异步非阻塞的,在高并发下Nginx能保持低资源低消耗高性能。
  • 轻量级,比Apache占用更少的内存及资源。
  • 静态处理,Nginx静态处理性能比Apache高3倍以上。

Apache

  • Apache处理请求是同步阻塞的,性能较差。
  • 模块超多,基本想到的都可以找到。
  • 超稳定。

相关链接:Nginx和Apache对比

网络

常见的HTTP方法有哪些

GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT

相关链接:HTTP请求方式中8种请求方法

GET和POST有什么区别

GET

  • GET请求是可以缓存的,我们可以从浏览器历史记录中查找到GET请求。
  • GET方法提交的数据会直接填充在请求报文的URL上。
  • 浏览器处理的URL最大限度长度为1024B(不同浏览器不一样),所以GET方法提交参数长度有限制。

POST

  • POST方法提交的数据会附在正文上(request body)。
  • 一般请求正文的长度是没有限制的,但表单中所能处理的长度一般为100k(不同协议不同浏览器不一样)。

所以GET方法可以用来传输一些可以公开的参数信息,解析也比较方便,如百度的搜索的关键词,而POST方法可以用来提交一个用户的敏感信息。

相关链接:HTTP请求方式中8种请求方法

额外阅读:99%的人都理解错了HTTP中GET与POST的区别

POST请求中application/x-www-form-urlencoded和multipart/form-data有什么区别

application/x-www-form-urlencoded

  • 最常见的 POST 提交数据的方式,原生Form表单默认格式。
  • 提交的表单数据会转换为键值对并按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。
  • 无法上传文件
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8

name=test&val1=1&val2=%E6%B5%8B%E8%AF%95&val3%5B%5D=2

multipart/form-data

  • 以标签为单元,用分隔符(boundary)分开。
  • 它既可以上传键值对,也可以上传文件,甚至多个文件。
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"

title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

相关链接:POST的Content-Type引发的HTTP相关知识思考

HTTP报文包含哪些内容

HTTP的请求报文由四部分组成(请求行+请求头部+空行+请求体)。

请求行

  1. 请求方法:GET、POST
  2. 请求对应的URL地址:/abc/example.html
  3. 协议名称及版本号:HTTP/1.1

请求头

请求头包含多个属性

  • Host:给出了接收请求的服务器的主机名和端口号
  • Referer:提供了包含当前请求URI的文档的URL
  • User-Agent:将发起请求的应用程序名称告知服务器
  • Cookie:客户端用它向服务器传送数据
  • ...

空行

包含一个回车符和一个换行符。

请求体

它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。

相关链接:一篇让你彻底了解http请求报文和响应报文的结构

常见的header属性

  • Client-IP:提供了运行客户端的机器的IP地址
  • From:提供了客户端用户的E-mail地址
  • Host:给出了接收请求的服务器的主机名和端口号
  • Referer:提供了包含当前请求URI的文档的URL
  • UA-OS:给出了运行在客户端机器上的操作系统名称及版本
  • User-Agent:将发起请求的应用程序名称告知服务器
  • Accept:告诉服务器能够发送哪些媒体类型
  • Accept-Charset:告诉服务器能够发送哪些字符集
  • Cookie:客户端用它向服务器传送数据

相关链接:http headers大总结

TCP与UDP的区别和使用场景

TCP

  • TCP充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。
  • TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。
  • 根据 TCP 的这些机制,在 IP 这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现)。

UDP

  • UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务。
  • 传输途中出现丢包,UDP 也不负责重发。
  • 当包的到达顺序出现乱序时,UDP没有纠正的功能。

使用场景

TCP

  • SMTP:电子邮件
  • TELNET:远程终端接入
  • HTTP:万维网
  • FTP:文件传输

UDP

  • DNS:域名转换
  • TFTP:文件传输
  • SNMP:网络管理
  • NFS:远程文件服务器

相关链接:面试官:说说UDP和TCP的区别及应用场景

什么是Socket

Socket,又称套接字,是Linux跨进程通信(IPC,Inter Process Communication,详情参考:Linux进程间通信方式总结)方式的一种。根据通信域的不同可以划分成2种:Internet domain socket 和 Unix domain socket。

Internet domain socket

Internet domain socket用于实现不同主机上的进程间通信。

一个完整的Socket的组成应该是由**[协议,本地地址,本地端口,远程地址,远程端口]**组成的一个5维数组

Socket根据通信协议的不同还可以分为3种:

  • 流式套接字(SOCK_STREAM):最常见的套接字,使用TCP协议,提供可靠的、面向连接的通信流。
  • 数据报套接字(SOCK_DGRAM):使用UDP协议,提供无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠性。
  • 原始套接字:允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。原始套接字主要用于一些协议的开发,可以进行比较底层的操作。

Unix domain socket

Unix domain socket 又叫 IPC(inter-process communication 进程间通信) socket,用于实现同一主机上的进程间通信。

不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

一般通过监听读写一个xxx.sock文件来实现,如Nginx与PHP-FPM之间通信。

相关链接:手撕Linux Socket——Socket原理与实践分析

WebSocket和Socket的区别

Socket

简单理解:Socket = IP地址 + 端口 + 协议。

具体来说,Socket是一套标准,它完成了对TCP/IP的高度封装,屏蔽网络细节以方便开发者更好地进行网络编程。Socket有自己的原语。

WebSocket

WebSocket是一个持久化的协议,它是伴随HTML5而出的协议,用来解决HTTP不支持持久化连接的问题。

总之,WebSocket和Socket其实是两个东西,Socket一个是网络编程的标准接口,而WebSocket是应用层通信协议。

相关链接:WebSocket与普通Socket的差异

select模型和epoll模型的区别

select

时间复杂度O(n)。

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

epoll

时间复杂度O(1)。

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

总结

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

  1. epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
  2. 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
  3. select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

相关链接:select、poll、epoll之间的区别

HTTPS链接的建立流程

https中的s表示SSL或者TLS,就是在原http的基础上加上一层用于数据加密、解密、身份认证的安全层。

建立流程:

  1. 客户端: 我要发起HTTPS请求,麻烦给我一个非对称加密的公钥。
  2. 服务器: (生成一对非对称加密的密钥对,然后把公钥发给客户端),接着,这是公钥。
  3. 客户端:(收到公钥,生成一个随机数,作为上图中那一把密钥,用刚才收到的公钥加密这个密钥,然后发给服务器)这是我刚生成的加密过的密钥。
  4. 服务器:(收到加密后的密钥,用本地的第一步自己生成的非对称加密的私钥解密,得到真正的密钥)。
  5. 现在,客户端和服务器都知道了这把密钥,就能愉快地用这个密钥对称加密数据。

相关链接:看完还不懂HTTPS我直播吃翔

CSRF和XSS攻击的区别以及防御手段

XSS

跨站脚本(Cross-site scripting,通常简称为XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言

防御:

其实很简单,只要把JavaScript的(< script>< /script>)这个脚本标签进行处理就行了,通常是做一层转义,把<和>转变成’&lt‘和’&gt‘。甚至把单双引号,斜杆这些脚本经常使用到的符号进行转义,从而导致xss攻击失效。

CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法

简单讲就是你刚刚登陆了腾讯充值中心,登陆充钱完了,你就关掉网站了,换了另外一个网站www.xiaomizhou.com

然后这个网站里面有个图片或者是超链接,这个url是www.qq.com/chongzhizhongxin/pay?accout=xiaomizhou&number=10000;(我们假设这是qq充值中心对于充值的api接口,现实中肯定不是这样也没有这么简单)。只要你点击了这个按钮,你就会向小米粥账户充值10000个q币了。

防御:

只要我们开发人员,在一些重要的接口上设置一个验证过程,而这个验证是需要在qq充值中心网页的一个随机码(可以是时间戳加密),然后通过一系列的加密才能够获得。

而csrf攻击是在www.xiaomizhou.com上发布出来的,没有进入到qq充值中心,所以无法获得这个随机数,从而调用接口的时候这个部分的信息为空或者错误,然后调用失败。

相关链接:用最简单的话,一分钟讲明白xss和csrf攻击,以及如何防御

什么是RPC

说起RPC,就不能不提到分布式,这个促使RPC诞生的领域。

假设你有一个计算器接口,Calculator,以及它的实现类CalculatorImpl,那么在系统还是单体应用时,你要调用Calculator的add方法来执行一个加运算,直接new一个CalculatorImpl,然后调用add方法就行了,这其实就是非常普通的本地函数调用,因为在同一个地址空间,或者说在同一块内存,所以通过方法栈和参数栈就可以实现。

现在,基于高性能和高可靠等因素的考虑,你决定将系统改造为分布式应用,将很多可以共享的功能都单独拎出来,比如上面说到的计算器,你单独把它放到一个服务里头,让别的服务去调用它。

这下问题来了,服务A里头并没有CalculatorImpl这个类,那它要怎样调用服务B的CalculatorImpl的add方法呢?

有同学会说,可以模仿B/S架构的调用方式呀,在B服务暴露一个Restful接口,然后A服务通过调用这个Restful接口来间接调用CalculatorImpl的add方法。

很好,这已经很接近RPC了,不过如果是这样,那每次调用时,是不是都需要写一串发起http请求的代码呢?比如httpClient.sendRequest…之类的,能不能像本地调用一样,去发起远程调用,让使用者感知不到远程调用的过程呢。

这时候,有同学就会说,用代理模式呀!而且最好是结合Spring IoC一起使用,通过Spring注入calculator对象,注入时,如果扫描到对象加了@Reference注解,那么就给它生成一个代理对象,将这个代理对象放进容器中。而这个代理对象的内部,就是通过httpClient来实现RPC远程过程调用的。

可能上面这段描述比较抽象,不过这就是很多RPC框架要解决的问题和解决的思路,比如阿里的Dubbo。

总结一下,RPC要解决的两个问题:

  1. 解决分布式系统中,服务之间的调用问题。
  2. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

相关链接:如何给老婆解释什么是RPC

数据结构和算法

了解哪些排序算法

冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序。

相关链接:常用的排序算法总结

Top K问题

例如:有10亿个数,如果找出其中最大的10000个?

针对top K类问题,通常比较好的方案是分治+Trie树/hash+小顶堆,即先将数据集按照Hash方法分解成多个小数据集,然后使用Trie树或者Hash统计每个小数据集中的数据,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有top K中求出最终的top K。

相关链接:10亿个数中找出最大的10000个数

字符串的最长不重复子串问题

这个题目,其实主要的考点在于对 滑动窗口 的理解。

滑动窗口:滑动窗口是数组、字符串问题中常用的抽象概念。

窗口通常是在数组、字符串中由开始和结束索引定义的一系列元素的集合,即 [i, j)(左闭,右开)。

而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将 [i, j) 向右滑动 1 个元素,

则它将变为 [i+1, j+1)[i+1,j+1)(左闭,右开)。

相关链接:让我们一起啃算法----无重复字符的最长子串

打乱一个有序数组

Knuth-Shuffle 洗牌算法

假设有这样一个数组 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],我们使用 Knuth-Shuffle 算法将数据打乱。

基本流程是这样的,从最后一个数开始,往前遍历,每一次,从当前数和第 1 个数之间,随机选择一个数,与当前数字进行交换(这里的随机选择就直接使用程序语言中的 Random 随机一个索引即可)。

这个算法之所以公平,是因为保证了每一个元素出现在每一个位置上的概率,都是一样的

相关链接:一个公平的洗牌算法 | Knuth-Shuffle

前端

var let const分别是什么

ES6 中新增let命令,用来声明变量。它的用法类似于var,但是不存在变量提升,且只在let命令所在的代码块内有效,

const是用了声明只读常量的,一旦声明,常量的值就不能修改。

letconst 命令都不允许重复声明同一个变量。

var 是允许重复声明的,但重复声明显然是不建议的。

相关链接:浅谈var、let 和 const 的区别

Vue的生命周期

Vue实例有一个完整的生命周期,生命周期也就是指一个实例从开始创建到销毁的这个过程

  • beforeCreate() 在实例创建之间执行,数据未加载状态
  • created() 在实例创建、数据加载后,能初始化数据,dom渲染之前执行
  • beforeMount() 虚拟dom已创建完成,在数据渲染前最后一次更改数据
  • mounted() 页面、数据渲染完成,真实dom挂载完成
  • beforeUpdate() 重新渲染之前触发
  • updated() 数据已经更改完成,dom 也重新 render 完成,更改数据会陷入死循环
  • beforeDestory()destoryed() 前者是销毁前执行(实例仍然完全可用),后者则是销毁后执行

相关链接:Vue 出场率99%的面试题

跨域问题如何解决

前端调用的后端接口不属于同一个域(域名或端口不同),就会产生跨域问题,也就是说你的应用访问了该应用域名或端口之外的域名或端口。

解决跨域问题的三种思路

JSONP

JSONP是JSON with Padding的缩写。

<script>标签的跨域是不受同源策略限制的。那么我们向后端请求一个js文件,在这个js文件当中,返回一个函数的执行,而我们想要得到的数据,是这个函数的参数。

CORS

CORS是Cross-Origin Resource Sharing的缩写。

CORS新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。

服务器返回的response header中,会有一个Access-Control-Allow-Origin字段,里面写明允许那些“源”,浏览器发现两者一致,或者服务器允许所有的“源”,那么跨域成功!

反向代理

跨域只是浏览器向服务器发送请求的时候,浏览器的限制。而服务器和服务器之间是没有跨域的限制的。反向代理是利用代理服务器接收到请求之后,转发给真正的服务器,并把结果返回到浏览器上。

相关链接:前端面试题——跨域

什么叫闭包

小"黄"书(你不知道的JavaScript):当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

红宝书(JavaScript高级程序设计):闭包是指有权访问另一个函数作用域中的变量的函数。

var local = "变量"
function foo(){
  console.log(local)
}

三行代码中,有一个局部变量 local,有一个函数 foo,foo 里面可以访问到 local 变量。

好了这就是一个闭包:

「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。

相关链接:「每日一题」JS 中的闭包是什么?

Go

Go和PHP有哪些区别

Go 是由 Google 设计的一门静态类型的编译型语言。

PHP 是一种动态类型语言。

数据类型

Go 同时支持有符号和无符号整数,而 PHP 只支持有符号整数。

另一个主要区别是数组。Go 对 array 和 map 有单独的类型,而 PHP 数组实际上是有序的 map。

Go 与 PHP 相比没有对象。但是,Go 有一个类似于 object 的 struct 类型。

函数

Go 函数可以返回任意数量的结果,而 PHP 函数只能返回一个结果。

但是,PHP 可以通过返回数组来模拟相同的功能。

相关链接:Go与PHP的语法对比

Go中的协程之间怎么通信

不要通过共享内存来通信,而应该通过通信来共享内存。

Go中的协程一般都是通过Channel来进行通信。

Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。

相关链接:GO语言基础进阶教程:channel通道

Go的协程调度模型

Go的协程调度模型即GPM调度模型。

G:Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。

P: Processor,表示逻辑处理器,对 G 来说,P 相当于 CPU 核,G 只有绑定到 P 才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等。P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量)。P 的数量由用户设置的 GoMAXPROCS 决定,但是不论 GoMAXPROCS 设置为多大,P 的数量最大为 256。

M: Machine,OS 内核线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取。**M 的数量是不定的,由 Go Runtime 调整,**为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。

具体流程如下

  1. 我们通过 go func () 来创建一个 goroutine。
  2. 有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中。
  3. G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行。
  4. 一个 M 调度 G 执行的过程是一个循环机制。
  5. 当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P。
  6. 当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

相关链接:全文图解Golang 调度器 GMP 原理与调度

小结

以上就是今年我遇到的大部分面试题,其实还有很多题目但是记不起来了。

可以看到,除了PHP以外,还会考到一些前端或者Go的题目,了解不同方向的技术也是挺重要的。

希望这篇文章对你有所帮助。

祝各位都能早日找到自己喜欢的工作。