15.1 腾讯
Linux中敲一个指令系统反馈给我的结果很慢(比如说ls那个控制台出结果很慢),有没有什么排查思路?
-
使用
top/htop:查看CPU、内存、IO负载,是否有进程占用过高。 -
iotop:检查磁盘IO是否成为瓶颈(如大量读写)。 -
df -h和du:检查磁盘空间是否不足。 -
dmesg:查看内核日志是否有硬件/驱动错误。 -
网络问题(如
ls挂载了网络存储):用ping/netstat排查。
TOP命令排查占用率主要看哪些信息?如果一个进程占用率很高会影响我机器卡顿吗?(会跟CPU的核心数有关系吗)如果单核CPU一个进程占用率百分之百会影响linux系统卡顿吗,那多核CPU呢?
-
TOP主要看:
-
%CPU:进程CPU占用率。
-
%MEM:内存占用。
-
LOAD AVG:系统负载(1/5/15分钟平均值,超过CPU核心数可能有问题)。
-
-
高占用影响:
-
单核100%:会导致系统卡顿(其他进程无法使用CPU)。
-
多核:若只有一个进程占满1核(如8核机器占100%),其他核空闲,影响较小;但若多个进程占满所有核,则卡顿。
-
TOP看到系统的占用率是什么级别的?
-
TOP顶部的
%Cpu(s):-
us (user):用户态进程占用。
-
sy (system):内核态占用。
-
id (idle):空闲CPU比例(越低越忙)。
-
wa (IO-wait):等待IO的CPU时间(高说明磁盘慢)。
-
-
整体负载:看 Load Average(建议不超过CPU核心数)。
Linux内存不足怎么用命令查看?看的是实际内存还是虚拟内存?
-
命令:
-
free -h:查看 实际内存(used/free) 和 缓冲/缓存(buff/cache)。 -
vmstat 1:看 si/so(交换分区使用情况,高说明内存不足)。 -
top按 M 排序内存占用进程。
-
-
看的是实际内存(RAM),但也会显示 虚拟内存(Swap) 使用情况。
虚拟内存的意义是什么?怎么映射到真实的物理地址的?
-
意义:
-
扩展可用内存(把不常用的数据放到磁盘交换空间 Swap)。
-
提供连续的虚拟地址空间(进程看不到物理内存碎片)。
-
-
映射到物理地址:
-
通过 MMU(内存管理单元) + 页表(Page Table) 实现虚拟地址 → 物理地址转换。
-
若物理内存不足,部分页会被换出到 Swap。
-
现在有一个游戏排行榜,这个排行榜访问的QPS大概在1000w左右,写的QPS大概在500w左右,我们数据库的极限访问量大概在50w左右,你该怎么设计这个服务?
方案:
-
读多写少 → 缓存:
-
Redis集群(读写分离 + 分片)缓存排行榜数据,读 100% 走缓存。
-
写操作:先写 消息队列(Kafka/RocketMQ),异步批量更新数据库 & 缓存。
-
-
数据库优化:
-
分库分表(按玩家ID哈希)。
-
使用跳表/Redis ZSET 维护排行榜(高效插入+查询)。
-
二次哈希方法是什么?
-
定义:当哈希冲突时,用 第二个哈希函数 计算步长,进行 再探测(如
hash2(key) = prime - (key % prime))。 -
用途:用于 开放寻址法 解决哈希冲突(如
hash1(key) + i * hash2(key))。
手撕:给定哈希函数,使用哈希桶实现一个KV Cache,小于4K的记录使用二级索引?
#include <vector> #include <list> #include <unordered_map> #include <string> struct KV { std::string key; std::string value; bool is_small; // 是否 <4K }; class KVCACHE { private: static const int BUCKET_SIZE = 1000; std::vector<std::list<KV>> buckets; std::unordered_map<std::string, std::list<KV>::iterator> index; // 一级索引 std::unordered_map<std::string, std::list<KV>::iterator> small_index; // 二级索引(<4K) public: KVCACHE() : buckets(BUCKET_SIZE) {} void put(const std::string& key, const std::string& value) { size_t hash = std::hash<std::string>{}(key) % BUCKET_SIZE; auto& bucket = buckets[hash]; auto it = std::find_if(bucket.begin(), bucket.end(), [&](const KV& kv) { return kv.key == key; }); bool is_small = (value.size() < 4096); if (it != bucket.end()) { // 更新 it->value = value; it->is_small = is_small; } else { // 插入 bucket.emplace_back(KV{key, value, is_small}); it = std::prev(bucket.end()); } index[key] = it; // 一级索引 if (is_small) small_index[key] = it; // 二级索引 } std::string get(const std::string& key) { if (index.count(key)) { auto it = index[key]; return it->value; } return ""; } std::string get_small(const std::string& key) { if (small_index.count(key)) { auto it = small_index[key]; return it->value; } return ""; } };
一个数mod 512,怎么改成位运算?
a % 512等价于 a & 511(因为 512 是 2^9,512-1=511,即 0x1FF)
一个30000个结点的单链表,不能修改链表结点,怎么快速找到第20000大的数?
-
方法:快速选择(QuickSelect)变种(但链表不支持随机访问,改用 堆)。
-
最优解:维护一个大小为20000的最小堆(遍历链表,堆满后替换最小的)。
有若干个槽,第一个槽放i个球,第二个槽放i*j个球,第三个槽放i*j*j个球,后面以此类推,求第n个球是第几个槽中的第几个?
规律:第 n个球所在的槽 k满足:
-
槽
k的总球数 = i + ij + ij^2 + ... + ij^(k-1) = i(j^k - 1)/(j - 1)(等比数列求和)。 -
找到最小的
k使得前k-1槽总球数 < n ≤ 前k槽总球数。
int find_slot(int i, int j, int n) { int k = 1, total = 0; while (true) { int slot_balls = i * pow(j, k - 1); // 第k槽球数 = i*j^(k-1) if (total + slot_balls >= n) { int pos_in_slot = n - total; return {k, pos_in_slot}; } total += slot_balls; k++; } }
进程上下文切换会发生什么?
-
CPU寄存器状态保存/恢复(如程序计数器PC、栈指针SP)。
-
页表切换(TLB可能失效,影响内存访问速度)。
-
内核栈切换(进程的内核态数据保存)。
-
调度开销(CPU缓存命中率下降,影响性能)。
如果HTTPS握手失败,你怎么分析原因?如果证书过期,会有什么表现?
-
常见原因:
-
证书问题(过期、不受信任、域名不匹配)。
-
协议/加密套件不兼容(客户端和服务端不支持相同的TLS版本)。
-
网络问题(防火墙拦截443端口,中间人攻击)。
-
-
证书过期表现:
-
浏览器提示 “证书已过期”,或返回 SSL_ERROR_EXPIRED_CERT_ALERT。
-
curl -vhttps://example.com查看证书有效期(expire date)。
-
几十亿个int值去重问题?
-
方法1(内存足够):
-
std::unordered_set<int>(哈希表去重,但几十亿数据可能内存不足)。
-
-
方法2(内存不足):
-
外部排序 + 归并去重(先排序,再扫描去重)。
-
布隆过滤器(Bloom Filter)(概率性去重,可能有误判)。
-
分片处理(按哈希值分片到多个文件,分别去重)。
-
new了再删除,能否调用成员函数?
可以调用,但极其危险。若delete后对象内存已被回收(但未被覆盖),调用非虚成员函数可能“看似正常”(因函数代码在代码段,不依赖对象内存);但若访问成员变量或虚函数(依赖虚表指针,已被释放),必然导致未定义行为(崩溃或数据错误)。绝对不要这样做。
一个类如何返回不能修改的成员变量?
返回成员变量的const引用或const值。
内存缺页是什么?
当程序访问的虚拟内存地址尚未映射到物理内存(如首次访问或被换出到磁盘)时,CPU触发缺页异常,操作系统捕获后分配物理页(或从磁盘换入),更新页表并恢复进程执行。本质是虚拟内存的“按需加载”机制。
动态库和静态库,项目用了哪些动态库,如何管理,升级后接口变了怎么维护?
-
区别:静态库(
.a/.lib)编译时直接嵌入可执行文件;动态库(.so/.dll)运行时加载,节省空间且多进程共享。 -
管理:Linux用
ldd查看依赖的动态库,Windows用工具(如Dependency Walker);通过包管理器(apt/yum)或构建系统(CMake的find_library)管理。 -
升级维护:接口变化时需重新编译依赖项目(若ABI兼容可仅替换库文件),最好通过版本号控制(如
libfoo.so.1→libfoo.so.2),并提供过渡期兼容层。
push_back和emplace_back的区别?
push_back接受已构造的对象(可能触发拷贝/移动构造);emplace_back在容器内直接构造对象(通过参数转发给构造函数),避免额外拷贝/移动,效率更高。
无栈协程和有栈协程,怎么切换,上下文怎么保存
-
有栈协程:每个协程有独立的调用栈(保存局部变量/返回地址),切换时通过保存/恢复整个栈和寄存器状态(如ucontext库、Boost.Coroutine),开销大但灵活。
-
无栈协程(如C++20协程):无独立栈,依赖编译器生成状态机,切换时仅保存局部变量和挂起点(continuation),通过
co_await/co_yield触发挂起/恢复,上下文由编译器隐式管理(栈复用,开销小)。
mysql实现乐观锁
通过版本号或时间戳字段实现:
-
表中增加
version字段(初始值为0); -
读取数据时同时获取当前
version; -
更新时带上条件
WHERE id=xxx AND version=读取的version,并自增version; -
若受影响行数为0,说明数据已被其他事务修改(冲突),需重试或报错。
-
(本质是利用CAS思想,避免悲观锁的开销)
15.2 阿里
游戏内网可以访问,外网部分用户正常访问,部分用户不行,怎么排查,可能是什么原理,如果是直连ip呢怎么排查,怎么查看占用了多少描述符?
-
外网部分用户正常,部分不行:
-
检查DNS解析(
nslookup)。 -
网络路由(
traceroute看是否丢包)。 -
防火墙/安全组(是否限制IP或端口)。
-
CDN/负载均衡(是否配置正确)。
-
-
直连IP排查:
-
telnet IP 端口测试连通性。 -
netstat -anp | grep 端口看服务是否监听。 -
ss -s或lsof -i :端口查看连接数和描述符占用。
-
如果通知线程先通知了,异步任务线程才开始wait,那么这个线程还能处理任务队列中的任务吗?
如果 notify先于 wait:
-
线程会错过信号,可能永远阻塞在
wait(除非用条件变量的while循环检查 或 超时机制)。 -
解决方案:用
std::condition_variable+mutex+ 标志位 确保同步。
一个类的成员函数中有一个lamda表达式,可以访问这个类的私有成员吗,如何访问,如何传参?
-
可以访问(Lambda在类成员函数内,默认捕获
this即可)。 -
传参:如果Lambda在类外使用,需显式传
this或私有成员的引用/指针。
lambda表达式可以拷贝吗?如果一个lamda表达式有一个按值捕获的变量a,再拷贝了这个lamda表达式是什么结果?
-
Lambda可以拷贝(默认是可拷贝的)。
-
按值捕获的变量
a:-
拷贝Lambda时,
a也会被深拷贝(每个Lambda副本有自己的a副本)。 -
修改一个Lambda的
a不影响另一个。
-
threadlocal了解吗,作用是什么,什么时候创建呢?
-
作用:每个线程有独立的变量副本,避免多线程竞争。
-
创建时机:线程第一次访问时初始化(惰性初始化)。
-
C++实现:
thread_local关键字(如thread_local int x;)。 -
跨线程访问:
-
默认不能(每个线程有自己的副本)。
-
硬要传:手动传递值(如通过参数或全局变量)。
-
如果一个线程创建了一个threadlocal变量a,它可以让另一个线程访问吗,如果硬要传可以吗?
-
一个线程的
thread_local变量不能直接被另一个线程访问(每个线程独立存储)。 -
如果硬要传:
-
显式传递值(如通过函数参数、全局变量)。
-
不能用
thread_local直接共享(设计上就不允许)。
-
什么场景下用list比vector好呢?
-
频繁在头部/中间插入删除(
list是双向链表,O(1) 操作,vector是O(n))。 -
不需要随机访问(
list没有[]运算符,只有迭代器)。 -
大对象且避免拷贝(
list插入时不移动元素)。-
否则优先用
vector(缓存友好,随机访问快)。
-
vector里有10个成员,迭代器指向第3个元素,现在删除第4个元素,那么这个迭代器会失效吗?
不会。
unordered_map也是有很多元素,如果插入一个元素,原本的迭代器会失效吗,什么情况下会失效?
-
一般情况:插入元素 不会使已有迭代器失效(除非触发 rehash)。
-
失效情况:
-
插入导致 rehash(扩容) → 所有迭代器失效(但指针/引用仍有效)。
-
移动语义move一般在什么情况下用?
适用场景:
-
临时对象(右值) 赋值/构造给新对象(避免深拷贝)。
-
STL 容器操作(如
std::vector::push_back(std::move(obj)))。
什么场景下用move可以获得不错的性能增益,什么情况下不能?
-
能获得性能增益的场景:
-
大对象(如
std::string、std::vector)(避免深拷贝)。 -
资源管理类(如文件句柄、锁)(转移所有权而非复制)。
-
-
不能获得增益的场景:
-
基本类型(
int、float)(移动和拷贝无区别)。 -
没有移动构造/赋值的类(退化为拷贝)。
-
一个结构体里有一个1000size的数组,一个实例在move之后,那这个数组发生什么变化?
答案:
-
默认情况下,数组是逐元素拷贝的(
move不会优化)。 -
如果数组是
std::array或手动实现移动语义,则 指针/资源会被“窃取”(原对象数组变为空/未定义)。
如果我给三个变量赋值,那我用三个原子变量可以吗?
没懂题目意思。
父进程open了一个文件描述符,fork了一个子进程,那子进程可以访问这个文件描述符吗?
可以!fork()后,子进程继承父进程的文件描述符(指向同一个文件表项,共享文件偏移量)。
如果子进程close了这个文件描述符,父进程还可以访问吗?
不受影响!文件描述符是 进程独立的,子进程 close不会影响父进程的文件操作。
父进程读文件读到第10个字节,如果fork了一个子进程,子进程读的话,是从第几个字节开始读?
-
从第10字节开始!因为 父子进程共享文件偏移量(同一个文件表项),所以子进程会接着父进程的位置继续读。
-
注意:如果父子进程 同时读写,可能会产生竞争(需加锁)。
15.3 网易
kd树是什么?
KD树(K-Dimensional Tree) 是一种用于 多维空间数据(如2D/3D点) 的 空间划分数据结构,常用于 最近邻搜索(KNN)、范围查询。
-
构建方式:交替按不同维度(如X/Y/Z)划分数据,类似二叉搜索树但扩展到多维。
-
用途:加速高维数据的搜索(如图像特征匹配、地理定位)。
客户端应用层如果接受不了协议会发生什么情况,各种场景都分析下?
-
协议格式错误(如JSON/XML解析失败):
-
表现:客户端崩溃、报错(如
Invalid JSON),或忽略错误继续运行(可能导致逻辑异常)。
-
-
版本不兼容(如协议字段变更):
-
表现:字段缺失/多余,客户端可能用默认值、拒绝连接或功能异常。
-
-
加密/压缩问题:
-
表现:解密/解压失败,连接被断开或数据乱码。
-
-
网络传输问题(如TCP粘包):
-
表现:数据解析错位,触发协议校验错误。
-
动态连接和静态连接,怎么查看动态连接,他们的优缺点?
| 对比项 | 静态链接(Static Linking) | 动态链接(Dynamic Linking) |
| 查看方式 | 二进制文件较大,ldd无依赖(或仅系统库) | ldd <可执行文件>查看动态库依赖(如 libc.so) |
| 优点 | 部署简单(无外部依赖),运行速度快 | 节省磁盘/内存(共享库),更新库无需重新编译 |
| 缺点 | 体积大,更新库需重新编译 | 依赖外部库,可能因版本问题导致运行失败 |
random 7怎么去实现random 10?
方法:拒绝采样(拒绝无效区间)。
int random10() { while (true) { int num = (random7() - 1) * 7 + random7(); // 生成1~49均匀分布 if (num <= 40) return num % 10 + 1; // 取1~40映射到1~10 } }
原理:49个数中取前 40个(保证均匀),拒绝 41~49重新采样。
C++比Python效率高很多是否正确?
部分正确。C++是编译型语言,直接操作硬件,执行效率通常远高于解释型的Python。但在开发效率、代码简洁性上Python更优。
竞技手游,在考虑网络底层设计时通常可选TCP或UDP,这两种协议哪种更适合?
通常选UDP。竞技手游对实时性要求高(如MOBA、FPS),TCP的重传和拥塞控制会引入延迟,而UDP允许自定义可靠传输(如QUIC、KCP)。
通常UDP被认为更快,它为什么比TCP快?
UDP无连接、无重传、无拥塞控制,头部开销小(8字节 vs TCP的20+字节),直接发送数据,省去了确认、重传等机制。
运营商QOS的问题具体能展开说一下吗?
QoS(服务质量)是运营商对流量优先级管理的技术。可能限制非优先业务(如游戏UDP包)的带宽/延迟,通过流量整形、丢包策略影响实时性,导致UDP游戏卡顿。
除了explain工具,还有哪些查询优化手段?为什么加了索引后查询会更快?
-
手段:避免
SELECT *、合理分页(如LIMIT 1000,10优化为子查询)、减少JOIN、使用覆盖索引。 -
索引加速原因:索引(如B+树)将数据有序存储,减少全表扫描,通过树高度快速定位数据(时间复杂度O(log n))。
MySQL索引通常用什么数据结构实现?
通常用B+树(平衡多路搜索树),适合范围查询和磁盘IO优化;少数情况用Hash索引(等值查询快,但无序)。
新型数据库跟MySQL在业务场景上有什么区别?
-
MySQL:适合OLTP(事务处理),如订单、用户数据,强一致性需求。
-
新型数据库(如MongoDB/Redis/Elasticsearch):
-
MongoDB:灵活Schema,适合JSON文档存储(如内容管理)。
-
Redis:内存数据库,高速缓存/会话存储。
-
Elasticsearch:全文检索/日志分析。
-
float在内存中的格式是怎样的?符号位、指数位、尾数位各占多少比特?
-
格式:32位单精度浮点数
-
各部分位数:
-
符号位(Sign):1 bit(最高位)
-
指数位(Exponent):8 bits(中间8位)
-
尾数位(Mantissa/Significand):23 bits(最低23位)
-
-
公式:(-1)^S × 1.M × 2^(E-127)
若指数位为8 bit,如何计算float能表示的最大值?IEEE 754对特殊值有哪些规定?
-
最大值公式:(-1)^0 × 1.111...1 × 2^(254-127)
-
计算过程:
-
最大指数:2^8-2=254(全1保留给特殊值)
-
最大尾数:1.111...1(23个1,即2-2^-23)
-
最大值 ≈ 2^127 × (2-2^-23) ≈ 3.4028235×10^38
-
-
IEEE 754特殊值:
-
0指数全0:±0、非规格化数
-
0指数全1:±∞、NaN
-
new与malloc有什么区别?请展开malloc的底层实现过程。
// 区别: // 1. new/delete是运算符,malloc/free是函数 // 2. new自动计算大小,malloc需要手动计算 // 3. new调用构造/析构,malloc不会 // 4. new失败抛异常,malloc返回NULL
malloc底层:
-
向操作系统申请大块内存(brk/mmap)
-
维护空闲内存链表
-
从链表中分割合适大小的块
-
返回对齐的虚拟地址
结合虚拟地址空间概念,描述malloc从分配虚拟地址到建立物理映射的完整流程。
-
虚拟地址分配:
-
malloc从进程堆空间分配虚拟地址
-
更新进程页表(标记为未映射)
-
-
首次访问触发缺页中断:
-
内核处理缺页中断
-
通过brk/mmap扩展堆空间
-
建立虚拟页到物理页的映射
-
-
物理内存分配:
-
从空闲物理页分配
-
更新TLB和页表项
-
发生缺页中断时,操作系统会做哪些事情?
-
检查访问合法性(权限/地址有效性)
-
如果是堆/栈扩展:
-
分配新的物理页
-
建立页表映射
-
-
如果是文件映射:
-
从磁盘读取对应页
-
填充物理页
-
-
更新TLB缓存
-
重新执行触发中断的指令
多级页表的作用是什么?
-
节省内存:
-
只为使用的虚拟页分配页表项
-
典型:64位系统用4级页表,稀疏地址高效
-
-
按需分配:
-
未使用的地址空间不占用页表内存
-
-
实现方式:
-
顶级页表 → 二级页表 → ... → 物理页帧
-
伙伴系统(buddy system)的机制是怎样的?
-
核心思想:
-
内存按2的幂次划分(1KB,2KB,4KB...)
-
相邻相同大小的块可合并为"伙伴"
-
-
分配过程:
-
找到满足需求的最小2^n块
-
如需分割则递归分裂伙伴块
-
-
释放过程:
-
释放后检查伙伴是否空闲
-
若空闲则合并为更大的块
-
是否使用过redis?它高效的原因有哪些?
-
内存存储:数据全在内存
-
IO多路复用:epoll/kqueue
-
单线程模型:避免锁竞争
-
高效数据结构:
-
SDS字符串
-
跳表
-
哈希表
-
-
惰性删除:后台异步回收
该KV存储的持久化机制分别是如何实现的?
-
RDB(快照):
-
fork子进程生成二进制快照
-
save/bgsave命令控制
-
-
AOF(追加日志):
-
记录所有写命令
-
可配置同步频率(always/everysec/no)
-
-
混合持久化:
-
RDB+AOF,兼顾恢复速度和数据安全
-
如何设计一个既支持快速插入删除,又支持下标索引的容器?
方案:组合 vector(索引) + unordered_map(快速查找位置)。
deque的大致实现原理是什么?
-
分段连续存储:由多个固定大小的 块(chunk) 组成,通过 中央控制块(map) 管理这些块的指针。
-
特点:
-
头尾插入删除均 O(1)(无需整体搬迁)。
-
随机访问 O(1)(通过块号+块内偏移计算地址)。
-
手撕:请手写双向链表结构,并实现尾部插入?Node**与Node*的区别是什么?
struct Node { int val; Node* prev; Node* next; Node(int v) : val(v), prev(nullptr), next(nullptr) {} }; class DoublyLinkedList { Node* head; Node* tail; public: DoublyLinkedList() : head(nullptr), tail(nullptr) {} void push_back(int val) { Node* node = new Node(val); if (!tail) head = tail = node; else { tail->next = node; node->prev = tail; tail = node; } } };
C++智能指针有哪些?各自作用?
| 类型 | 作用 | 底层机制 |
| unique_ptr | 独占所有权,不可拷贝 | 析构时自动 delete |
| shared_ptr | 共享所有权,引用计数 | 引用计数归零时 delete |
| weak_ptr | 不增加引用计数,解决循环引用 | 需配合 shared_ptr使用 |
weak_ptr 如何打破循环引用?
场景:两个 shared_ptr互相引用导致内存泄漏。
解决:将一方改为 weak_ptr(不增加引用计数)。
如何检测代码存在循环引用或内存泄漏?
-
工具:
-
Valgrind(Linux,检测内存泄漏和非法访问)。
-
AddressSanitizer (ASan)(编译时加
-fsanitize=address)。
-
-
代码规范:
-
避免长期持有
shared_ptr,优先用weak_ptr观察。
-
其他语言的垃圾回收策略有了解吗?
-
Java(JVM):分代收集(新生代用 复制算法,老年代用 标记-整理/清除)。
-
Go:并发标记清除(三色标记法,减少STW时间)。
-
Python:引用计数 + 分代GC(解决循环引用)。
全局/静态变量位于哪个段?
-
.data段:已初始化的全局/静态变量。 -
.bss段:未初始化的全局/静态变量(运行时初始化为0)。
为什么要做内存对齐?
-
性能优化:CPU按内存块(如4/8字节)访问,对齐减少访存次数。
-
硬件要求:某些架构(如ARM)访问未对齐地址会触发异常。
多进程间有哪些通信方式?
-
管道(Pipe):单向,父子进程间通信
-
命名管道(FIFO):有名管道,任意进程间
-
共享内存:最快,需同步机制
-
消息队列:带优先级的消息传递
-
信号量:进程间同步
-
信号(Signal):异步通知
-
Socket:任意进程间(包括跨网络)
共享内存读写时如何同步?
// 使用信号量同步示例 sem_t *sem = sem_open("/mysem", O_CREAT, 0644, 1); // 初始值为1的互斥信号量 // 写进程 sem_wait(sem); // P操作 // 写入共享内存 sem_post(sem); // V操作 // 读进程 sem_wait(sem); // 读取共享内存 sem_post(sem); sem_close(sem);
TCP 连接如何检测对端断开?
-
recv()返回0:对端正常关闭
-
recv()返回-1,errno=ECONNRESET:对端异常断开
-
心跳机制:定期发送探测包
-
SO_KEEPALIVE:系统级保活机制
TCP 三次握手、四次挥手过程?
三次握手:
-
SYN=1, seq=x →
-
SYN=1, ACK=1, seq=y, ack=x+1 →
-
ACK=1, seq=x+1, ack=y+1
四次挥手:
-
FIN=1, seq=u →
-
ACK=1, seq=v, ack=u+1 →
-
FIN=1, ACK=1, seq=w, ack=u+1 →
-
ACK=1, seq=u+1, ack=w+1
游戏为何常用 UDP?需在 UDP 之上做哪些改进?
原因:
-
低延迟(无连接)
-
无重传机制(避免卡顿)
-
头部开销小
改进:
-
应用层确认机制:重要数据添加ACK
-
序列号:检测丢包和乱序
-
重传策略:选择性重传
-
流量控制:滑动窗口
-
KCP/QUIC:基于UDP的可靠传输协议
KCP 为什么延迟更低?
-
快速重传:收到3个冗余ACK立即重传
-
无延迟ACK:不等待定时器直接响应
-
选择性重传:只重传丢失的包
-
更小的RTT:优化拥塞控制算法
-
自定义拥塞控制:更激进的策略
UDP 服务端如何维护连接状态?
-
会话表:用哈希表存储客户端状态
-
超时机制:定期清理不活跃连接
-
序列号:跟踪数据包顺序
-
心跳包:客户端定期发送保活
Redis 的 zset 怎么实现?
跳表提供O(logN)的插入/删除/排名查询,哈希表提供O(1)的成员查找
跳表原理是什么?
-
多层链表结构:底层是全量有序链表,上层是索引层
-
随机层数:插入节点时随机决定层数(通常概率1/2)
-
查询路径:从最高层开始,逐层向下查找
-
时间复杂度:查找/插入/删除平均O(logN),最坏O(N)
Redis 集群如何把数据分布到各节点?
哈希槽(Hash Slot)方案:
-
16384个槽:集群将所有key映射到0-16383槽位
-
CRC16算法:
slot = CRC16(key) % 16384 -
槽位分配:启动时协商每个节点负责哪些槽
-
重定向:客户端访问错误节点时返回MOVED重定向
求数组第 K 大元素有哪些方法?
方法1:快速选择(QuickSelect) - O(n)平均
方法2:最小堆 - O(nlogk)
如何用 Redis 实现分布式锁?
-
NX:不存在才设置
-
PX:设置过期时间
-
随机值:防止误删其他客户端的锁
-
Lua脚本:保证检查+删除的原子性
最长「0 和 1 数量相等」子串如何做?若把 0 换成 -1,求和为 0 的最长子段,有思路吗?
-
0→-1转换:将原数组的0变为-1
-
前缀和:计算累计和
-
哈希表记录:首次出现某个和的位置
-
最大长度:当相同和再次出现时,计算子段长度
你的数据库访问压力优化是怎么做的?
-
缓存:用Redis缓存热点数据,减少DB查询
-
索引优化:合理设计索引,避免全表扫描
-
分库分表:水平拆分大表,分散压力
-
读写分离:主库写,从库读
-
SQL优化:避免N+1查询,使用批量操作
MySQL的默认隔离级别是什么?
可重复读(REPEATABLE READ)
MySQL的默认隔离级别怎么防止幻读的?
通过MVCC(多版本并发控制) + 间隙锁(Gap Lock):
-
普通查询使用MVCC读取快照,避免幻读
-
范围查询时加间隙锁,阻止其他事务插入新记录
SSH是什么?了解SSH吗?
SSH(Secure Shell)是安全网络协议,用于:
-
加密远程登录(替代telnet)
-
安全文件传输(SCP/SFTP)
-
端口转发
-
核心功能:加密通信、身份认证(密码/密钥)
TimeWait状态知道是什么吗?
TCP连接关闭时的最后一个状态:
-
主动关闭方进入此状态(持续2MSL)
-
作用:确保最后一个ACK到达对方,让网络中残留的旧连接数据包消失
-
问题:大量TIME_WAIT会占用端口资源
mmap是什么?
内存映射文件(Memory Map):
-
将文件直接映射到进程地址空间
-
通过内存读写操作文件(无需read/write系统调用)
-
特点:高效(减少拷贝)、适合大文件处理
-
实现:mmap()系统调用
git用过吗?rebase是什么?
变基操作:
-
将当前分支的提交"移动"到目标分支最新提交之后
-
效果:保持提交历史线性整洁
-
与merge区别:不产生合并提交
-
常用场景:整理本地提交历史
-
风险:会重写提交历史(不要对已推送的提交rebase)
C/C++:define f(x) (x<<2),问 f(x+=10)结果
宏是文本替换,f(x+=10)展开为 (x+=10 << 2),运算符优先级导致先算 10 << 2(结果为 40),再 x += 40。若初始 x为 a,最终 x = a + 40,表达式值为 a + 40(注意不是 (x+10)<<2)。
智力题:8枚硬币一个天平,其中一枚比较重,问至少称几次
2次。
-
第一次:将8枚分成3、3、2,称两组3枚。若平衡,重币在2枚中(第二次称这2枚即可);若不平衡,重币在较重的3枚中。
-
第二次:从3枚中任取2枚称,平衡则剩余1枚为重币,不平衡则较重的一边为重币。
给一个序列,求小根堆中序遍历
小根堆是中序遍历无序的!小根堆仅保证父节点 ≤ 子节点(结构特性),但中序遍历(左-根-右)结果依赖具体插入顺序,没有固定规律(可能乱序)。若要有序序列,应直接对堆进行排序(如建堆后依次弹出最小值)。
讲讲编译器优化指令重排
编译器/处理器为提升性能,可能调整指令执行顺序(不改变单线程语义)。例如:将无依赖的读写操作重排,或提前加载数据。但需遵守as-if规则(单线程下结果不变)。多线程中可能引发问题(需用volatile、原子变量或内存屏障禁止重排)。
成员函数 delete this 有什么问题
-
对象生命周期问题:必须确保对象是通过
new分配的(否则行为未定义); -
悬空指针:
delete this后,其他代码若持有该对象的指针/引用,访问会导致崩溃; -
析构后操作:
delete this后不能再调用任何成员函数(对象已销毁)。
设计一个调度定时任务的定时器,怎么实现
核心思路:时间轮/最小堆 + 事件循环。
-
用最小堆(按触发时间排序)管理任务,每次取堆顶最早任务;
-
线程循环检查堆顶任务是否到期(或用
epoll_wait超时机制),到期后执行并移除; -
支持添加/删除任务(堆调整或时间轮槽位操作)。
多个连接对应多个协程处理耗时 IO 事件,怎么设计
协程 + 事件驱动:
-
每个连接绑定一个协程,发起IO操作时
co_await(挂起协程); -
使用IO多路复用(如epoll)监听所有连接的FD,IO就绪时唤醒对应协程继续执行;
-
协程调度器管理协程状态(挂起/就绪),通过非阻塞IO避免线程阻塞。
怎么样将阻塞的方式改成非阻塞的协程调度
-
将阻塞调用改为非阻塞(如socket设为非阻塞,IO操作立即返回
EAGAIN); -
发起IO时挂起协程(用
co_await或类似机制,保存当前协程上下文); -
注册IO事件到多路复用器(如epoll),监听FD就绪;
-
IO就绪时唤醒对应协程(恢复上下文继续执行),通过协程调度器实现无缝切换。(关键:协程挂起/恢复 + 非阻塞IO + 事件驱动)
15.4 米哈游
物理内存有2G,申请8G多内存会发生什么?
-
32位系统:进程地址空间通常最大 2~3G(受内核保留空间限制),直接申请 8G 会失败(
malloc/new返回NULL,或抛出std::bad_alloc)。 -
64位系统:地址空间足够大,但 物理内存不足时:
-
malloc可能成功(分配虚拟内存,但未实际占用物理内存),首次访问时触发缺页中断,若物理内存+Swap耗尽,OOM Killer 可能终止进程。
-
虚拟地址和物理地址的映射,多级页表相比单级页表的优化?
| 对比项 | 单级页表 | 多级页表(如二级/四级) |
| 空间占用 | 大(需映射所有虚拟页,浪费内存) | 小(仅存有效页的映射,节省内存) |
| 查询速度 | 快(一次查表) | 慢(需多级查表,但TLB加速) |
| 优化原理 | 不适用大地址空间(如64位系统) | 按需分配页表项(稀疏地址高效) |
多级页表示例(以二级页表为例):
-
顶级页表 存储二级页表指针,二级页表 存储物理页帧号。
https协议建立连接过程?密钥由谁提供?
-
步骤:
-
TCP三次握手 → TLS握手(ClientHello/ServerHello → 密钥交换 → 加密通信)。
-
-
密钥来源:
-
对称加密密钥:由 客户端和服务器协商生成(如ECDHE算法动态计算)。
-
证书(公钥):由 服务器提供(CA签发,用于验证身份和密钥交换)。
-
操作系统文件读写过程要注意的细节?
-
缓冲机制:
-
标准库(如C++
fstream) 默认有缓冲,需flush()或sync()确保数据落盘。 -
系统调用(如
write) 可能仍在内核缓冲区,需fsync(fd)强制同步。
-
-
原子性:
-
多进程/线程写同一文件 需加锁(如
flock)或使用 O_APPEND 模式避免覆盖。
-
-
错误处理:
-
检查
errno(如EINTR中断需重试)。
-
a服务用tcp往b服务io写入文件场景,要注意什么?
-
网络可靠性:
-
处理丢包/乱序:TCP已保证可靠传输,但需应用层处理 粘包/拆包(如定长头+长度字段)。
-
-
流量控制:
-
避免B服务处理不过来:A端需 限流(如令牌桶算法)或 B端反馈背压。
-
-
异常处理:
-
B服务崩溃/断开:A端检测
write返回-1(EPIPE错误)并重连。
-
-
数据完整性:
-
关键数据加校验(如CRC),或用 可靠协议(如gRPC)。
-
Linux怎么排查CPU问题(可以用什么工具)
-
top/htop:查看CPU使用率、进程占用 -
vmstat:监控CPU上下文切换、中断 -
pidstat:按进程统计CPU使用 -
perf:性能分析(如perf top) -
strace/ltrace:跟踪系统调用/库调用
如果排查出来CPU比如说IO的问题你的进一步排查思路是什么,你觉得可能出现这样的原因是啥
-
确认类型:
vmstat 1看us(用户态)、sy(内核态)、wa(IO等待) -
IO瓶颈:
iostat -x 1看磁盘%util、await -
可能原因:
-
磁盘慢(机械盘/RAID问题)
-
大量小文件读写
-
数据库未命中缓存(如MySQL buffer pool不足)
-
不合理日志写入(如同步刷盘)
-
假设有一个链路:客户端,nginx,后端,数据库,其中报错502,你会怎么排查
-
Nginx日志:检查
error_log中的502 Bad Gateway详情 -
后端状态:
-
curl -v直接测试后端接口 -
ps aux | grep 后端进程确认进程存活 -
netstat -tulnp看端口监听
-
-
数据库:检查慢查询、连接池耗尽(如
SHOW PROCESSLIST) -
Nginx配置:确认
proxy_pass后端地址正确,超时时间合理(如proxy_connect_timeout)
MySQL的主从复制: 假设有一个节点主要写,从节点读,那么你怎么保证一致性?有什么参数可以设置吗?
-
半同步复制:主库至少收到一个从库的ACK才返回成功(参数:
rpl_semi_sync_master_enabled=1) -
GTID复制:基于全局事务ID避免数据遗漏(
gtid_mode=ON) -
读写分离策略:强一致性读走主库,弱一致性读走从库
-
监控延迟:
Seconds_Behind_Master(SHOW SLAVE STATUS)
TCP的四次挥手: 四次挥手之后的状态是什么?为什么要有这个状态?
-
主动方:进入
TIME_WAIT(持续2MSL,通常60秒) -
被动方:直接进入
CLOSED -
为什么需要TIME_WAIT:
-
确保最后一个ACK到达对端
-
让网络中残留的旧连接数据包失效
-
time-wait会阻塞吗?如果在一个高并发的情况下,很多的time-wait阻塞,要怎么处理?
危害:占用端口资源,可能导致无法建立新连接
解决方案:
-
缩短TIME_WAIT时间:
sysctl -w net.ipv4.tcp_fin_timeout=30(默认60秒) -
开启端口复用:
sysctl -w net.ipv4.tcp_tw_reuse=1 -
快速回收:
sysctl -w net.ipv4.tcp_tw_recycle=1(注意:NAT环境下慎用) -
负载均衡:分散连接压力到多IP
-
优化业务:减少短连接(改用长连接)
socket通信里面的阻塞非阻塞什么意思?
-
阻塞:调用(如
recv/accept)未就绪时会一直等待(线程挂起),直到数据到达/连接建立才返回(如默认的socket行为)。 -
非阻塞:调用未就绪时立即返回错误码(如EWOULDBLOCK),需通过轮询(如
select/epoll)判断何时可操作,避免线程阻塞。
红黑树左旋右旋什么意思?
红黑树调整平衡的旋转操作,用于维护性质(如父节点为红时子节点必须为黑)。
-
左旋:以某个节点为支点,将其右子节点提升为父节点,原节点成为新父节点的左子节点(右子树的左子树挂到原节点右子树)。
-
右旋:对称操作,以节点为支点将其左子节点提升为父节点,原节点成为新父节点的右子节点(左子树的右子树挂到原节点左子树)。
Redis怎么做消息队列,可以使用什么数据结构?
常用List(LPUSH+BRPOP)或Stream(更强大):
-
List:生产者
LPUSH消息到队列头,消费者BRPOP阻塞等待队列尾消息(简单但功能有限)。 -
Stream:支持多消费者组、消息ID、确认消费等(类似Kafka,命令如
XADD/XREAD)。
怎么用Redis的数据结构实现一个延迟消息队列?
用Sorted Set(ZSET):
-
添加任务:
ZADD delay_queue <执行时间戳> <消息内容>(时间戳作为score)。 -
消费任务:定时(如每秒)执行
ZRANGEBYSCORE delay_queue 0 <当前时间戳>获取到期消息,处理完后ZREM移除。 -
(或用Redis 5.0+的Stream + 消费者延迟拉取间接实现)
手撕:自己实现vector,怎么设计?其中迭代器是什么怎么设计?
-
数据存储:动态数组(
T* data),记录size(当前元素数)和capacity(总容量)。 -
关键操作:
-
扩容:
push_back时若size==capacity,则重新分配更大内存(通常2倍扩容),拷贝旧数据。 -
迭代器:定义为类内嵌类型(如
typedef T* iterator),本质是对data指针的封装(支持++/--/*等操作),迭代器失效规则同STL(如扩容后原迭代器失效)。
-
C++中,类的成员变量初始化赋值默认是多少?
-
内置类型(int/float等):未显式初始化时值随机(内存残留值)(全局/静态成员默认0)。
-
类类型(如string):调用默认构造函数(若无则编译错误)。
-
最佳实践:始终在构造函数初始化列表或构造函数体内显式初始化所有成员,避免未定义行为。
websocket降级策略?
给一个路径包含/./../来得到最简路径?
企业MySQL一般是不使用join的,你还有什么办法吗?
用应用层关联(多次查询+代码拼接)或冗余字段(提前存储关联数据)。例如:分两次查询主表和从表数据,在代码里通过ID匹配关联(避免SQL的JOIN性能损耗,尤其跨库/大表时更可控)。
C++ 内存泄露怎么排查?
工具+代码检查:
-
工具:Valgrind(Linux)、Dr. Memory(Windows/Linux)、Visual Studio诊断工具(Windows)检测未释放的内存。
-
代码:检查
new/malloc是否配对delete/free,智能指针(unique_ptr/shared_ptr)替代裸指针,日志记录资源分配/释放点。
指针和引用区别?
-
指针:存储地址,可为空(
nullptr)、可修改指向(int* p = &a; p = &b;)、需解引用(*p)。 -
引用:别名(必须初始化且不可改绑定对象,如
int& r = a;),无空引用,语法更简洁(直接用r操作原对象)。
const_cast 作用?
移除或添加const/volatile属性(不改变底层数据)。例如:将const int*转为int*以修改原数据(慎用!仅当原对象本身非const时安全,否则未定义行为)。
static_cast 和 dynamic_cast 作用和区别?
-
static_cast:编译时类型转换(如基本类型转换、基类指针转派生类指针(需确定安全)、非const转const),无运行时检查。
-
dynamic_cast:运行时类型检查(仅用于含虚函数的类),安全转换基类指针/引用到派生类(失败返回
nullptr或抛异常),有性能开销。
map 和 unordered_map 区别?
-
map:基于红黑树,有序(按键排序),查找/插入/删除O(log n),稳定性能。
-
unordered_map:基于哈希表,无序,平均查找/插入/删除O(1),最坏O(n)(哈希冲突严重时),依赖哈希函数。
unordered_map 大量哈希冲突怎么解决?
-
优化哈希函数:使键分布更均匀(如自定义类型的哈希函数需覆盖所有字段)。
-
调整负载因子:
reserve预分配足够桶数,或设置较小的最大负载因子(如0.7)触发提前扩容。 -
换冲突处理方式:开放寻址法/链地址法优化(取决于实现,如改用更好的哈希表库)。
vector里存自定义类型,怎么拷贝?
默认调用自定义类型的拷贝构造函数和拷贝赋值运算符(若未定义则编译器生成浅拷贝)。需深拷贝时,自定义类中实现拷贝构造函数(如MyClass(const MyClass& other) { data = new int(*other.data); })和拷贝赋值运算符。
虚拟内存和物理内存?
-
虚拟内存:进程看到的连续地址空间(由操作系统映射管理),隔离进程、扩展可用空间。
-
物理内存:实际硬件内存(RAM),虚拟内存通过页表映射到物理页,未使用的虚拟内存不占物理内存(可能换出到磁盘)。
数据库为什么用 B+ 树不用红黑树?
-
矮胖结构:B+树高度更低(所有数据在叶子节点且链表连接),减少IO次数(数据库索引常驻磁盘)。
-
范围查询高效:叶子节点链表支持顺序遍历(红黑树需中序遍历,效率低)。
-
节点存储更多键:B+树非叶子节点只存键(不存数据),单节点能容纳更多分支。
tcp 和 udp 区别?
-
TCP:可靠传输(确认/重传/排序)、面向连接(三次握手)、流量控制(滑动窗口)、拥塞控制,慢但稳定(如文件传输)。
-
UDP:无连接、不可靠(不保证顺序/到达)、轻量级(无重传机制),快但可能丢包(如视频流/游戏实时通信)。
tcp 怎么优化?
-
调参:增大窗口大小(
net.ipv4.tcp_window_scaling)、调整MSS/MTU避免分片、启用快速重传(tcp_sack)。 -
减少RTT:就近部署服务器、使用CDN。
-
协议优化:长连接复用(避免频繁握手)、HTTP/2多路复用(减少连接数)。
场景设计题:有一个主线程处理游戏逻辑,和一些工作线程处理io,有客户端发来请求,怎么设计系统模型?
生产者-消费者模型:
-
IO线程:接收客户端请求,封装成任务(如
Request对象)投递到任务队列(线程安全,如加锁的queue或无锁队列)。 -
主线程:逻辑循环中从任务队列取出请求处理(或批量处理),结果通过共享数据(加锁/原子操作)或事件通知IO线程回传客户端。
-
(关键:线程间同步(互斥锁/条件变量)+ 任务队列避免锁竞争(如双缓冲队列))
手撕:无序数组使得每个元素与相邻元素都不相同?
遍历数组,遇到相邻重复时将当前元素与后面第一个不重复的交换(或后移插入)。
MTU 是什么,什么作用
MTU(Maximum Transmission Unit)是网络链路层一次能传输的最大数据包大小(通常以太网MTU为1500字节)。作用是避免数据包分片(过大需分片传输,降低效率且可能丢失重传),优化传输性能。
socket NO_DELAY 作用
即TCP_NODELAY选项,禁用Nagle算法(该算法会合并小数据包等待ACK或凑满缓冲区再发送)。开启后数据立即发送(减少延迟,适合实时交互场景如游戏、即时通讯)。
REUSE_ADDRESS 和 REUSE_PORT 作用
-
SO_REUSEADDR:允许绑定处于TIME_WAIT状态的地址(快速重启服务),或同一主机多进程绑定相同IP不同端口。
-
SO_REUSEPORT(Linux特有):允许多个进程/线程同时绑定相同IP和端口,内核自动负载均衡连接(提升多核并发能力)。
为什么需要 time_wait
TCP四次挥手后主动关闭方进入TIME_WAIT(2MSL时长),确保最后一个ACK到达对端(若丢失对端会重传FIN),同时让网络中残留的旧连接数据包失效(避免影响新连接)。
为什么要设计 reuse 这个配置,解决什么问题
解决端口/地址占用问题。默认情况下,TIME_WAIT状态的地址或正在使用的端口无法立即复用(导致服务重启失败或端口冲突),REUSEADDR/REUSEPORT通过配置允许强制复用,提升服务可用性和并发能力。
shared_ptr 线程安全吗,哪些安全,哪些不安全
控制块(引用计数)的原子操作是线程安全的(多线程同时拷贝/析构shared_ptr不会导致计数错误),但访问其管理的资源(指向的对象)不安全(需额外同步)。不同shared_ptr实例间的读写非原子(如一个线程修改指针,另一个读取可能冲突)。
两个线程将同一个 shared_ptr 调用拷贝赋值到一个新的对象上,线程安全吗,为什么
安全。因为shared_ptr的控制块(引用计数)通过原子操作保证线程安全,多个线程同时拷贝同一个shared_ptr时,引用计数的增减是原子的(不会导致计数错误或资源提前释放)。
shared_ptr 释放了资源,weak_ptr 怎么知道
weak_ptr不增加引用计数,但通过控制块中的弱引用计数跟踪资源状态。当最后一个shared_ptr析构时(强引用计数归零),资源被释放,但控制块保留(弱引用计数仍存在),weak_ptr调用expired()可判断资源是否已释放。
控制块释放时机?
当强引用计数(shared_ptr)和弱引用计数(weak_ptr)都归零时,控制块(包含引用计数和对象指针等元数据)才会被释放(避免内存泄漏)。
一个进程虚拟内存很大,物理内存很小,有什么风险吗
风险包括频繁缺页(Page Fault)导致性能下降(大量数据不在物理内存需从磁盘换入)、触发OOM Killer(系统内存耗尽时强制终止进程)、磁盘I/O压力大(频繁换页拖慢整体系统响应)
epoll 的两种模式?要发送一个很大的数据,epoll两种模式有什么区别?
-
LT模式:只要socket可写(缓冲区未满),epoll_wait会持续通知,可分多次发送数据(适合普通场景)。
-
ET模式:仅在socket状态变化(如从不可写变为可写)时通知一次,需一次性尽可能发送完数据(否则可能丢失后续通知,大块数据需循环写直到EAGAIN)。
你说的接收端的两种模式区别没问题,如果发送方也用epoll,两种模式有什么区别呢?
-
LT模式:发送缓冲区未满时持续通知,可分多次发送(逻辑简单但可能多次触发)。
-
ET模式:仅当发送缓冲区从不可用变为可用时通知一次,需一次性发完数据(需循环写直到返回EAGAIN,效率高但编码复杂)。
(大块数据推荐ET+非阻塞socket+循环写,减少epoll_wait触发次数)
怎么看一个进程的性能?你说了 cpu 内存,io 怎么看
-
CPU:
top/htop(查看使用率)、perf(分析热点函数)。 -
内存:
free -m(总量/剩余)、top(RSS)、valgrind(检测泄漏)。 -
IO:
iostat(磁盘读写速率/延迟)、vmstat(IO等待wa)、iotop(进程级IO占用)。
malloc 分配的内存,free 怎么知道大小
malloc内部通过维护分配元数据(通常在分配内存块头部)记录分配的大小(如块大小、对齐信息等),free时根据传入的指针偏移找到元数据,解析出原始分配大小后释放对应内存(用户无需手动传大小)。
tcmalloc 怎么做的了解吗
tcmalloc(Thread-Caching Malloc)是Google的高性能内存分配器,核心优化:
-
线程缓存:每个线程维护本地缓存(减少锁竞争)。
-
中央堆:线程缓存不足时从中央堆申请/归还内存(按大小分类管理)。
-
更快的分配/释放:针对小对象优化(减少系统调用,提升多线程场景性能)。
redis 怎么实现分布式锁
常用SET key value NX PX timeout命令(原子性操作):
-
NX:仅当key不存在时设置(避免重复加锁)。
-
PX timeout:设置过期时间(防止死锁)。
-
释放锁:通过Lua脚本校验value(确保是当前客户端持有的锁)后再删除key(避免误删他人锁)。
-
(或使用Redlock等更复杂的算法,但需权衡可靠性)
15.5 库洛
讲讲map 和 unordered_msp
-
map:基于红黑树(有序),按键排序,查找/插入/删除时间复杂度为 O(log n),稳定性能。 -
unordered_map:基于哈希表(无序),平均查找/插入/删除为 O(1),最坏 O(n)(哈希冲突严重时),依赖哈希函数。
unordered_map 什么时候采用红黑树
默认不使用红黑树(用哈希表)。
讲讲 STL 内存的管理分配器
STL通过分配器(Allocator)管理内存分配/释放(默认是std::allocator,调用new/delete)。分配器负责:
-
容器内存的分配(如
vector的扩容)、释放; -
可自定义分配器(如内存池、栈内存分配器),通过模板参数传入(如
vector<T, MyAllocator>)。
讲讲tcmalloc
tcmalloc(Thread-Caching Malloc)是Google的高性能内存分配器,核心优化:
-
线程缓存:每个线程维护本地内存缓存(减少锁竞争);
-
中央堆:线程缓存不足时从中央堆申请/归还内存(按大小分类管理);
-
小对象优化:针对小内存分配更高效,适合多线程高并发场景。
设计一个内存池
核心思路:预分配大块内存(如char* pool),按固定大小分块管理(或按需划分)。
-
分配:从空闲链表中取一块,无空闲时向系统申请新大块;
-
释放:将内存块回收到空闲链表(或标记复用);
-
优化:支持多尺寸块、对齐、线程安全(如加锁或线程本地缓存)。
shared_ptr 控制块有哪些信息
控制块(通常由make_shared或new隐式创建)包含:
-
强引用计数(
shared_ptr计数,归零时释放资源); -
弱引用计数(
weak_ptr计数,归零时释放控制块); -
指向实际资源的指针;
-
可能的删除器/分配器信息(自定义时使用)。
make_shared 比用 new 好在哪
-
性能:
make_shared一次性分配内存(包含控制块和对象),减少内存分配次数(new需分别分配对象和控制块); -
局部性:对象和控制块内存连续,缓存友好;
-
安全:避免
shared_ptr和裸指针混用导致控制块提前释放(如new后传裸指针给多个shared_ptr可能重复析构)。
讲讲redis 跳表
跳表(Skip List)是Redis有序集合(如ZSET)的底层实现之一,本质是多层链表:
-
结构:底层是普通链表,上层是跳跃节点(快速定位);
-
操作:查找/插入/删除平均 O(log n),比红黑树实现简单,且支持范围查询(链表有序)。
口述思路:很多个字符串,快速求前缀
用Trie(字典树):
-
每个节点存储一个字符,从根到某节点的路径表示一个前缀;
-
插入字符串时逐字符构建节点,查询前缀时沿路径查找(找到即存在,否则不存在);
-
时间复杂度:插入/查询均为 O(L)(L为前缀长度)。
倒排索引是什么
搜索引擎/数据库常用结构,用于快速通过值找键。例如:
-
传统索引(正排):
文档ID → 文档内容; -
倒排索引:
关键词 → 包含该关键词的文档ID列表; -
应用:快速检索包含特定词的文档(如搜索“Redis”时直接查倒排索引找到相关文档)。
15.X 其他公司
int取值范围?2^32 + 1 = ?(int情况下)
-
int取值范围(32位系统):-2^31 ~ 2^31-1(即 -2147483648 ~ 2147483647)
-
2^32 + 1:在int(32位有符号)下会溢出,实际值为 1(因为 2^32 溢出后为 0,+1=1)。若用
unsigned int,则为 2^32 + 1 = 4294967297;若用long long,则正常表示。
空类占几个字节?class A{ int a, char b} 占几个字节?
-
空类:占 1 字节(为了保证不同对象地址不同)
-
class A { int a; char b; }:占 8 字节(int(4) + char(1) + 对齐填充(3))
析构函数为什么最好是虚函数?
-
原因:当通过基类指针删除派生类对象时,若析构函数非虚,则只会调用基类析构函数,导致派生类资源泄漏。
-
最佳实践:若类可能被继承,且可能通过基类指针删除,析构函数应声明为虚函数。
虚析构函数调用的过程(其实问的是虚函数实现多态的原理)
-
虚函数实现多态:通过虚函数表(vtable)和虚表指针(vptr)实现。
-
每个含虚函数的类有一个 vtable,对象中存 vptr 指向其 vtable。
-
调用虚函数时,通过 vptr 找到正确的函数实现。
-
-
虚析构函数调用过程:同样通过 vtable 找到实际类的析构函数,保证派生类资源正确释放。
Base * a= nullptr; a可以调用虚函数吗?a可以调用成员函数吗?那a直接调用成员函数肯定会有问题,什么情况下有问题?
-
调用虚函数:未定义行为(UB),通常崩溃(因为要通过 vptr 找虚表,但 a 为 nullptr)。
-
调用成员函数(非虚):若函数内部不访问成员变量,可能“正常”执行(但不推荐!);若访问成员变量,则崩溃。
-
有问题的情况:任何访问成员变量或虚函数的调用,都可能因 this 为 nullptr 导致崩溃。
C++中,在main函数执行的代码有哪些?那全局变量的初始化顺序是怎么样的?
-
main前执行:
-
全局变量/静态变量的构造函数
-
全局对象的初始化
-
-
全局变量初始化顺序:
-
同一编译单元(.cpp文件)内:按定义顺序初始化
-
不同编译单元间:顺序不确定(可能导致“静态初始化顺序问题”)
-
什么是稳定/不稳定排序?
-
稳定排序:相等元素的相对顺序保持不变(如冒泡、插入、归并排序)
-
不稳定排序:相等元素的相对顺序可能改变(如快速、堆排序)
假如要对100个玩家,按照等级排序,那么希望调用一次排序函数和调用三次排序函数结果一致,如何实现?除了稳定排序呢?
-
方法:除稳定排序外,可先排序并记录原始索引,排序时将索引作为次要关键字,保证相同等级玩家按原始顺序排列。
-
示例:排序时不仅比较等级,还比较原始下标,实现人为“稳定性”。
一个升序数组,一个降序数组,如何合并成一个升序数组?时间复杂度O(m+n)?
降序数组从后往前遍历,视为升序,然后双指针合并。
一个数组,删去其奇数索引的元素,且其他元素相对位置顺序不变,且空间复杂度O(1)?
双指针原地覆盖,最后调整大小,满足 O(1) 空间 且 顺序不变。
用过 awk 吗?
awk是文本处理工具,适合按行/列处理结构化文本。
C++20 的协程和其他语言的协程有什么区别?
| 对比项 | C++20 协程 | 其他语言(如 Python/Go) |
| 语法 | 底层(需手动管理 promise/awaitable) | 高层(async/await语法糖) |
| 调度 | 无内置调度器,需自行实现 | 通常有内置调度(如 Go 的 goroutine) |
| 运行时 | 依赖编译器实现,无标准运行时 | 语言原生支持 |
| 用途 | 底层异步 I/O、自定义协程逻辑 | 高并发、异步编程 |
设计一个序列化格式,有整数、浮点数、字符串三种字段?
方案:TLV(Type-Length-Value)格式
-
格式:
[类型(1字节)][长度(4字节)][值(变长)] -
类型定义:
-
0x01:整数(4字节)
-
0x02:浮点数(8字节)
-
0x03:字符串(长度后接 UTF-8 字节)
-
-
示例:序列化
int(42)→\x01\x00\x00\x00\x04\x2A\x00\x00\x00
设计一个哈希表的序列化方式,要求能直接 mmap 访问哈希表?
-
固定头部:记录桶数量、键值对总数。
-
桶数组:每个桶存储键值对的内存偏移量(固定大小,如 8 字节)。
-
键值对数据区:连续存储所有键值对(键+值+长度信息),按写入顺序排列。
-
优势:mmap 后可直接通过偏移量访问数据,无需反序列化。
-
设计一个分布式 KV 存储方案?
-
数据分片:按 Key 的哈希值分片到多个节点(如一致性哈希)。
-
副本机制:每个分片多个副本(如 Raft 协议保证一致性)。
-
接口:提供
Get(key)/Put(key, value)API。 -
底层存储:每个节点用 RocksDB(LSM 树)或 内存哈希表 + 定期刷盘。
-
扩展性:支持动态扩缩容分片。
SQL占位符了解吗?
了解。SQL 占位符(如 ?或 $1)用于预编译 SQL,防止注入。
批量 SQL INSERT 操作时,每条记录的列结构(即占位符数量和类型)不固定时如何解决?
方法:动态生成 SQL 或使用 JSON/键值对格式
-
动态 SQL 拼接:根据每条记录的列生成对应的
INSERT语句(需防注入)。 -
JSON 格式:插入单条 JSON 记录,应用层解析(如
INSERT INTO table (data) VALUES ('{"name":"Alice","age":20}'))。 -
通用占位符表:用
Map<String, Value>表示列名和值,后端动态拼接 SQL。 -
推荐:使用 ORM 框架(如 SQLAlchemy、Hibernate)或 批量 JSON 导入。
MySQL的数据存储在哪?
-
数据存储位置:默认在磁盘的 数据目录(datadir) 下(可通过
show variables like 'datadir';查看),以 表空间文件(.ibd)、索引文件、日志文件(redo/undo) 等形式存储。 -
具体文件:每个数据库是一个文件夹,表数据可能存储在
.frm(表结构,MySQL 8.0+ 已移除)、.ibd(InnoDB 表数据+索引)等文件中。
一条 SELECT 语句从被客户端发出到最终从存储文件中找到数据并返回结果的过程描述一下
-
客户端发送SQL → 服务器通过 MySQL协议 接收请求。
-
解析与优化:解析SQL生成执行计划,优化器选择最优索引/表访问方式。
-
存储引擎交互:
-
若数据在 内存缓冲池(Buffer Pool) 中,直接返回。
-
若不在,从 磁盘数据文件(如.ibd) 读取对应页(Page),加载到缓冲池后再返回。
-
-
结果返回:将查询结果通过协议返回给客户端。
说说MySQL 客户端/服务器协议?
-
二进制协议:默认基于 TCP,采用 请求-响应 模式。
-
特点:
-
支持认证、查询、结果集返回等阶段。
-
数据以 包(Packet) 形式传输(默认最大 4MB,可配置)。
-
客户端/服务端通过 序列号、状态标志 协同工作。
-
-
典型流程:连接 → 认证 → 发送SQL → 接收结果。
了解游戏开发服务端同步机制吗?
-
常见同步方式:
-
状态同步:服务端计算游戏逻辑,将角色状态(位置/血量)广播给客户端(如 MMO)。
-
帧同步:服务端同步输入(操作指令),客户端本地计算逻辑(如 MOBA/RTS)。
-
-
关键技术:
-
网络延迟补偿(如插值、预测回滚)。
-
数据压缩与增量更新。
-
权威验证(防止作弊)。
-
让你设计一个游戏用户的签到功能,能统计签到次数给用户发放奖励,详细说说你的设计思路?
-
数据存储:
-
用户签到记录表(
sign_record):user_id, sign_date, is_signed(记录每日是否签到)。 -
用户签到统计表(
sign_stats):user_id, total_days(累计签到天数)。
-
-
签到流程:
-
用户请求签到 → 服务端检查
sign_date是否为当天且未签到 → 若未签到,更新is_signed=1,total_days+1。
-
-
奖励发放:根据
total_days或每日签到配置表发放对应奖励。
当用户签到次数到了一定的数量给用户发放奖励,说说在用户点击点击“领取”键来领取奖励时,服务端如何实现这一过程?
-
请求处理:用户点击领取 → 服务端接收
user_id和目标奖励条件(如签到第N天)。 -
条件校验:
-
检查
sign_stats.total_days >= N或当日是否已签到。 -
检查该奖励是否已领取(通过
reward_status字段或独立领取记录表)。
-
-
发放奖励:若条件满足,更新用户背包/货币,标记奖励为已领取。
-
返回结果:通知客户端奖励已发放。
如何防止用户重复领取?
-
数据库唯一约束:在领取记录表中设置
(user_id, reward_id)唯一索引,重复插入会报错。 -
状态标记:在用户表或奖励表中增加
is_rewarded标志位,领过后置为1。 -
事务控制:领取操作用 事务 包裹,先检查后更新,避免并发重复领取。
-
幂等设计:领取接口保证多次请求结果一致(如通过唯一请求ID或条件预检)。
推荐方案:唯一约束 + 事务(最可靠)。
unorderedmap如何遍历。遍历过程中如果删除某个数会不会迭代器失效?
用迭代器遍历(如for(auto it = m.begin(); it != m.end(); ++it))或范围for循环(for(const auto& [k,v]: m))。
删除时:直接erase(it++)(先递增迭代器再删除旧迭代器指向的元素)可避免失效;若直接erase(it),当前迭代器会失效(后续不能再使用),但C++17起erase返回下一个有效迭代器(应写为it = m.erase(it))。
vector中存了100W个数,需要将第50W个数据删除,怎么做高效?
直接调用vec.erase(vec.begin() + 500000),但注意效率问题:vector是连续内存,删除中间元素会导致后续所有元素向前移动(约50W次拷贝)。若后续不再频繁随机访问,可考虑改用std::list(删除O(1)),但牺牲了随机访问性能。当前场景下只能接受O(n)移动开销。
map怎么让自定义类型做key值?
需为自定义类型提供比较规则(默认用std::less<Key>,即<运算符)。两种方式:
-
重载
<运算符:确保自定义类型支持严格弱序比较(如bool operator<(const MyType& other) const { ... })。 -
自定义比较函数对象:传入
std::map<MyType, Value, MyCompare>,其中MyCompare是仿函数(如struct MyCompare { bool operator()(const MyType& a, const MyType& b) const { ... } })。
hashmap中存放大量游戏数据,现在需要扩容,但直接扩容会导致卡顿,怎么处理这个卡顿?
采用渐进式扩容(如Redis的哈希表策略):
-
提前分配新桶数组(不立即迁移数据);
-
每次操作(插入/查询)时顺带迁移少量旧数据(如1~2个桶),分摊到多次请求中;
-
旧桶数组保留直到所有数据迁移完成。避免一次性全量迁移导致的集中卡顿。
1000发子弹,和50架敌机,怎么设计数据结构?
-
子弹:高频创建/销毁,用对象池(预分配内存)+ 双端队列(
deque)管理(减少动态内存分配开销); -
敌机:数量少且需频繁查询/更新状态,用数组(
vector)或list存储(根据是否需随机访问选择); -
碰撞检测:子弹和敌机分别用空间分区(如网格/四叉树)优化,减少遍历次数(如只检测相邻区域的子弹和敌机)。
一个进程的地址空间是怎么样的?
-
代码段(Text):只读,存放程序指令。
-
数据段(Data):全局/静态变量(已初始化)。
-
BSS段:全局/静态变量(未初始化)。
-
堆(Heap):动态内存分配(
malloc/new,向高地址增长)。 -
内存映射区(mmap):动态库、文件映射。
-
栈(Stack):局部变量、函数调用(向低地址增长)。
一个进程把内存用光了会发生什么事?
-
malloc/new失败:返回NULL(C)或抛出std::bad_alloc(C++)。 -
OOM Killer(Linux):若系统物理内存+Swap耗尽,内核可能强制终止进程(选择占用内存高的进程)。
-
程序崩溃:访问非法内存(如野指针)导致 段错误(Segmentation Fault)。
-
系统变慢:频繁触发缺页中断(Page Fault),磁盘Swap交换加剧延迟。
用Linux命令实现功能:在一个日志文件中,每条日志中都有一个client_ip字段记录一个ip,找出该日志中出现最多次的ip?
awk '{print $client_ip_field}' logfile.log | sort | uniq -c | sort -nr | head -1
epoll和select的优缺点,怎么理解异步非阻塞?
| 特性 | epoll | select |
| 优点 | 高并发(O(1) 事件通知),支持百万级连接 | 简单,跨平台 |
| 缺点 | 仅 Linux 支持 | 每次调用需重传 fd_set(O(n) 遍历) |
| 异步非阻塞 | epoll + 非阻塞 IO 是典型组合 | 需轮询,效率低 |
异步非阻塞理解:
-
非阻塞:IO 操作立即返回(如
read()不会阻塞线程)。 -
异步:IO 完成后回调通知(如
libuv/Boost.Asio)。 -
epoll 是 同步非阻塞(需主动检查事件),但常与异步编程模型结合使用。
SQL语句实现功能:一个数据库表中存储三个字段:username、key、value,一个用户会有多条不同的字段记录,越活跃的用户对应字段越多,找出最活跃的10个用户?
例: username key value zhangsan sex 1 lisi sex 0 zhangsan name zhangsan 上面表中zhangsan比lisi活跃,因为他对应两个属性,lisi只有一个
SELECT username, COUNT(*) AS active_count FROM your_table GROUP BY username ORDER BY active_count DESC LIMIT 10;
用redis去存微信朋友圈,你会怎么设计?
-
用户朋友圈存储:
-
Hash 存储单条朋友圈(
user_id:post_id→{content, time, ...})。 -
Sorted Set (ZSET) 按时间排序用户的所有帖子(
user_id→post_id:timestamp)。
-
-
点赞/评论:
-
Set 存储点赞用户(
post_id:likes→{user1, user2})。 -
Hash 存储评论(
post_id:comments→{comment_id: {user, text}})。
-
-
Feed 流:
-
关注用户的 ZSET 合并(用
ZUNIONSTORE聚合好友动态,按时间排序)。
-
# 发布朋友圈 HSET user:1001:post:1 content "Hello" time 1710000000 ZADD user:1001:posts 1710000000 "1" # 点赞 SADD post:1:likes user:2001 # 获取用户最新10条朋友圈 ZREVRANGE user:1001:posts 0 9
redis中,zset用什么命令实现根据分值倒序排列?
-
倒序获取:
ZREVRANGE key start stop [WITHSCORES] -
倒序范围查询:
ZREVRANGEBYSCORE key max min [WITHSCORES]
什么是模板元编程?
模板元编程(TMP)是在编译期通过C++模板展开计算的技术,利用模板特化、递归实例化等特性,在代码编译阶段完成逻辑处理(如类型推导、数值计算)。典型例子:std::integral_constant、编译期斐波那契数列计算(通过模板递归实例化实现)。优点是提升运行时性能(计算提前完成),缺点是代码可读性差、编译时间长。
176万+

被折叠的 条评论
为什么被折叠?



