腾讯QQ浏览器后台开发一面面经,问得倒不难,反手秒挂。。。
1、GET和POST区别,什么是幂等?
GET
- 用法:用于从服务器检索资源。GET 请求应该是安全的,不会对服务器上的资源进行修改。
- 参数传递:通常通过 URL 的查询字符串传递参数。
- 幂等性:GET 请求是幂等的,无论发起多少次相同的 GET 请求,结果都是相同的,不会改变服务器上的资源状态。
POST
- 用法:用于向服务器发送数据,通常用于创建资源或提交数据进行处理。POST 请求会对服务器上的资源产生变化。
- 参数传递:通常通过请求体传递参数,而不是通过 URL。
- 幂等性:POST 请求不是幂等的。重复相同的 POST 请求可能会导致不同的结果,例如创建多个资源。
幂等性是指一个操作无论执行多少次,其结果都是相同的。换句话说,幂等操作在重复执行时不会改变系统的状态或引起额外的副作用。
幂等性在HTTP方法中的应用
在HTTP协议中,不同的HTTP方法具有不同的幂等性特性。常见HTTP方法的幂等性:
幂等的HTTP方法
GET:获取资源。多次执行相同的GET请求,返回的结果是相同的,不会对服务器上的资源产生任何修改。
GET /resource/123无论执行多少次,都会返回ID为123的资源内容,不改变服务器状态。
PUT:更新资源。多次执行相同的PUT请求,资源的最终状态保持一致,即便请求被重复,资源的内容仍然是更新后的状态。
PUT /resource/123 Content-Type: application/json { "name": "new name", "value": "new value" }无论执行多少次,ID为123的资源都会被更新为相同的内容。
DELETE:删除资源。多次执行相同的DELETE请求,资源的最终状态是被删除的,无论资源是否已经被删除,重复删除操作不会引起新的副作用。
DELETE /resource/123无论执行多少次,ID为123的资源都会被删除,不存在时不会有其他副作用。
HEAD:与GET类似,但只请求资源的元数据而不返回资源本身。多次执行相同的HEAD请求,结果也是相同的。
非幂等的HTTP方法
POST:创建资源或提交数据。多次执行相同的POST请求,可能会导致不同的结果。例如,创建多个相同的资源或重复提交数据。
POST /resource Content-Type: application/json { "name": "new resource", "value": "new value" }每次执行都会创建一个新的资源,导致服务器上的资源数量增加。
幂等性的实现
在实现幂等操作时,可以使用一些常见的设计模式和技术,例如:
- 唯一请求标识符:为每个请求生成一个唯一的标识符,并在服务器端记录已处理的标识符,以避免重复处理。
- 乐观锁定:在更新资源时,使用版本号或时间戳来确保资源在更新期间未被其他请求修改。
- 数据库约束:在数据库层面设置唯一约束,防止重复的数据插入。
2、HTTPS如何加密?
HTTPS通过使用SSL或TLS协议来加密数据,从而确保客户端和服务器之间的通信安全。
HTTPS的加密过程:
1. 建立连接
当客户端(如浏览器)尝试与服务器建立HTTPS连接时,首先会进行TCP连接的三次握手。完成TCP连接后,HTTPS加密流程正式开始。
2. SSL/TLS握手过程
SSL/TLS握手是HTTPS加密的核心部分。握手过程:
2.1 客户端Hello
客户端向服务器发送一个ClientHello消息,包含这些内容:
- 客户端支持的SSL/TLS版本
- 支持的加密算法(如AES、RSA等)
- 一个随机数
- 其他支持的选项和扩展
2.2 服务器Hello
服务器收到ClientHello消息后,回应一个ServerHello消息,包含以下内容:
- 服务器选择的SSL/TLS版本
- 选择的加密算法
- 另一个随机数
- 服务器的数字证书
2.3 服务器证书
服务器向客户端发送其数字证书,该证书包含服务器的公钥,由受信任的证书颁发机构(CA)签名。客户端使用内置的CA证书来验证服务器证书的有效性和真实性。
2.4 密钥交换
基于选定的加密算法,客户端和服务器进行密钥交换。常见的密钥交换方法包括:
- RSA:客户端使用服务器的公钥加密一个随机生成的对称密钥,并将其发送给服务器。服务器使用其私钥解密这个对称密钥。
- DH(Diffie-Hellman)/ECDH(Elliptic Curve Diffie-Hellman):客户端和服务器通过交换各自的公钥,协商出一个共享的对称密钥。
2.5 生成会话密钥
客户端和服务器各自使用握手过程中生成的随机数和协商的对称密钥,生成一个会话密钥。这个会话密钥将用于对后续的通信数据进行加密。
2.6 握手结束
客户端和服务器各自发送一个Finished消息,使用会话密钥加密,以确认握手过程成功完成。
3. 数据加密传输
握手过程结束后,客户端和服务器使用生成的会话密钥对通信数据进行对称加密(如AES)。对称加密具有高效且快速的优点,适合大数据量的传输。
4. 数据完整性验证
除了加密,SSL/TLS还提供数据完整性验证机制,确保数据在传输过程中未被篡改。常用的方法是使用HMAC(Hash-based Message Authentication Code),它结合了密钥和哈希函数。
5. 关闭连接
当通信结束时,客户端和服务器可以通过SSL/TLS关闭消息来安全地终止连接。
3、TCP和UDP的区别?
1. 连接方式
TCP
- 面向连接:TCP是面向连接的协议。在传输数据之前,TCP需要建立一个连接。这是通过三次握手过程实现的,以确保通信双方准备好传输数据。
- 三次握手:
- 客户端向服务器发送SYN(同步)包,要求建立连接。
- 服务器收到SYN包后,回应一个SYN-ACK(同步-确认)包。
- 客户端收到SYN-ACK包后,再发送一个ACK(确认)包,连接建立成功。
UDP
- 无连接:UDP是无连接的协议。数据在发送之前不需要建立连接。客户端和服务器之间不进行握手过程,数据包可以直接发送。
- 传输方式:由于无连接特性,UDP传输更快,但不保证数据包的顺序和可靠性。
2. 数据传输
TCP
- 可靠传输:TCP保证数据的可靠传输。通过确认机制(ACK)和重传机制,TCP确保数据包按顺序到达,并且没有丢失。
- 流量控制:TCP使用流量控制(Flow Control)机制,调整数据传输速率,防止接收端因处理不过来而丢包。
- 拥塞控制:TCP使用拥塞控制(Congestion Control)机制,防止网络过载,保证数据传输的稳定性。
UDP
- 不可靠传输:UDP不保证数据包的可靠性。数据包可能丢失、重复或乱序,发送后不等待确认。
- 无流量控制和拥塞控制:UDP没有流量控制和拥塞控制机制。数据包以最快的速度发送,网络质量不佳时可能导致数据包丢失。
3. 头部开销
TCP
- 头部较大:TCP头部通常有20字节,包含许多控制信息(如序列号、确认号、窗口大小等),以支持可靠传输和流控制。
- 开销高:由于复杂的控制机制和头部信息,TCP开销较高。
UDP
- 头部较小:UDP头部只有8字节,包含基本的源端口、目标端口、长度和校验和等信息。
- 开销低:由于无连接和无控制机制,UDP开销较低,适合需要快速传输的小数据量应用。
4. 适用场景
TCP
- 适用于可靠性要求高的应用:如网页浏览(HTTP/HTTPS)、文件传输(FTP)、电子邮件(SMTP/IMAP)、远程登录(SSH)。
- 典型应用:
- HTTP/HTTPS:网页浏览,要求数据完整性和可靠性。
- FTP:文件传输,数据不能丢失。
- SMTP:邮件传输,需要确认和重传机制。
UDP
- 适用于实时性要求高、对可靠性要求不高的应用:如视频流媒体(IPTV)、在线游戏、语音通话(VoIP)、DNS查询。
- 典型应用:
- IPTV:视频流媒体,允许少量数据丢失而不影响观看体验。
- VoIP:语音通话,实时性高,偶尔丢包不明显影响通话质量。
- DNS:域名解析请求,单个请求数据量小且丢失后可重试。
4、TCP可靠传输,最大序列号确认?
TCP可靠传输机制
1. 序列号和确认号
- 序列号(Sequence Number):TCP将数据流分成若干数据段,每个数据段都有一个唯一的序列号。序列号标识数据段的第一个字节的位置。
- 确认号(Acknowledgment Number):接收端使用确认号告诉发送端下一个期望收到的数据字节的位置。即确认号表示接收端已成功收到的数据字节的下一个字节位置。
2. 三次握手
TCP在建立连接时通过三次握手过程来初始化序列号和确认号:
- 客户端发送一个带有初始序列号(ISN,Initial Sequence Number)的SYN包。
- 服务器响应一个带有服务器初始序列号的SYN-ACK包,并确认客户端的ISN。
- 客户端发送ACK包,确认服务器的ISN。
3. 数据传输和确认
在数据传输过程中,发送端和接收端通过序列号和确认号来协调数据的发送和接收:
- 发送数据段:发送端发送一个数据段,包含当前的序列号。
- 接收确认:接收端收到数据段后,发送一个确认包(ACK),包含确认号。
- 重传机制:如果发送端在一定时间内未收到确认包,则重传未确认的数据段。
4. 滑动窗口
TCP使用滑动窗口机制进行流控制,确保发送端不会发送超过接收端处理能力的数据量。窗口大小由接收端在ACK包中告知发送端。
最大序列号确认
最大序列号确认是指TCP使用的序列号范围限制,以确保序列号能够循环使用。TCP序列号是一个32位的无符号整数,因此最大序列号为2^32 - 1(即4294967295)。超过最大序列号后,序列号从0重新开始。
序列号范围
- 初始序列号:在连接建立时,双方会选择各自的初始序列号。
- 序列号增长:每发送一个字节的数据,序列号增加1。
- 序列号回绕:当序列号达到最大值后,下一个序列号从0开始。
示例
假设客户端发送的数据量很大,序列号可能会超过最大值。下面是一个简单的示例:
- 初始序列号为
4294967290。 - 发送8个字节的数据段:
- 数据段1:序列号为
4294967290到4294967297。 - 数据段2:序列号为
4294967298到4294967299,并且序列号回绕,从0重新开始。
- 数据段1:序列号为
TCP处理序列号回绕
为了正确处理序列号回绕,TCP协议中有以下机制:
- 窗口大小:滑动窗口机制确保接收端能够正确处理序列号回绕。窗口大小不超过
2^31,即一半的序列号空间,确保发送端和接收端能够正确区分新的数据段和旧的数据段。 - 超时重传:如果发送端未在指定时间内收到确认包,会重传未确认的数据段,确保数据传输的可靠性。
5、慢SQL,索引数据结构,回表?
慢SQL查询
慢SQL查询是指执行时间较长的SQL查询。这些查询可能导致数据库性能下降,影响用户体验。排查慢SQL通常包括以下步骤:
- 识别慢SQL:
- 使用数据库自带的慢查询日志,例如MySQL的慢查询日志。
- 使用性能监控工具(如PMM、New Relic)检测慢SQL。
- 分析慢SQL:
- 使用
EXPLAIN命令查看查询执行计划。 - 分析查询的扫描范围、索引使用情况、排序和合并操作等。
- 使用
- 优化慢SQL:
- 添加或调整索引。
- 重写查询语句,避免全表扫描。
- 拆分复杂查询,减少子查询的嵌套。
索引的数据结构
B+树数据结构
B+树是一种自平衡的树结构,是B-树的改进版本。它在数据库和文件系统中广泛应用,特别适合用于索引。B+树具有以下特点:
- 所有叶子节点都在同一层:这使得树的高度最小化,从而提高查询效率。
- 叶子节点包含所有数据:所有的实际数据都存储在叶子节点中,而非叶子节点只存储键值和指向子节点的指针。
- 叶子节点之间通过链表相连:这使得范围查询更高效,因为只需遍历叶子节点的链表。
B+树与B-树的区别
- 数据存储位置:
- B-树:数据可以存储在所有节点(内节点和叶子节点)。
- B+树:数据只存储在叶子节点,内节点只存储键值和指针。
- 叶子节点链表:
- B-树:叶子节点之间没有链表结构。
- B+树:叶子节点之间通过链表相连,方便范围查询。
- 查询效率:
- B-树:查询可能在任何节点结束,效率稍低。
- B+树:查询一定在叶子节点结束,且由于叶子节点链表的存在,范围查询更高效。
回表现象
回表(也称为回行)是指在使用索引查询数据时,查询过程需要从索引表回到数据表读取完整记录的现象。这通常发生在使用非聚簇索引(Secondary Index)时。
聚簇索引和非聚簇索引
- 聚簇索引(Clustered Index):聚簇索引将数据行实际存储在索引中,索引的叶节点即为数据行。每个表只能有一个聚簇索引。MySQL的InnoDB存储引擎默认使用主键作为聚簇索引。
- 非聚簇索引(Non-Clustered Index):非聚簇索引的叶节点包含指向实际数据行的指针。查询时需要先通过索引找到指针,再回表读取完整数据。
回表示例:
假设有一个表t_users,包含以下列:
CREATE TABLE t_users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
email VARCHAR(50)
);为name列创建一个非聚簇索引:
CREATE INDEX idx_name ON t_users (name);执行以下查询:
SELECT id, age FROM t_users WHERE name = 'John';查询过程:
- 通过
idx_name索引找到所有name为John的记录位置。 - 根据索引中的指针回表读取完整数据行,获取
id和age。
优化回表
减少回表次数可以提高查询性能。常见的方法包括:
- 覆盖索引:在索引中包含所有查询所需的列,避免回表。例如,创建包含
id和age列的联合索引:
CREATE INDEX idx_name_id_age ON t_users (name, id, age);6、where、group by、order by、limit执行顺序?
- WHERE:
WHERE子句用于筛选出符合条件的行。它通常是 SQL 查询的第一个子句,并在查询执行之初就进行过滤。 - GROUP BY:
GROUP BY子句将查询结果按指定列进行分组。在分组完成后,聚合函数(如SUM()、COUNT()等)会对每个组进行计算。 - HAVING:
HAVING子句用于筛选出分组后的结果集中符合条件的组。它类似于WHERE子句,但是HAVING是在GROUP BY后对分组结果进行过滤。 - ORDER BY:
ORDER BY子句用于对查询结果进行排序。它在前面子句执行完毕后,在结果集上进行排序。 - LIMIT:
LIMIT子句用于限制查询结果返回的行数。它是在所有前面的子句执行完毕后,最后被执行的。
7、Redis常用的数据类型?
- 字符串(String):最基本的数据类型,可以是文本、整数或者浮点数。常用命令包括
SET、GET、INCR、DECR等。 - 列表(List):由多个字符串组成的有序集合,支持从两端插入和删除元素。常用命令包括
LPUSH、RPUSH、LPOP、RPOP等。 - 集合(Set):不重复且无序的字符串集合。常用命令包括
SADD、SREM、SMEMBERS、SISMEMBER等。 - 有序集合(Sorted Set):类似于集合,但每个成员都关联一个分数(score),可以按照分数排序。常用命令包括
ZADD、ZREM、ZRANGE、ZSCORE等。 - 哈希表(Hash):包含键值对的无序散列表。常用命令包括
HSET、HGET、HDEL、HGETALL等。 - 位图(Bitmap):由二进制位组成的数据结构,支持对位进行操作。常用命令包括
SETBIT、GETBIT、BITCOUNT、BITOP等。 - HyperLogLog:用于基数(cardinality)估算的数据结构,可以接受大量重复数据,在空间上比普通集合更加节省。常用命令包括
PFADD、PFCOUNT等。 - 地理空间(Geospatial):存储地理空间信息的数据结构,可以存储地理位置的经度和纬度,并支持计算两个位置之间的距离。常用命令包括
GEOADD、GEODIST、GEORADIUS、GEORADIUSBYMEMBER等。
8、Hash常见的扩容方法?
- 重新分配空间:最简单的方法是重新分配更大的空间,并将原有的数据重新哈希到新的空间中。这种方法比较消耗性能,因为需要重新计算哈希,并将数据从旧空间拷贝到新空间。
- 渐进式扩容:为了减少重新分配空间时的性能损耗,可以采用渐进式扩容的方法。具体做法是先申请一块稍大一点的空间,然后在新空间中继续插入新的元素,同时定期地将旧空间中的数据逐渐迁移到新空间中。这种方法可以减少一次性的性能损耗,但会增加一定的复杂度。
- 动态调整大小:在 Hash 的设计中,可以预留一些额外的空间,当元素数量接近预留空间时,就触发扩容操作。这种方法可以在一定程度上减少扩容的次数,但需要合理设置预留空间的大小。
- 链表转化为平衡树:当 Hash 中某个 field 对应的 value 是一个很长的链表时,可以将这个链表转化为平衡树(如跳表),以减少查找的时间复杂度。
- 增量式 rehash:Redis 在进行 rehash 时,并不是一次性将所有的 key-value 对重新分配到新的 Hash table 中,而是采用增量式的方式,每次只迁移一小部分数据,通过
rehash配置选项可以调整每次 rehash 的数量。
9、ZSet底层数据结构?
ZSet 的底层数据结构是跳跃表(Skip List)和哈希表(Hash Table)的混合体。
- 跳跃表:跳跃表是一种有序链表的数据结构,它通过在不同层级上建立索引来加速查找操作。每个节点包含一个指向下一个节点的指针,以及一个指向同一层级下一个节点的指针(即“跳跃”的含义)。跳跃表中的每个节点都包含一个成员和对应的分数。在 ZSet 中,跳跃表用于按照成员的分数来排序和快速查找成员。
- 哈希表:除了跳跃表之外,ZSet 还使用哈希表来存储成员到分数的映射关系。哈希表的查询速度非常快,用于快速定位成员的分数。
通过将跳跃表和哈希表结合使用,Redis 的 ZSet 实现了按照分数排序的有序集合,并且在插入、删除、查找等操作上保持了较高的性能。跳跃表提供了快速的有序遍历能力,而哈希表则提供了快速查找成员的能力,两者结合使得 ZSet 成为一个非常高效的数据结构。
10、算法:三数之和
思路
- 首先对数组进行排序,然后遍历数组,将当前元素作为第一个数(nums[i])。
- 在剩余的数组中使用双指针,一个指针从当前元素的下一个位置开始向右移动(指向第二个数,nums[left]),另一个指针从数组末尾向左移动(指向第三个数,nums[right])。
- 如果 nums[i] + nums[left] + nums[right] == 0,则找到一个满足条件的三元组,将其加入结果集中。同时需要注意去重,跳过重复元素。
- 如果和大于 0,则将右指针左移;如果和小于 0,则将左指针右移。直到左指针大于等于右指针时结束。
参考代码
C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result; // 存储结果的数组
int n = nums.size();
if (n < 3) return result; // 如果数组长度小于3,直接返回空数组
sort(nums.begin(), nums.end()); // 对数组进行排序
for (int i = 0; i < n - 2; ++i) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重,避免重复计算
int target = -nums[i]; // 将当前元素作为目标值
int left = i + 1, right = n - 1; // 双指针分别指向当前元素的下一个位置和数组末尾
while (left < right) {
int sum = nums[left] + nums[right]; // 计算当前两个数的和
if (sum == target) {
result.push_back({nums[i], nums[left], nums[right]}); // 找到一个符合条件的三元组,加入结果集
// 去重,避免重复计算
while (left < right && nums[left] == nums[left + 1]) ++left;
while (left < right && nums[right] == nums[right - 1]) --right;
++left;
--right;
} else if (sum < target) {
++left; // 和小于目标值,左指针右移
} else {
--right; // 和大于目标值,右指针左移
}
}
}
return result;
}
};
int main() {
vector<int> nums = {-1, 0, 1, 2, -1, -4};
Solution solution;
vector<vector<int>> result = solution.threeSum(nums);
// 输出结果
for (auto& triplet : result) {
for (int num : triplet) {
cout << num << " ";
}
cout << endl;
}
return 0;
}