腾讯微信一面凉经,上来就是两道算法题。。。
公众号:阿Q技术站
来源:https://www.nowcoder.com/feed/main/detail/6720cdde74d04f69802ac776088132e0
1、手撕两道算法题
1.1、旋转字符串
思路
- 字符串长度检查:首先检查两个字符串的长度,如果长度不一致,则
s
不可能通过旋转变成goal
,直接返回false
。 - 模拟旋转操作:循环
s.length()
次,每次将字符串s
的最左边字符移动到最右边,然后检查旋转后的字符串是否等于goal
。 - 返回结果:如果在某次旋转中,字符串
s
等于goal
,返回true
;如果循环结束后仍未找到匹配,则返回false
。
参考代码
C++
#include <iostream>
#include <string>
// 判断字符串 s 经过若干次旋转后能否变成字符串 goal
bool canBeRotatedToGoal(const std::string& s, const std::string& goal) {
// 如果 s 和 goal 长度不一致,直接返回 false
if (s.length() != goal.length()) {
return false;
}
// 模拟旋转操作
std::string rotated_s = s;
for (size_t i = 0; i < s.length(); ++i) {
// 将最左边的字符移动到最右边
rotated_s = rotated_s.substr(1) + rotated_s[0];
// 检查旋转后的字符串是否等于 goal
if (rotated_s == goal) {
return true;
}
}
// 如果没有找到匹配,返回 false
return false;
}
int main() {
std::string s, goal;
// 输入字符串 s 和 goal
std::cout << "请输入字符串 s: ";
std::cin >> s;
std::cout << "请输入字符串 goal: ";
std::cin >> goal;
// 调用函数判断是否可以通过旋转 s 变成 goal
if (canBeRotatedToGoal(s, goal)) {
std::cout << "true" << std::endl;
} else {
std::cout << "false" << std::endl;
}
return 0;
}
1.2、删除链表中重复元素
思路
- 特殊情况处理:首先检查链表是否为空或只有一个节点。如果是,直接返回链表头节点,因为没有重复元素。
- 遍历链表:使用一个指针遍历链表,从头节点开始。
- 检查重复元素:如果当前节点的值与下一个节点的值相同,则跳过下一个节点,即将当前节点的
next
指针指向下一个节点的next
节点,删除下一个节点。 - 继续遍历:如果当前节点的值与下一个节点的值不同,则将指针移动到下一个节点。
- 返回结果:遍历结束后,返回处理后的链表头节点。
参考代码
C++
#include <iostream>
// 定义链表节点结构
struct ListNode {
int val; // 节点的值
ListNode* next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(nullptr) {}
};
// 删除排序链表中的重复元素
ListNode* deleteDuplicates(ListNode* head) {
// 特殊情况处理:链表为空或只有一个节点
if (head == nullptr || head->next == nullptr) {
return head;
}
// 指针初始化,指向链表头节点
ListNode* current = head;
// 遍历链表
while (current != nullptr && current->next != nullptr) {
// 如果当前节点的值与下一个节点的值相同
if (current->val == current->next->val) {
// 跳过下一个节点
ListNode* temp = current->next;
current->next = current->next->next;
delete temp; // 释放内存
} else {
// 移动指针到下一个节点
current = current->next;
}
}
// 返回处理后的链表头节点
return head;
}
// 打印链表
void printList(ListNode* head) {
ListNode* current = head;
while (current != nullptr) {
std::cout << current->val << " ";
current = current->next;
}
std::cout << std::endl;
}
// 创建链表用于测试
ListNode* createList(const std::initializer_list<int>& values) {
ListNode* head = nullptr;
ListNode* tail = nullptr;
for (int value : values) {
ListNode* newNode = new ListNode(value);
if (head == nullptr) {
head = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
}
return head;
}
int main() {
// 输入链表的元素
ListNode* head = createList({1, 1, 2, 3, 3});
std::cout << "原始链表: ";
printList(head);
// 调用函数删除重复元素
head = deleteDuplicates(head);
std::cout << "删除重复元素后的链表: ";
printList(head);
return 0;
}
2、Linux常用命令有哪些?
3、awk命令有了解过嘛
4、平时怎么查看日志?
1. 使用命令行查看日志
在Linux和Unix系统中,日志文件通常保存在/var/log
目录下。常用的日志文件包括:
/var/log/syslog
:系统日志/var/log/auth.log
:认证日志/var/log/kern.log
:内核日志/var/log/apache2
:Apache日志/var/log/nginx
:Nginx日志
常用的命令行工具包括:
a. cat
cat
命令可以显示文件的内容:
cat /var/log/syslog
b. less
less
命令可以分页查看文件内容,适合查看较长的日志文件:
less /var/log/syslog
使用less
时可以使用以下快捷键:
Space
:向下翻页b
:向上翻页q
:退出
c. tail
tail
命令用于查看文件的最后几行:
tail /var/log/syslog
tail
的常用选项包括:
-n
:指定显示的行数-f
:实时跟踪日志文件的更新
tail -n 100 /var/log/syslog
tail -f /var/log/syslog
d. grep
grep
命令用于搜索日志文件中的特定关键字:
grep "error" /var/log/syslog
可以结合tail -f
和grep
实时监控日志中特定关键字的出现:
tail -f /var/log/syslog | grep "error"
2. 使用日志管理工具
a. Logrotate
logrotate
是一个日志轮转工具,用于管理日志文件的大小和归档。配置文件通常在/etc/logrotate.conf
和/etc/logrotate.d/
目录下。
b. Journalctl
journalctl
用于查看systemd
管理的日志:
journalctl
常用选项包括:
-u
:查看特定服务的日志-f
:实时查看日志--since
和--until
:指定时间范围
journalctl -u nginx
journalctl -f
journalctl --since "2023-05-01" --until "2023-05-02"
5、有没有用过命令查看程序运行的栈信息
1. 使用GDB查看栈信息
GDB(GNU调试器)是一个强大的调试工具,可以在程序运行时或程序崩溃后查看栈信息。
a. 调试正在运行的程序
-
启动程序:
./your_program
-
查找程序的PID(进程ID):
pidof your_program
-
附加GDB到运行中的进程:
gdb -p <pid>
-
在GDB中查看栈信息:
(gdb) bt
bt
(backtrace)命令用于显示当前线程的栈帧信息。
b. 调试崩溃的程序
-
确保程序编译时包含调试信息:
g++ -g -o your_program your_program.cpp
-
运行程序并生成core dump(内存转储文件):
ulimit -c unlimited ./your_program
-
当程序崩溃时,会生成一个core dump文件(如
core
或core.<pid>
)。 -
使用GDB加载core dump文件和程序执行文件:
gdb your_program core
-
在GDB中查看栈信息:
(gdb) bt
2. 使用pstack
查看栈信息
pstack
是一个简单的工具,可以快速查看正在运行的程序的栈信息。
-
安装
pstack
:sudo apt-get install pstack # 在Debian/Ubuntu系统上 sudo yum install pstack # 在RHEL/CentOS系统上
-
查找程序的PID:
pidof your_program
-
使用
pstack
查看栈信息:pstack <pid>
3. 使用gstack
查看栈信息
gstack
是GDB的一个简单前端,用于快速获取正在运行的进程的栈信息。
-
安装
gstack
:sudo apt-get install gdb # 在Debian/Ubuntu系统上,gstack通常与gdb一起安装 sudo yum install gdb # 在RHEL/CentOS系统上
-
查找程序的PID:
pidof your_program
-
使用
gstack
查看栈信息:gstack <pid>
4. 使用strace
查看系统调用栈
strace
可以跟踪程序的系统调用,虽然不直接提供函数调用栈,但在某些调试场景下非常有用。
-
安装
strace
:sudo apt-get install strace # 在Debian/Ubuntu系统上 sudo yum install strace # 在RHEL/CentOS系统上
-
使用
strace
启动程序:strace -f -o strace_output.txt ./your_program
这会将程序运行期间的所有系统调用记录到
strace_output.txt
文件中。 -
查看
strace_output.txt
文件中的系统调用信息。
6、netstat命令怎么使用?用来干啥?
netstat
命令是一个网络工具,用于显示网络连接、路由表、接口统计信息、伪装连接(masquerade connections),以及多播成员(multicast memberships)。它是诊断网络问题、监控网络流量和查看网络统计信息的强大工具。
基本用法
netstat [选项]
常见选项
-a
:显示所有连接和监听端口。-t
:仅显示TCP连接。-u
:仅显示UDP连接。-n
:以数字形式显示地址和端口号(不进行DNS解析)。-l
:仅显示监听的套接字。-p
:显示每个连接的进程ID(PID)和进程名称。-r
:显示路由表。-i
:显示网络接口信息。-s
:显示网络统计信息。-c
:每隔一段时间重复显示统计信息。
具体示例
1. 显示所有连接
显示所有的网络连接(包括监听和非监听的):
netstat -a
2. 显示TCP连接
仅显示TCP连接:
netstat -t
3. 显示UDP连接
仅显示UDP连接:
netstat -u
4. 以数字形式显示地址和端口号
不进行DNS解析,以数字形式显示地址和端口号:
netstat -n
5. 显示监听套接字
仅显示监听的网络端口:
netstat -l
6. 显示进程信息
显示每个连接的进程ID(PID)和进程名称:
sudo netstat -p
7. 显示路由表
显示当前系统的路由表:
netstat -r
8. 显示网络接口信息
显示网络接口的信息(类似于ifconfig
或ip addr
):
netstat -i
9. 显示网络统计信息
显示各种协议的统计信息:
netstat -s
10. 持续刷新显示
每隔一段时间重复显示统计信息(默认每秒刷新一次,可以指定刷新间隔):
netstat -c
7、Linux网络抓包用什么命令来实现?
1. 使用tcpdump
进行抓包
tcpdump
是一个命令行工具,用于捕获网络数据包并显示网络接口上的流量。它非常灵活,可以根据不同的条件过滤和捕获数据包。
安装tcpdump
在大多数Linux发行版中,可以通过包管理器安装tcpdump
:
sudo apt-get install tcpdump # 在Debian/Ubuntu系统上
sudo yum install tcpdump # 在RHEL/CentOS系统上
基本用法
tcpdump [选项] [表达式]
-i
:指定网络接口。-w
:将捕获的数据包保存到文件。-r
:从文件读取捕获的数据包。-n
:不进行主机名解析。-v
:详细输出。-vv
:更详细的输出。-c
:指定捕获的数据包数量。-s
:指定捕获数据包的长度。
常见示例
- 抓取所有流量
sudo tcpdump
- 指定网络接口抓包
sudo tcpdump -i eth0
- 抓取特定数量的数据包
sudo tcpdump -c 10
- 抓取并保存到文件
sudo tcpdump -i eth0 -w capture.pcap
- 从文件读取并解析
sudo tcpdump -r capture.pcap
- 抓取特定端口的流量
sudo tcpdump port 80
- 抓取特定主机的流量
sudo tcpdump host 192.168.1.1
- 抓取特定协议的流量
sudo tcpdump tcp
sudo tcpdump udp
sudo tcpdump icmp
- 抓取数据包并详细显示
sudo tcpdump -v
2. 使用Wireshark进行抓包
Wireshark
是一个图形化的网络分析工具,功能非常强大,可以实时捕获和分析网络数据包。Wireshark
也提供命令行工具tshark
,功能与tcpdump
类似。
安装Wireshark
在大多数Linux发行版中,可以通过包管理器安装Wireshark:
sudo apt-get install wireshark # 在Debian/Ubuntu系统上
sudo yum install wireshark # 在RHEL/CentOS系统上
使用Wireshark
- 启动Wireshark:
在命令行输入wireshark
,启动图形化界面。
wireshark
- 选择网络接口进行捕获:
在Wireshark界面中,选择要捕获流量的网络接口,然后点击“Start”。
- 使用过滤器:
Wireshark提供了强大的过滤功能,可以在捕获过程中或捕获后进行过滤。常用的过滤表达式包括:
ip.addr == 192.168.1.1
:过滤特定IP地址的流量。tcp.port == 80
:过滤特定端口的TCP流量。http
:过滤HTTP流量。icmp
:过滤ICMP流量。
- 保存捕获数据:
捕获的数据可以保存为pcap
文件,以便以后分析。点击“File” -> “Save As”。
使用tshark
tshark
是Wireshark的命令行版本,功能与tcpdump
类似。
- 抓取所有流量:
sudo tshark
- 抓取并保存到文件:
sudo tshark -i eth0 -w capture.pcap
- 读取并解析文件:
sudo tshark -r capture.pcap
8、top命令用过嘛?如何查看僵死进程?
top
命令是一个实时显示系统中任务信息的命令行工具,用于监控系统性能,包括CPU、内存使用情况,以及各个进程的状态。
基本用法
在终端输入top
命令即可启动该工具:
top
top
命令界面介绍
启动top
命令后,你会看到一个动态更新的界面,显示系统的实时信息。界面主要分为以下几部分:
- 系统概要信息:
- 显示当前时间、系统运行时间、用户数、负载均值等。
- CPU使用情况:包括用户态、系统态、空闲、等待I/O等。
- 内存和交换空间使用情况。
- 进程信息:
- PID:进程ID。
- USER:进程所有者。
- PR:进程优先级。
- NI:进程的nice值。
- VIRT:进程使用的虚拟内存总量。
- RES:进程使用的物理内存量。
- SHR:进程使用的共享内存量。
- S:进程状态(S表示睡眠,R表示运行,Z表示僵死)。
- %CPU:进程占用的CPU百分比。
- %MEM:进程占用的内存百分比。
- TIME+:进程使用的CPU时间总量。
- COMMAND:运行的命令。
查看僵死进程
僵死进程(Zombie Process)是指已经终止但其父进程尚未调用wait()
系统调用获取其状态信息的进程。这种进程仍然占用进程表中的一个条目。
在top
命令中,僵死进程的状态标识为Z
。查看僵死进程的方法如下:
-
启动
top
命令:top
-
查看进程状态列(
S
列)中标识为Z
的进程:在
top
界面中,找到S
列,查看其中状态为Z
的进程。这些即为僵死进程。 -
如果有大量进程,使用筛选功能:
在
top
界面中按下o
键,然后输入以下筛选条件:S=Z
按下回车键,
top
将只显示僵死进程。
处理僵死进程
处理僵死进程通常有以下几种方法:
-
查找并重启父进程:
如果父进程可以重启,重启父进程可能会清理僵死进程。
-
终止父进程:
如果父进程无法重启或不再需要,可以通过终止父进程来清理僵死进程。找到父进程的PID,然后使用
kill
命令终止父进程:sudo kill -9 <parent_pid>
这将强制终止父进程,并且其所有子进程(包括僵死进程)将被
init
进程接管,init
进程会调用wait()
清理僵死进程。
top
命令的其他常用操作
在top
命令界面中,还可以使用其他键来执行一些操作:
h
:显示帮助。q
:退出top
。u
:按用户筛选进程。k
:终止进程。输入要终止的进程PID,然后输入信号编号(如9
表示SIGKILL
)。r
:调整进程优先级(nice值)。s
:更改刷新间隔时间(默认为3秒)。f
:添加或删除列。
9、进程,线程和协程有什么区别?
1. 进程(Process)
定义:
- 进程是操作系统分配资源和调度的基本单位。它是一个运行中的程序实例,包括程序代码、数据、进程控制块(PCB)、堆栈等。
特点:
- 独立性:进程之间独立运行,每个进程都有自己独立的地址空间。
- 资源占用:每个进程占用一定的系统资源,如内存、文件句柄等。
- 上下文切换开销大:进程切换需要保存和恢复大量的上下文信息,开销较大。
- 并发性:进程可以并发执行,不同进程之间可以通过进程间通信(IPC)进行数据交换。
2. 线程(Thread)
定义:
- 线程是进程中的一个执行单元,是调度和执行的基本单位。一个进程可以包含多个线程,线程共享进程的资源(如内存、文件句柄等)。
特点:
- 轻量级:线程比进程更轻量级,同一个进程内的多个线程共享进程的资源。
- 共享资源:线程共享进程的内存和资源,但每个线程有自己的堆栈和寄存器上下文。
- 上下文切换开销小:线程切换的开销比进程切换小,因为线程共享相同的地址空间。
- 并发性:线程可以并发执行,适用于需要高并发和高性能的应用。
3. 协程(Coroutine)
定义:
- 协程是一种用户态的轻量级线程,也称为微线程。协程由程序员在用户空间中调度,不依赖操作系统内核的线程调度机制。
特点:
- 轻量级:协程比线程更轻量级,创建和切换的开销非常小。
- 非抢占式:协程的切换由程序员控制,通常是协作式的,即一个协程主动让出控制权给另一个协程。
- 单线程执行:协程通常运行在单个线程中,适用于I/O密集型任务,可以避免多线程的同步问题。
- 无需锁机制:由于协程在单线程中运行,不会出现多线程竞争问题,因此无需使用锁机制来保护共享资源。
区别表
特性 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
---|---|---|---|
调度单位 | 操作系统 | 操作系统 | 用户程序 |
创建开销 | 大 | 中 | 小 |
上下文切换开销 | 大 | 小 | 极小 |
资源独立性 | 独立资源,拥有独立的地址空间 | 共享进程的资源 | 共享线程的资源 |
资源共享 | 不共享 | 共享进程的内存和文件句柄 | 共享线程的内存和文件句柄 |
并发性 | 并发执行,可以多进程并行 | 并发执行,可以多线程并行 | 单线程内并发执行 |
通信方式 | 进程间通信(IPC) | 线程间的共享内存和同步机制 | 协程间直接调用和传递 |
适用场景 | 独立运行的程序、进程间需要隔离的场景 | 高并发、高性能的计算和I/O密集型应用 | I/O密集型任务、需要大量并发的轻量级任务 |
10、进程和线程的区别在哪里?
11、进程之间的通信方式有哪些?
进程间通信(IPC,Inter-Process Communication)是指在不同进程之间传递数据的机制。由于进程拥有独立的地址空间,相互隔离,因此需要特殊的方式来进行数据交换。
1. 管道(Pipe)
定义:
- 管道是一种半双工的通信方式,数据只能单向流动,通常用于具有亲缘关系的进程间通信。
特点:
- 匿名管道:只能在父子进程间使用。
- 单向传输:数据只能从管道的一端流向另一端。
示例:
int fd[2];
pipe(fd);
if (fork() == 0) { // 子进程
close(fd[0]);
write(fd[1], "hello", 5);
close(fd[1]);
} else { // 父进程
close(fd[1]);
char buffer[5];
read(fd[0], buffer, 5);
close(fd[0]);
}
2. 命名管道(Named Pipe 或 FIFO)
定义:
- 命名管道是一种特殊的文件类型,允许无亲缘关系的进程间通信,支持半双工或全双工通信。
特点:
- 文件系统中的文件:存在于文件系统中,可以被无关的进程使用。
- 全双工:可以支持双向通信。
示例:
mkfifo /tmp/myfifo
3. 消息队列(Message Queue)
定义:
- 消息队列是存放在内核中的消息链表,进程可以通过消息队列发送和接收消息。
特点:
- 异步通信:进程可以非阻塞地发送和接收消息。
- 消息持久化:内核维护消息队列,可以在进程重启后继续通信。
示例:
int msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
msgsnd(msqid, &msg, sizeof(msg), 0);
msgrcv(msqid, &msg, sizeof(msg), 0, 0);
4. 共享内存(Shared Memory)
定义:
- 共享内存允许多个进程共享一段内存,可以实现最快的IPC,因为数据直接在内存中传输。
特点:
- 高效:不需要数据拷贝,直接在内存中读取和写入。
- 同步问题:需要使用同步机制(如信号量)来避免竞争条件。
示例:
int shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0666);
char *shmaddr = shmat(shmid, NULL, 0);
strcpy(shmaddr, "hello");
shmdt(shmaddr);
5. 信号(Signal)
定义:
- 信号是一种软件中断,用于通知进程某个事件发生。
特点:
- 异步通知:进程可以在任何时候接收信号。
- 简单轻量:适用于进程间简单的通知和控制。
示例:
kill(pid, SIGINT);
6. 套接字(Socket)
定义:
- 套接字是一种网络通信方式,可以用于同一台机器或不同机器上的进程间通信。
特点:
- 网络通信:支持跨网络的进程间通信。
- 灵活:支持多种协议(TCP、UDP等)。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
send(sockfd, "hello", 5, 0);
recv(sockfd, buffer, 5, 0);
7. 信号量(Semaphore)
定义:
- 信号量是一种用于进程间同步的机制,可以控制多个进程对共享资源的访问。
特点:
- 同步控制:用于解决共享资源的并发访问问题。
- 计数功能:支持计数,可以控制多个资源的并发访问。
示例:
sem_t sem;
sem_init(&sem, 1, 1);
sem_wait(&sem);
sem_post(&sem);
sem_destroy(&sem);
区别表
特性 | 管道(Pipe) | 命名管道(FIFO) | 消息队列(Message Queue) | 共享内存(Shared Memory) | 信号(Signal) | 套接字(Socket) | 信号量(Semaphore) |
---|---|---|---|---|---|---|---|
通信方向 | 半双工 | 半双工或全双工 | 单向或双向 | 双向 | 单向 | 双向 | 单向(同步控制) |
是否支持无亲缘关系 | 否 | 是 | 是 | 是 | 是 | 是 | 是 |
是否支持同步控制 | 否 | 否 | 是 | 是 | 否 | 是 | 是 |
通信速度 | 中 | 中 | 中 | 高 | 高 | 中 | 中 |
使用难度 | 低 | 低 | 中 | 高 | 低 | 中 | 中 |
12、前端发起请求之后到达后端,中间过程是什么?
1. DNS 解析
- 用户在浏览器中输入域名(例如
www.example.com
)并按下回车。 - 浏览器向本地 DNS 服务器发送 DNS 查询请求,查找域名对应的 IP 地址。
- 如果本地 DNS 服务器没有缓存该域名的 IP 地址,它会向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址。
- 本地 DNS 服务器继续向顶级域名服务器发送请求,顶级域名服务器返回次级域名服务器的地址。
- 本地 DNS 服务器向次级域名服务器发送请求,次级域名服务器返回域名对应的 IP 地址。
- 本地 DNS 服务器将 IP 地址返回给浏览器。
2. 建立 TCP 连接
- 浏览器根据域名解析出的 IP 地址,向服务器发起 TCP 连接请求(三次握手)。
- 服务器接受连接请求,建立 TCP 连接(三次握手完成)。
3. 发送 HTTP 请求
- 浏览器向服务器发送 HTTP 请求,请求包括请求方法(GET、POST等)、请求头(包括用户代理、Cookie等)、请求体(POST 请求的数据)等信息。
4. 服务器处理请求
- 服务器接收到请求后,根据请求的内容进行处理,可能包括访问数据库、调用后端逻辑处理程序等。
- 服务器处理完请求后,返回 HTTP 响应。
5. 返回 HTTP 响应
- 服务器将处理结果封装成 HTTP 响应,包括状态码、响应头(包括内容类型、内容长度等)、响应体(返回的数据)等。
- 服务器向浏览器发送 HTTP 响应。
6. 接收 HTTP 响应
- 浏览器接收到 HTTP 响应后,根据状态码和响应内容进行处理。
- 如果状态码为 200(OK),浏览器将响应体中的数据显示在页面上;如果是其他状态码,浏览器可能显示错误页面或者进行其他处理。
7. 关闭 TCP 连接
- 数据传输完成后,浏览器和服务器之间的 TCP 连接可以被关闭(四次挥手)。
13、UDP访问DNS的过程是怎么样的?
- 客户端发起DNS查询请求:
- 用户在浏览器中输入一个域名(例如
www.xxx.com
)。 - 浏览器将域名发送给操作系统的DNS解析器。
- 用户在浏览器中输入一个域名(例如
- DNS解析器检查缓存:
- DNS解析器首先检查本地缓存中是否有对应的IP地址。如果缓存中有,则直接返回该IP地址。
- 如果缓存中没有对应的记录,则DNS解析器向配置的DNS服务器发送查询请求。
- 构造DNS请求报文:
- DNS解析器构造一个DNS请求报文。该报文通常包含查询域名、查询类型(如A记录)以及一些标志。
- 发送DNS请求:
- DNS解析器使用UDP协议将请求报文发送到DNS服务器的53号端口。UDP是一种无连接协议,适用于简单且快速的请求响应场景。
- DNS服务器接收请求:
- DNS服务器接收到请求后,解析请求报文并查找对应的域名记录。
- 如果该DNS服务器本身没有缓存该域名的记录,它会向上级DNS服务器继续查询,直到找到该域名的最终解析记录。
- 构造DNS响应报文:
- DNS服务器找到解析记录后,构造一个DNS响应报文。该报文包含查询的域名、记录类型、TTL(生存时间)以及对应的IP地址。
- 发送DNS响应:
- DNS服务器使用UDP协议将响应报文发送回客户端(DNS解析器)。
- 客户端接收响应:
- DNS解析器接收到DNS响应报文后,从中提取IP地址并缓存一段时间(根据TTL值)。
- 然后将IP地址返回给请求的应用程序(例如浏览器)。
- 浏览器与目标服务器通信:
- 浏览器使用获取的IP地址与目标服务器建立TCP连接(如HTTP/HTTPS),进行后续的网页请求和数据传输。
14、为什么要三次握手?
TCP(传输控制协议)中的三次握手是为了确保双方建立可靠的连接。三次握手过程确保了客户端和服务器之间的通信通道是有效的,并且双方都准备好开始传输数据。以下是三次握手的详细步骤和原因:
步骤一:SYN
客户端发送SYN:
- 客户端向服务器发送一个SYN(同步)报文段,请求建立连接。
- SYN报文段包含一个初始序列号(Sequence Number,简称Seq),例如Seq = X。
步骤二:SYN-ACK
服务器发送SYN-ACK:
- 服务器接收到客户端的SYN报文段后,向客户端发送一个SYN-ACK(同步-确认)报文段。
- 这个报文段包含服务器的初始序列号(例如,Seq = Y),以及对客户端SYN的确认序列号(Acknowledgment Number,简称Ack),Ack = X + 1。
步骤三:ACK
客户端发送ACK:
- 客户端接收到服务器的SYN-ACK报文段后,向服务器发送一个ACK(确认)报文段。
- 这个报文段包含客户端自己的确认序列号,Ack = Y + 1。
- 至此,三次握手完成,连接建立。
三次握手的目的和原因
- 确认双方的接收能力:
- 第一次握手:客户端发送SYN,表示客户端希望建立连接,并向服务器告知自己的初始序列号。
- 第二次握手:服务器收到SYN后,返回SYN-ACK,表示服务器接收到客户端的请求,并愿意建立连接。同时,服务器也发送自己的初始序列号。
- 第三次握手:客户端收到SYN-ACK后,发送ACK,确认收到了服务器的初始序列号,并表示连接可以建立。
- 同步双方的初始序列号:
- 在三次握手过程中,双方交换初始序列号,用于后续数据传输中的序列控制和确认。
- 防止重复的连接初始化:
- 通过三次握手,可以避免因为网络延迟导致的重复连接初始化。假设没有三次握手,旧的连接请求可能在网络中滞留,并被错误地认为是新的连接请求,导致连接的混乱和资源的浪费。
- 确保双方都准备好通信:
- 三次握手确保了双方都收到了对方的连接请求,并且都同意建立连接。这样可以避免资源的浪费和潜在的通信问题。
15、三次握手与四次挥手的区别是什么?为什么要多一次?
连接建立(三次握手):
- 三次握手的目的是确保双方都知道对方的存在,并且双方都可以发送和接收数据。三次握手中,客户端和服务器各发送一次确认报文就可以完成连接的建立。
连接终止(四次挥手):
- 四次挥手的目的是确保双方都完成了所有数据的发送和接收,并且双方都可以安全地关闭连接。
- 多出的一次握手是因为TCP连接是全双工的,即双方都可以同时发送和接收数据。在连接终止时,每一方都需要单独发送一个FIN报文段来表示自己已经完成了数据的发送,并需要对方的确认(ACK)。
四次挥手多一次的原因:
- 全双工通信的特点:在TCP连接中,客户端和服务器都可以同时发送和接收数据。为了安全终止连接,每一方都需要单独发送FIN报文并等待对方的确认。
- 顺序关闭:一方(如客户端)发送FIN报文后,另一方(如服务器)可能还有数据要发送,因此需要等待数据发送完毕后再发送自己的FIN报文。这就需要一个额外的步骤来完成整个连接的安全关闭。
16、四次挥手的过程中如果处在timewait状态的请求较多,会有什么结果?要怎么解决这个问题?
TIME_WAIT状态的原因
在四次挥手过程中,当一方(通常是主动关闭连接的一方)发送了最后一个ACK报文段并进入TIME_WAIT状态后,它会保持这个状态一段时间(通常是两个最大段寿命时间,2*MSL,MSL一般为2分钟,因此TIME_WAIT状态持续4分钟)。这样做的目的是确保所有的TCP段都已经从网络中消失,避免旧连接的数据干扰新连接。
TIME_WAIT状态带来的问题
-
资源浪费:
- 每个处于TIME_WAIT状态的连接都占用系统的资源(如TCP端口、内存等)。
- 大量的TIME_WAIT连接可能导致系统资源耗尽,无法处理新的连接请求。
-
端口耗尽:
服务器上的可用端口数量是有限的。如果有大量连接处于TIME_WAIT状态,可用端口可能会被耗尽,导致新连接请求被拒绝。
-
新连接延迟:
当新的连接请求到达时,如果使用的端口仍处于TIME_WAIT状态,系统可能会延迟处理这些请求,等待TIME_WAIT状态结束。
解决方法
-
缩短TIME_WAIT持续时间:
-
调整系统参数以缩短TIME_WAIT状态的持续时间。例如,在Linux系统中,可以通过修改
tcp_fin_timeout
参数来缩短TIME_WAIT状态的持续时间:
sysctl -w net.ipv4.tcp_fin_timeout=30
-
这将TIME_WAIT状态的持续时间从默认的60秒缩短到30秒。
-
-
启用端口重用:
-
允许系统重用处于TIME_WAIT状态的端口,以便新连接可以使用这些端口。可以通过修改
tcp_tw_reuse
参数来实现:
sysctl -w net.ipv4.tcp_tw_reuse=1
-
这将允许在TIME_WAIT状态的端口被新连接重用。
-
-
启用快速重用:
-
允许快速回收处于TIME_WAIT状态的连接,以便新连接可以快速使用这些端口。可以通过修改
tcp_tw_recycle
参数来实现(注意,启用
tcp_tw_recycle
可能会引起一些问题,尤其是在NAT环境中,所以使用时需要谨慎):
sysctl -w net.ipv4.tcp_tw_recycle=1
-
-
增加可用端口范围:
-
扩大可用端口范围,以增加系统可以使用的端口数量,从而减少端口耗尽的可能性。可以通过修改
ip_local_port_range
参数来实现:
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
-
17、Redis的缓存击穿有了解过吗?
Redis缓存击穿是指当一个热点数据在缓存中失效(或未命中)后,大量的请求同时到达数据库,导致数据库瞬间负载剧增的情况。这种现象会对数据库产生很大的压力,甚至可能导致数据库崩溃。
18、如何解决缓存击穿问题?
缓存击穿的原因
缓存击穿通常发生在以下情况下:
- 缓存失效:
- 热点数据缓存的过期时间到期,而这个数据有大量请求访问。
- 高并发请求:
- 有大量并发请求同时访问同一条缓存失效的数据。
解决缓存击穿的方法
为了防止缓存击穿,可以采用以下几种策略:
- 设置热点数据永不过期:
- 对于特别热点的数据,可以设置它们的缓存永不过期,或在数据过期前主动刷新缓存。
- 优点:避免缓存失效带来的数据库压力。
- 缺点:需要人工干预,不能动态管理所有数据。
- 互斥锁:
- 当缓存失效且有大量请求同时到达时,可以通过互斥锁(如分布式锁)来控制只有一个请求能查询数据库并刷新缓存,其他请求等待。
- 实现方法:在查询缓存时,如果缓存未命中,使用分布式锁(如Redis的SETNX命令)锁住这个缓存键。当第一个请求获取到锁后,查询数据库并更新缓存,其他请求在锁释放后重新查询缓存。
- 提前更新缓存:
- 在缓存即将过期前,提前主动更新缓存。例如,在缓存过期前一段时间内(如1分钟),后台任务定时刷新缓存。
- 实现方法:通过后台任务或定时器在缓存过期前刷新缓存,确保缓存数据始终有效。
- 请求分流:
- 对于高并发请求,可以对请求进行分流,减少同时到达数据库的请求数量。
- 实现方法:通过负载均衡、降级策略等手段,将部分请求引导到备用缓存节点或降低请求频率。
19、Redis和MySQL的数据同步如何保证?
1. 缓存更新策略
1.1. 读写分离模式
- 读操作:
- 客户端请求数据时,首先查询Redis缓存。
- 如果缓存命中,则返回缓存中的数据。
- 如果缓存未命中,则查询MySQL数据库,并将查询结果写入Redis缓存,然后返回数据。
- 写操作:
- 更新数据时,首先更新MySQL数据库。
- 然后删除或更新Redis缓存中的相应数据。
伪代码示例:
def get_data(key):
value = redis.get(key)
if value is None:
value = db.query("SELECT * FROM table WHERE key = %s", key)
redis.set(key, value, ex=3600) # 设置缓存,并设定过期时间
return value
def update_data(key, value):
db.update("UPDATE table SET value = %s WHERE key = %s", value, key)
redis.delete(key) # 删除缓存中的数据
1.2. 写操作后更新缓存
- 在写操作后,不仅更新MySQL数据库,还要同步更新Redis缓存。
伪代码示例:
def update_data(key, value):
db.update("UPDATE table SET value = %s WHERE key = %s", value, key)
redis.set(key, value, ex=3600) # 更新缓存中的数据
2. 延迟双删策略
延迟双删策略可以有效地避免由于写操作引起的缓存不一致问题。
- 步骤:
- 更新数据库中的数据。
- 删除Redis缓存中的数据。
- 等待一段时间(例如500毫秒)。
- 再次删除Redis缓存中的数据。
伪代码示例:
def update_data(key, value):
db.update("UPDATE table SET value = %s WHERE key = %s", value, key)
redis.delete(key) # 第一次删除缓存
time.sleep(0.5) # 等待500毫秒
redis.delete(key) # 第二次删除缓存
3. 异步同步机制
- 使用消息队列(如Kafka、RabbitMQ)来异步同步数据变化。
- 步骤:
- 当数据在MySQL中发生变化时,发送消息到消息队列。
- 后台服务从消息队列中消费消息,并根据消息内容更新Redis缓存。
伪代码示例:
def update_data(key, value):
db.update("UPDATE table SET value = %s WHERE key = %s", value, key)
send_message_to_queue(key) # 发送更新消息到消息队列
# 后台服务
def handle_queue_message():
while True:
message = get_message_from_queue()
key = message['key']
value = db.query("SELECT * FROM table WHERE key = %s", key)
redis.set(key, value, ex=3600) # 更新缓存
4. 缓存预热
- 在系统启动或缓存失效时,预先加载热点数据到缓存中,避免缓存击穿。
- 步骤:
- 在系统启动时,读取热点数据并加载到Redis缓存中。
- 定期更新Redis缓存中的热点数据。
伪代码示例:
def preload_cache():
hot_keys = get_hot_keys() # 获取热点数据的key列表
for key in hot_keys:
value = db.query("SELECT * FROM table WHERE key = %s", key)
redis.set(key, value, ex=3600) # 预加载到缓存中
5. 双写一致性
- 在写操作时,同时更新MySQL数据库和Redis缓存,确保两者数据一致。
伪代码示例:
def update_data(key, value):
db.update("UPDATE table SET value = %s WHERE key = %s", value, key)
redis.set(key, value, ex=3600) # 同时更新缓存
20、Redis和MySQL的区别是?有什么关联?
Redis 和 MySQL 的区别
数据库类型
- Redis:
- 类型: 内存数据库(内存中键值存储)。
- 用途: 高速缓存、消息队列、实时数据分析。
- 数据模型: 基于键值对,支持多种数据结构(字符串、哈希、列表、集合、有序集合、位图、HyperLogLog、Geospatial)。
- MySQL:
- 类型: 关系型数据库(RDBMS)。
- 用途: 事务处理、持久化存储、结构化数据管理。
- 数据模型: 基于表格,支持复杂的SQL查询和事务。
性能
- Redis:
- 速度: 极快,数据存储在内存中,读写延迟通常在微秒级别。
- 并发: 支持高并发访问,适合高频读写场景。
- MySQL:
- 速度: 相对较慢,数据存储在磁盘上,读写延迟通常在毫秒级别。
- 并发: 并发性能较Redis稍低,但通过配置和优化可支持较高的并发。
持久化
- Redis:
- 持久化: 提供RDB(快照)和AOF(Append-Only File)两种持久化机制,但默认主要在内存中操作。
- 数据丢失风险: 在持久化策略不当或故障时,可能会有数据丢失的风险。
- MySQL:
- 持久化: 数据默认存储在磁盘上,通过日志和事务机制保证数据的持久性和一致性。
- 数据安全: 通过ACID(原子性、一致性、隔离性、持久性)事务模型,保证数据高度可靠和一致。
数据结构和操作
- Redis:
- 数据结构: 丰富的数据结构支持(如列表、集合、哈希等),适合多样化的应用场景。
- 操作: 提供丰富的命令集合,针对不同数据结构进行高效操作。
- MySQL:
- 数据结构: 基于表的结构,适合结构化数据的存储和管理。
- 操作: 支持复杂的SQL查询、索引、视图、存储过程等,适合复杂的业务逻辑处理。
Redis 和 MySQL 的关联
尽管Redis和MySQL是不同类型的数据库,它们可以结合使用,以发挥各自的优势:
- 缓存层和持久层:
- Redis作为缓存:Redis常用作MySQL的缓存层,存储频繁访问的数据,减少MySQL的查询压力,提高系统的响应速度。
- MySQL作为持久存储:MySQL用于持久化存储和复杂的事务处理,保证数据的完整性和一致性。
- 数据同步:
- 缓存更新策略:当MySQL中的数据发生变化时,及时更新或删除Redis中的缓存数据,以保证数据一致性。
- 延迟双删策略:在更新MySQL数据后,删除Redis缓存,并在短暂延迟后再次删除缓存,防止短时间内的数据不一致。
- 消息队列同步:
- 异步更新:通过消息队列(如Kafka、RabbitMQ),将数据更新消息传递给后台服务,后台服务根据消息更新Redis缓存,实现异步同步。
- 缓存预热:
- 启动时预热缓存:在系统启动或缓存失效时,预先将热点数据加载到Redis缓存中,避免缓存击穿带来的性能问题。
21、Redis的热查询有没有了解?怎么解决的?
Redis中的热查询(Hot Query)指的是某些特定的数据或键在短时间内被频繁访问的情况。这种情况可能会导致某些键的访问量过高,从而对Redis实例造成压力,甚至可能影响系统的整体性能。
1. 缓存预热
原理: 在系统启动或缓存失效时,预先加载热点数据到Redis缓存中,确保这些数据在第一次请求时已经在缓存中,避免缓存击穿。
实现方法:
- 启动时:在系统启动时,通过后台任务加载热点数据到缓存中。
- 定时刷新:定期刷新热点数据,确保缓存中的数据是最新的。
2. 缓存雪崩
原理: 缓存雪崩是指在某一时刻大量缓存失效,导致大量请求直接落到数据库上,从而引起数据库压力过大甚至崩溃。
解决方法:
- 缓存数据过期时间随机化:为不同的缓存设置不同的过期时间,避免同一时间大量缓存失效。
- 双缓存策略:在主缓存失效时,使用备用缓存进行查询,减少数据库的压力。
3. 分布式缓存
原理: 将缓存数据分布到多个Redis实例中,通过水平扩展缓解单实例的压力。
实现方法:
- 一致性哈希:使用一致性哈希算法,将键分布到多个Redis实例中,确保均匀分布。
- 分布式缓存工具:使用Redis Cluster或其他分布式缓存工具,如Codis、Twemproxy等,实现自动分片和负载均衡。
4. 限流和降级
原理: 对访问量过高的热点数据进行限流和降级处理,保护后端数据库和缓存系统。
方法:
- 限流:对每秒访问频率进行限制,超过限制的请求进行排队或直接拒绝。
- 降级:在缓存或数据库压力过大时,返回预先设定的默认值或友好提示。
5. 异步更新缓存
原理: 使用异步方式更新缓存,避免高并发请求同时访问数据库。
方法:
- 消息队列:使用消息队列(如Kafka、RabbitMQ)将更新请求异步处理。
- 后台任务:使用后台任务系统(如Celery)异步更新缓存。
6. 热点数据分片
原理: 对热点数据进行分片,将单一热点键拆分为多个子键,分散访问压力。
方法:
- 分片策略:根据业务需求对数据进行分片,例如按时间、地域等维度进行拆分。
- 组合键:使用组合键存储分片数据,减少单个键的访问压力。
22、MySQL的乐观锁和悲观锁是什么?
特性 | 乐观锁 | 悲观锁 |
---|---|---|
定义 | 假设并发冲突不会频繁发生,不直接加锁 | 假设并发冲突会频繁发生,直接加锁 |
原理 | 使用版本号或时间戳进行冲突检测 | 使用数据库锁机制(如行锁、表锁) |
使用场景 | 读多写少,冲突较少 | 写多读少,冲突较多 |
优点 | 提高并发性能,避免锁竞争 | 保证数据一致性,避免并发冲突 |
缺点 | 冲突时需要重试,处理复杂 | 加锁导致性能下降,特别是在高并发场景下 |
实现方法 | 通过版本号或时间戳字段进行更新检查 | 通过数据库的锁机制,如SELECT ... FOR UPDATE |
典型示例 | 版本号字段更新,条件检查是否一致 | 在事务中使用SELECT ... FOR UPDATE 加锁 |
适用场景 | 大部分是读操作,少量写操作 | 大部分是写操作,需要保证数据一致性 |
乐观锁(Optimistic Lock)
定义: 乐观锁假设并发冲突不会频繁发生,因此在数据操作时不直接加锁,而是在提交更新时检查是否有冲突。常见的实现方式是使用版本号(version)或时间戳(timestamp)。
原理: 每次读取数据时,获取该数据的版本号或时间戳。在更新数据时,检查数据库中的版本号或时间戳是否与读取时的一致。如果一致,则更新数据并修改版本号或时间戳;如果不一致,则说明数据已经被其他事务修改,操作失败,需要重新尝试。
使用场景: 适用于读多写少的场景,冲突较少时性能更优。
示例:
-- 创建表时添加版本号字段
CREATE TABLE items (
id INT PRIMARY KEY,
name VARCHAR(100),
quantity INT,
version INT
);
-- 更新数据时使用版本号进行乐观锁控制
UPDATE items
SET quantity = quantity - 1, version = version + 1
WHERE id = 1 AND version = 10;
-- 检查影响的行数,如果影响的行数为0,说明版本号不匹配,更新失败
SELECT ROW_COUNT();
悲观锁(Pessimistic Lock)
定义: 悲观锁假设并发冲突会频繁发生,因此在数据操作时直接加锁,阻止其他事务同时访问数据。常见的实现方式是使用数据库的锁机制,如行锁或表锁。
原理: 在读取或修改数据之前,先获取锁,其他事务必须等待锁释放后才能访问该数据。悲观锁确保了只有一个事务能够对数据进行操作,但会导致并发性能下降。
使用场景: 适用于写多读少的场景,冲突较多时更能保证数据一致性。
示例:
-- 开启事务
START TRANSACTION;
-- 对行数据进行锁定(SELECT ... FOR UPDATE 会对读取的行加排他锁)
SELECT quantity
FROM items
WHERE id = 1
FOR UPDATE;
-- 执行更新操作
UPDATE items
SET quantity = quantity - 1
WHERE id = 1;
-- 提交事务
COMMIT;
23、MySQL如何定位慢查询?
1. 使用慢查询日志(Slow Query Log)
配置慢查询日志:在MySQL配置文件中启用慢查询日志,并设置阈值,例如将执行时间超过1秒的查询记录到日志中。
示例配置:
slow_query_log = 1
slow_query_log_file = /path/to/slow_query.log
long_query_time = 1
查看慢查询日志:通过查看慢查询日志文件,可以定位执行时间较长的查询语句,并分析优化。
2. 使用Performance Schema
启用Performance Schema:在MySQL配置文件中启用Performance Schema,以收集有关查询性能的更多信息。
示例配置:
performance_schema = ON
查询慢查询语句:使用Performance Schema的查询语句来查找执行时间较长的查询。
SELECT * FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%your_query%';
3. 使用EXPLAIN分析查询计划
使用EXPLAIN:在执行查询语句前添加EXPLAIN关键字,可以查看查询的执行计划,了解MySQL如何执行查询。
示例:
EXPLAIN SELECT * FROM your_table WHERE your_condition;
24、MySQL定位了慢查询之后,要怎么优化慢查询?
1. 使用合适的索引
分析查询语句:通过EXPLAIN
关键字分析查询语句的执行计划,查看是否使用了索引,以及使用了哪些索引。
添加索引:根据查询条件和表的访问模式,添加适当的索引。避免过多索引,以免影响写入性能。
示例:
CREATE INDEX idx_name ON your_table (column_name);
2. 优化查询语句
避免使用通配符:尽量避免在WHERE
子句中使用通配符%
,可以考虑使用前缀索引。
避免全表扫描:尽量避免使用没有索引或无法使用索引的条件进行查询,以免导致全表扫描。
减少查询返回的列:只选择需要的列,避免使用SELECT *
。
3. 使用缓存
查询缓存:考虑使用MySQL的查询缓存功能,缓存经常查询的结果。
应用缓存:考虑在应用层使用缓存,减少对数据库的频繁查询。
4. 优化表结构和数据类型
合理设计表结构:避免使用过多的冗余字段,合理拆分大表。
选择合适的数据类型:选择合适的数据类型,避免使用过大或不必要的数据类型,以节省存储空间和提高查询效率。
5. 定期优化数据库
定期分析查询日志:定期分析慢查询日志,发现潜在的性能问题。
定期优化表:定期对表进行优化,包括ANALYZE TABLE
、OPTIMIZE TABLE
等操作。
25、MySQL的底层数据结构有没有了解?怎么实现的?
1. 表(Table)
数据存储方式:MySQL中的表数据通常存储在磁盘上,每个表对应一个或多个文件。表的结构定义存储在.frm
文件中,而实际数据存储在.ibd
或.MYD
文件中。
数据组织方式:
- 行存储(Row Storage):默认情况下,MySQL使用行存储方式,即每一行数据作为一个整体存储。这种方式适用于多数 OLTP 场景。
- 列存储(Column Storage):某些存储引擎(如InnoDB的压缩表)支持列存储,将同一列的数据存储在一起,提高了查询性能,适用于 OLAP 场景。
2. 索引(Index)
索引类型:MySQL支持多种类型的索引,包括B-Tree索引、哈希索引、全文索引等。
B-Tree索引:
- 数据结构:B-Tree索引是MySQL中最常用的索引类型。B-Tree(Balance Tree)是一种平衡树,通过在每个节点中存储多个键和子节点的指针来实现快速查找。
- 实现方式:在InnoDB存储引擎中,每个索引对应一棵B-Tree,主键索引和辅助索引都是B-Tree索引。
哈希索引:
- 数据结构:哈希索引将索引列的值通过哈希函数计算得到一个哈希值,然后将该哈希值映射到一个哈希表中的某个位置,通过这个位置来快速定位数据。
- 实现方式:MySQL并不直接支持哈希索引,但可以通过在Memory存储引擎上创建临时表来实现哈希索引。
3. 日志(Log)
事务日志(Redo Log):
- 数据结构:事务日志记录了事务对数据库的修改操作。InnoDB存储引擎使用两个重做日志文件(
ib_logfile0
和ib_logfile1
)来记录重做日志。 - 实现方式:InnoDB存储引擎将事务的修改操作记录到重做日志中,然后再将这些修改操作应用到实际的数据页上,以保证事务的持久性。
二进制日志(Binary Log):
- 数据结构:二进制日志记录了所有对数据库的修改操作,包括数据更新、插入、删除等。二进制日志是用于数据库备份、复制和恢复的重要组成部分。
- 实现方式:MySQL将二进制日志记录到二进制日志文件中,可以通过设置参数来配置二进制日志的大小和保存时长。
错误日志(Error Log):
- 数据结构:错误日志记录了MySQL运行过程中的错误信息,包括启动、运行和关闭过程中的错误信息。
- 实现方式:错误日志通常记录到文件中,可以通过设置参数来配置错误日志的保存位置和级别。