目录
<2.10.Go语言普通指针和unsafe.Pointer有什么区别?
<4.C/C++/Go中的并发控制组件都是怎么实现的?怎么使用?
[<4.8. I/O模型](#<4.8. I/O模型)
[<4.9. select/poll/epoll](#<4.9. select/poll/epoll)
[<5.5.TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?](#<5.5.TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?)
[<5.6.GET 和 POST 方法有什么区别?](#<5.6.GET 和 POST 方法有什么区别?)
[<5.15.UDP 和 TCP 有什么区别呢?分别的应用场景是?](#<5.15.UDP 和 TCP 有什么区别呢?分别的应用场景是?)
[<5.16.TCP 和 UDP 可以使用同一个端口吗?](#<5.16.TCP 和 UDP 可以使用同一个端口吗?)
[<5.20.什么是 SYN 攻击?如何避免 SYN 攻击?](#<5.20.什么是 SYN 攻击?如何避免 SYN 攻击?)
[<5.25.服务器出现大量 TIME_WAIT 状态的原因有哪些?](#<5.25.服务器出现大量 TIME_WAIT 状态的原因有哪些?)
[<5.26.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?](#<5.26.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?)
[<5.29.既然有了 HTTP,还要有 RPC 协议?](#<5.29.既然有了 HTTP,还要有 RPC 协议?)
<6.1.Redis有哪些数据结构?底层是什么?API?应用场景?
[<6.5.Redis 如何实现服务高可用?](#<6.5.Redis 如何实现服务高可用?)
[<6.14.如何用 Redis 实现分布式锁的?](#<6.14.如何用 Redis 实现分布式锁的?)
<6.16.发布/订阅(Pub/Sub)模式是什么,有啥优缺点?
[<6.17.如何优化 Redis 性能?](#<6.17.如何优化 Redis 性能?)
[<7.7.where 和 having 的区别?](#<7.7.where 和 having 的区别?)
[<7.9.MySQL中的bin log的作用是什么?](#<7.9.MySQL中的bin log的作用是什么?)
[<8.1.MQ如何保证的高性能?- - Kafka为什么这么快?](#<8.1.MQ如何保证的高性能?- - Kafka为什么这么快?)
1.C/C++
<1.1.代码的执行过程?
1、预处理:展开头文件/宏替换/条件编译(选择要运行的代码段)/去掉注释,生成test.i、list.i文件
2、编译:检查语法,生成汇编代码存到文件test.s、list.s(在这一步产生编译错误)
3、汇编:cpu看不懂汇编代码,在这一步要将汇编代码转成二进制机器码,产生test.o、list.o文件 汇编产生字段 将啥啥字(函数名、变量名)以块的形式处理
4、链接:将目标文件test.c和list.c链接到一起生成可执行文件
<1.2.全局变量、static、const解释一下
|-------|---------------|------------|------------|-----------|-----------|
| 特性 | 全局变量 | static全局变量 | static局部变量 | const全局常量 | const局部常量 |
| 存储区域 | 全局/静态区 | 全局/静态区 | 全局/静态区 | 常量区 | 栈区 |
| 生命周期 | 整个程序运行期间 | 整个程序运行期间 | 整个程序运行期间 | 整个程序运行期间 | 函数调用期间 |
| 作用域 | 整个工程(需extern) | 仅当前文件 | 仅函数内部 | 整个工程 | 函数内 |
| 是否可修改 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
| 跨文件访问 | 可以 | 不可以 | 不可以 | 可以 | 不可以 |
用extern来声明一个外部变量。在程序编译时不对该变量分配内存,链接时去外部找到该函数的定义,并执行;用extern声明一个外部函数时(比如在头文件中声明),可以隐式不加extern;
extern int x; 是声明,不分配内存,需依赖其他文件的定义。
int x; 是定义,分配内存(若多次定义会导致链接错误)。
static修饰类成员,该成员属于类,被每个对象共享,可通过类名::直接访问。
extern 使变量 / 函数具有外部链接属性(跨文件可见)。
static 使变量 / 函数具有内部链接属性(仅当前文件可见)。
<1.3.说一下C/C++中volatile
volatile用于阻止编译器优化;比如for (int i = 0; i < 1000000; i++) {} // 可能被完全移除;如果 i 用volatile修饰,该循环就会被保留;
编译器会先去从内存将数据加载到寄存器,如果它认为内存数据没有改变就会直接从寄存器读取数据,从而不会执行部分代码;比如:一个循环等待一个信号,编译器会忽略信号handler改变这个变量,从而优化成死循环....
//volatile在java中用于线程同步的原语,C++类比atomic
<1.4.说一下原码/反码/补码
- 正数的原码/反码/补码一致;
- 负数的 反码=原码符号位不变其它位取反,补码=反码+1;计算机存储负数补码利于用加法器计算加减法。
<1.5.说一下函数的重载、重写、重定义
- 函数重载:C++中支持函数名相同、参数列表不同的函数重载;(返回值是否相同都不影响)
- 函数重写:
<1.6.说一下C++中的多态
面向对象语言对比面向过程语言,三大特性(封装、继承、多态);其中多态是指同一行为有不同的表现行为即一个接口的不同实现;
多态从实现上分为动态多态和静态多态,静态多态指的是编译时多态(模板-编译时替换类型、方法的重载-同一函数名不同实现、运算符重载);动态多态指的是运行时多态,通过类的继承重写实现
多态可以增加代码的可扩展性、统一接口、降低代码耦合度;
<1.7.解释一下RAII
MyClass stackObj(100); // 这个对象直接在栈上
MyClass* obj = new MyClass(); // obj 是指针变量(在栈上),*obj 是堆对象(在堆上)
我们一般采用第一种创建方式(锁、智能指针)来使用变量,以达到用栈对象的析构函数管理对象中堆空间的释放,因为栈对象出了作用域会自动释放;而第二种new的方式只会销毁obj指针,它指向的MyClass堆对象没有释放,析构也调用,必须显式调用delete去析构,这种实现多余...
<1.8.STL有几大特性(STL源码解析)
STL有六大核心特性;
- **泛型编程:**编写与类型无关的代码。如:template模板编程、sort可接收不同类型的元素
- **容器:**用于存储和管理数据的数据结构。如:vector/List/deque/set/map/unordered_set...
- **算法:**用于操作容器中数据的函数模板。如:find/count/sort/reverse/equal
- **迭代器:**提供访问容器元素的统一接口。如:前向/反向/双向/常量 迭代器
- **仿函数:**行为类似函数的对象。operator()实现;
- **适配器:**修改容器或函数对象接口的包装器。如:栈/队列/优先级队列
<1.9.依赖、聚合、组合、继承的高内聚低耦合排序
<1.10.Go语言和C++语言最大的区别是什么?
- 内存回收:手动管理内存/自动GC
- 并发模型:多线程编程/多协程编程-内置并发
- 语法:复杂模板/轻量接口、复杂类实现/简单接口实现
<1.11.解释C++的强制类型转换
- static_cast<>:编译时检查类型相关性、是否合法,发现明显错误;
- dynamic_cast<>:运行时检查类型转换是否合法,比如:父类指针指向的子类对象,但此时需要访问子类对象的特有方法,就需要对父类指针进行转换,转成该子类指针,但如果有AB两个子类,该父类指的是A对象,如果进行B对象类型转换就会发生错误;
- const_static<>:无视变量是否是const,进行转化(添加或移除const属性时);
- reinterpret_cast<>:可以对不相关类型间进行重新解释,不检查;
<1.12.C++继承与多态
当基类指针指向子类对象时:
- 如果调用的是普通方法(如:子类重写父类方法),则调用的是父类的同名方法,根据指针类型而定,重写构成隐藏;
- 如果子类重写的是父类的virtual方法,就根据对象类型,调用对象方法;
- 不能通过基类指针调用派生类新增方法,要使用
dynamic_cast进行安全向下转型;
<1.13.说一下C++并发编程
- thread():thread t(func,a,b);t.join(),t.detach();当子线程没有引用主线程局部变量、相关资源,处理后台任务可以选择detach,否则在析构前必须detach,要不就会terminate;
- mutex:mutex mtx 初始化;lock(mtx)加锁;unlock(mtx)解锁;
- lock_guard:简单RAII管理,lock_guard<mutex> lock(mtx) 初始化并加锁;出作用域释放;
- unique_lock:RAII+灵活管理锁;unique_lock<mutex> lock(mtx,defer_lock)初始化锁-不加defer_lock即初始化并加锁;lock.lock()加锁;lock.unlock()解锁;出作用域时将持有锁析构;
- conditional_variable:conditional_variable cv 初始化;cv.wait(uniq_lock,函数条件),函数指针/lambda表达式/函数对象...;cv.notify_one() ,cv.notify_all()唤醒;
- atomic:atomic<int> atomic_int(0); // 初始化为0
<1.14.孤儿进程和僵尸进程
- 孤儿进程:父进程早于子进程退出,子进程被操作系统领养,称为孤儿进程;
- 僵尸进程:父进程还未退出,子进程已经退出但未被父进程wait/waitpid回收,大部分资源已经释放,但是占用Pid并且子进程的退出状态也无法感知;
< 1.15.右值引用、move、移动构造、移动拷贝
- 右值引用(&&):右值引用的值是临时的值,比如:函数的返回值、a+b、move(target)...
- 一般类内会实现移动构造和移动赋值,传参MallocRAII&& other,花销小于拷贝构造;
- 如果类实现了移动构造,在作为函数返回值的时候就会优先使用,没有才会使用拷贝构造;
<1.16. 说一下const和constexper的区别
- const:const int a = b;在运行时才初始化,运行时常量;
- constexpr:constexpr修饰的变量在编译器完成初始化,可以赋值给数组constexpr int x = 5;int array[x];constexpr修饰的函数调用在编译时就已经完成constexpr func();int array[func()]
放在编译时调用函数还比较省时,可用于构造编译期可知的对象;
<1.17.weak_ptr怎么解决shared_ptr循环引用的问题?
造成这个问题的本质是,指向该对象的指针已经销毁,但是由于该对象被其它对象内部的指针指向,而对方的对象指针也已经销毁,两个对象由于还被对方对象中的指针引用,所以引用计数不为零,析构不了,而析构不了这两个对象也导致两个指针无法销毁,一直循环引用,内存泄露;
可以将AB任何一个类中的只想对方的共享指针改成weak_ptr;
<1.18.封装、继承、多态三大特性解释
<1.19.bind()、lambda、function三者如何使用
<1.20.C++常见大厂面试题
https://www.nowcoder.com/discuss/810192080105455616
结尾
2.Golang
<2.1.如何分析程序的运行时间与CPU利用率情况?
- $ time go run test2.go;shell内置的time可以分析出程序总耗时、用户态耗时、内核态耗时;
- $ /usr/bin/time -v go run test2.go; 比起time更加详细,有CPU占用、文件IO、socket、进程切换情况、内存使用情况......
<2.2.如何分析golang程序的内存使用情况?
- **top -p** (pidof snippet_mem);在运行代码($go build -o snippet_mem && ./snippet_mem)后可以打开另一个窗口分析当前该进程内从占用;
- $ GODEBUG='gctrace=1' ./snippet_mem;在程序执行时添加godebug='gctrace=1'表明每进行一次垃圾回收汇总所回收内存的大小及耗时,并将这些内容汇总成单行打印到标准输出;
- runtime.ReadMemStats():在函数中调用runtime库的该函数,可打印运行到此处的内存调用
<2.3.说一下Go语言的GMP模型
P:维护一个G队列,供M调度;协程崩溃不会导致整个进程崩溃(有异常捕获),除非触发进程级错误(全局变量...);线程崩溃不写异常捕获函数,系统会调用terminate()函数让进程退出;协程的调度由调度器,抢占式也有时间片限制;
<2.4.说一下Golang的内存逃逸
- <1.引用类型变量名头部是分配到栈上的,出了函数,堆上数据被gc,栈上头部被销毁;
- <2.一般给一个引用对象的引用成员变量进行赋值就会发生; data := []interface{}(1,2);data[0]=1
- Golang中一个函数内局部变量,不管是不是动态new出来的,它会被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。上述两种情况都让编译器无法预计该变量真正的
<2.5.说一下Go语言中的引用类型变量
- silce、map、channel、func、*(指针类型)、interface
- interface{}:内部实现了两个指针,一个value(指向实际数值的指针),一个type(指向类型的指针);[]interface{}、struct XX interface{内部方法},实际上就是限制接收哪种类型的interface{};
interface可以接收多种参数; - s = append(s, 1, 2, 3);s1 = append(s1, s2);s1 = append(s1, s2...);不能用append将两个slice直接拼接,append()第二个参数只能接收多个int,必须将s2打散(s2...);
<2.6.Go语言中的var、make、new有什么区别?
- var:在栈上声明一个变量,初始化栈头为0值(如果没赋值);(int)0、(string)""、nil(切片);
- make:<1.给底层数据在堆上申请空间;<2.申请栈头,并初始化使其指向堆空间;只能用于silce、map、channel;
- new:func new(Type) *Type;调用new(),用于内存分配;传入要申请空间的类型,new申请空间后并初始化为0值,返回该类型的指针,指向这块空间;该空间指的是头部;

<2.7.Go语言中defer什么时候执行
- defer在所在函数的生命周期最后执行,多个defer先进后出的栈结构;
- 先执行return退出,再执行defer语句;
<2.8.Channel
|------------------------------------------------------|---------------------------------|
| lock | 保证并发安全的互斥锁 |
| qcount(缓存区已有元数), dataqsiz(缓冲区大小), buf(指向缓冲区指针) | 管理有缓冲 Channel 的环形缓冲区 |
| elemsize, elemtype | 描述 Channel 元素的类型和大小 |
| sendq, recvq | 管理因 Channel 操作而阻塞的 Goroutine 队列 |
| closed | 标记 Channel 的关闭状态 |
<2.9.两个interface的比较规则
- 当type和value都相等时两个Interface才相等;
- 当都为nil时才相等;比如说一个指针变量为nil和一个Interface为nil,两者类型就不相等;
<2.10.Go语言普通指针和unsafe.Pointer有什么区别?
普通指针比如*int、*string,它们有明确的类型信息,编译器会进行类型检查和垃圾回收跟踪。不同类型的指针之间不能直接转换,这是Go类型安全的体现。
而unsafe.Pointer是Go的通用指针类型,可以理解为C语言中的void*,它绕过了Go的类型系统。unsafe.Pointer可以与任意类型的指针相互转换,也可以与uintptr进行转换来做指针运算。
另外,通指针受GC管理和类型约束,unsafe.Pointer不受类型约束但仍受GC跟踪
<2.11.Go语言中多协程编程
- sync.mutex:默认模式(保证性能)、饥饿模式(保证公平);
- waitgroup:一个原子计数器和一个信号量协作实现协程等待
- sync.map:用两个map来实现;无锁read map用来读覆盖大多数情况,加锁dirty map用来改,当read map中未命中次数>=dirty map时,dirty提升成新的read,清空dirty;读写分离,以空间换时间;Java中有一款map是用的细粒度的桶锁;
<2.12.Go语言发生了内存泄露如何定位和优化?
- p-prof:通过go tool pprof...heap/goroutine 可以分析堆内存分布和goroutine泄露;
- trace工具:通过go tool trace可以看到协程的生命周期和阻塞情况;
- runtime统计:runtime.readmemstats()监控内存使用趋势、runtime.NumGoroutine()监控协程数量;
定位方法:如果内存异常上涨不回收,就用pprof工具分析哪个函数分配内存最多。如果是goroutine泄漏,会看到goroutine数量异常增长,然后分析这些goroutine阻塞在哪里。
优化:对goroutine加context超时控制,确保协程有退出机制,避免无限阻塞;
<2.14.如何排查死锁?
- 死锁发现:如果CPU占用率占用极低或者程序完全卡死、请求超时这些情况发生;
- 确认死锁:不同语言有自带的死锁排查工具,搜索deadlock;
- 阅读线程转储,找到blocked状态的线程(死锁的关键参与者),堆栈会显示它正在等待获取哪个锁,它当前已经持有了哪个锁;---pstack
- 然后修改代码逻辑,固定锁的获取顺序;加以使用超时机制兜底,如果超时没获取到所有锁,就释放占有的锁;
<2.13.云智一面面经
- 对于K8S有什么了解?
K8S是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。 - 关于Gin框架restful接口,有几十个接口,要针对这些接口做鉴权,怎么设计?
<1.JWT创建用户凭证(加密令牌),通过用户-角色-权限,将具体的API访问权限分配给角色
<2.用数据库(mysql)持久化存储用户信息、角色定义、权限列表以及它们之间的关系;
<3.用Gin中的use为不同分组添加认证、授权中间件进行放行;请求按照调用链执行;
<4.认证中间件:验证Token的签名和有效期,解析出用户、角色等信息存入上下文;
<5.从上下文拿到用户角色信息,确定访问当前http接口的权限,给该角色放行或返回403; - 了解过Gin的这些第三方中间件吗?
- 对于自己实现的rpc框架,在实际生产中还需要考虑哪些问题?
<1.可用性(见下一个问题回答)
<2.可观测性:为每个请求生成全局唯一TraceID,打印指标监控,打印日志;
<3.高性能:上下文控制防止大量请求超时;连接池复用连接+多路复用;高性能序列化协议;
<4.安全性: 如果用于生产级别,服务间的调用需要身份认证(如基于 TLS 双向认证、Token)和权限控制,防止内部服务被未授权访问;评估服务能承载的最大访问量,进行限流(令牌桶、漏桶),保护自身不被突发流量击垮;所有服务间通信使用TSL加密,防止数据传输的过程中被窃取或篡改; - 除了代理维护心跳去下线,如果说是因为服务能正常连接,但内部出错呢?
<1.客户端请求某个rpc节点失效,重试其它节点;(故障重试)
<2.zk客户端缓存可用节点列表进行轮询,watcher监听某个zk服务器探测到的异常节点进行下线,然后回调重新拉取列表;(服务发现与健康检查、负载均衡)
<3.客户端在调用某个节点失败时,在本地进行隔离,并向代理上报,代理发现超N个是上报就下线该节点;(熔断) - 如果你的服务被人恶意攻击,大量去调用你这个接口导致后端并发量激增,其它客户不可用,这个时候应该怎么考虑?
<1.web应用防火墙:
<2.IP黑名单与限流:
<3.设置API-QPS阈值:
<4.认证并对某用户ID限流:
<5.使用缓存拦截相同请求
<6.监控并告警 - 讲一下sigalflight的实现原理。
<1.mutex+map(key,call*);call(sync.waitgroup,interface{})
<2.协程一加锁访问map,根据key创建value为call,然后call.wg.add(1),放入map中,然后释放互斥锁执行接口调用;
<3.后续协程到达,加锁访问map发现key已经存在,对dups计数++,释放互斥锁,然后调用call.wg.wait()阻塞当前协程;
<4.协程一执行完毕,call.wg.done()唤醒所有wait,然后它们通过共享内存获取到call.val返回
<5.协程一等待其它协程读完,然后加锁对map进行清理,才最终会返回(第二次同步机制)
缺点:这 1000 个请求的协程全部阻塞在 call.wg.Wait() 上。如果这次唯一的下游调用因为网络问题慢查询(例如 2 秒),那么这 1000 个协程都将被阻塞 2 秒。服务协程池被瞬间打满,无法处理其他请求。---接口超时控制解决 - 讲一下set化架构是什么?
- 介绍一下golang中的interface接口的通用性。
- golang的继承和C++的继承有什么区别?
- zookeeper底层实现是什么?
<1.数据模型:树形结构+数据节点(一致性协议保证全局有序的事务ID)
<2.存储模型:内存存储+事务日志+快照
<3.分布式共识:多数派投票+两阶段投票来保障成功写入 - protobuf的底层实现是什么?
Protobuf 直接将数据序列化为二进制字节流,按照 Tag-Value 的格式;
<2.14.云智二面面经
- mongodb、mysql、redis这三种数据库的区别
- MySQL的一些命令:notify、添加索引、联合查询(按键联合)、or和in两种的区别
- redis用什么命令订阅?
- redis有哪些数据结构?
- redis的zset应用场景
- C++继承和多态的区别
- C++/Go怎么继承多个父类?
Go语言中通过struct{}定义类,可以通过组合嵌入的方式进行继承,C++必须显式继承并且多继承会有菱形继承的风险,Go多几次就是嵌入多个组合,遇见冲突时选择其一;
Go语言通过interface定义统一类的接口,其中包括方法签名但没有具体实现,哪个struct隐式实现该接口的所有方法,就继承了它;interface也可以进行组合; - 说一下C++并发处理
- 什么情况下会出现死锁?
- Go语言的并发机制是怎么实现的
- 无缓冲channel和有缓冲channel的区别?
- 怎么看待架构设计/微服务设计里,日志的作用?
- 除了我们自身的服务的注入以外,我们通过网关这种他其实都会做一个标准的这种这种框架的对吧?那我们能不能在框架里面去去追踪他的链路的一些信息呢?就是说在框架服务下面,其实我们更多的是要考虑它的链路,就是串行的这个这个过程是在哪一环出问题的。因为你单体的这种组件的话,我更多的是看单体服务的一个逻辑出问题了,对吧?嗯,微服务可能更注重的就是我在哪个环节出问题了,或者我在哪个组件出问题了,trace全链路追踪的设计
- 腾讯内部的蓝盾体系有了解吗?apm云商的产品有了解吗?
- Gin框架是怎么去实习一些通用请求处理的?
- 实习经历中的,本身缓存的数据的时效性是怎么解决的?
- 如果一个需求给到你,你怎么思考实效性的问题?
- 你是怎么看待ai对我们现在的工作的一个提效呢?
设计->编码->测试->调试->部署后 - 三个环节:编码、测试、离线交付后的持续运营(团队偏向这里)
- K8S了解吗?
- 如果服务出问题了怎么排查?
- 了解我们服务的产品吗?腾讯云、公有云、聚焦在K8S做一些稳定性保障,负责csig、teg两大云平台上的服务保障;
结尾
3.数据结构
<3.1.你了解哪些排序算法?
- **直接插入排序:**每次将新数和前n个有序数比较,从n-1进行swap,找到自己的位子;
- **希尔排序:**交换距离gap的两个节点,gap(n/3+1 ~ 2);
- **直接选择排序:**在数组剩余元素中选出一个最小元素放在最前面;
- 堆排序:<1.从最后一个非叶子节点到根节点调用向下调整算法构建一个最大堆(也是优先级队列的实现);<2.不断交换堆顶(已知最大)和最后一个节点(递减),最后得到一个升序数组;
- **冒泡排序:**在(1~n,n-1,n-2...)范围内,把最大的数沉底;
- **快速排序:**取出一个key,从左找一个大于key的元素,从右找一个小于key的元素进行交换,最后得到前半部分小于key,再交换key和begin/end的元素位置,后半部分大于key;再对前半部分、后半部分区间继续操作;
- **归并排序:**先使局部有序(两两有序),四四有序...最终全局归并有序;可以用来处理大文件排序,如果一个大文件过大内存无法加载,可以将它切分为多个内存可容纳的小文件,对这多个小文件进行排序,再对文件两两归并排序成中间文件...最后归并成有序的大文件;
<3.2.了解哪些常见的设计模式
- 单例模式:饿汉式-提前实例化好,有可能造成空间多余消耗;懒汉式-双重检查+锁机制获取创建并获取单例;对于频繁使用的对象,实例化耗费时间/资源(如数据库句柄),考虑使用;
- 工厂模式:
4.Linux操作系统
<4.C/C++/Go中的并发控制组件都是怎么实现的?怎么使用?
- **读写锁:**读线程加读锁,写线程加写锁即可;读写锁的加锁解锁实现是条件变量+互斥锁,读锁等待写状态true,写锁等待读状态count为0;读锁减为0时唤醒一个写线程,写状态变为true后唤醒所有读线程;
- **原子变量:**原子变量修改、查询是线程安全的,但有多步操作依旧不安全;原子变量不能直接被用来实现实现sem,就成了忙等待的自旋锁。
- **信号量:**信号量P操作--时如果为0就休眠等待,直到别的线程V操作会唤醒一个线程;
- **条件变量:**条件变量要结合锁使用,不满足条件时sleep,等待被唤醒。
- **互斥锁:**常用的互斥锁是休眠锁,获取不到就会休眠,等其它线程释放锁,系统会唤醒一个休眠的线程。
- channel: 无缓冲的channel在并发读写时,写协程先休眠,某个读协程会唤醒一个写协程进行通信;有缓冲channel多协程写入也是原子的,而多协程读取每个消息也只能被消费一次;channel的底层维护一个结构体(缓冲区容量/数据大小/缓冲区指针/元素类型/元素大小/close标识/发送阻塞链表/接收阻塞链表/互斥锁),互斥锁保证对内部资源的安全访问;
读/写一个nil channel;会永久阻塞
向已关闭的channel中写入会panic、读已关闭的channel,有数据读出,没数据返回一个零值
<4.1.进程和进程
线程是调度的基本单位,而进程则是资源拥有的基本单位。
线程共享相同的虚拟内存和全局变量等资源,私有栈和寄存器等(在上下文切换时需要保存)。
线程的三种类型:
用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;
轻量级进程(LightWeight Process):在内核中来支持用户线程;
Windows 和现代 Linux 都提供了高效的、一对一的线程实现,使得一个用户线程直接对应一个内核级的调度实体。
•Windows 的实现方式是原生的内核线程。
•Linux 的实现方式是轻量级进程(LWP),这是一种共享资源的特殊进程,但从调度和执行的效果上看,它与内核线程等价。
<4.2.进程间通信方式有哪些?
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
- 管道: 管道文件是特殊的文件,只存在于内存,不存于文件系统中。
匿名管道:ps auxf | grep mysql;单向、用完就销毁---父子/兄弟进程继承文件描述符f[0] f[1]
命名管道:mkfifo myPipe;向管道文件写入时,只有另一个管道读出才能退出---任意进程间 - **消息队列:**消息队列是保存在内核中的消息链表;数据大小有限制、通信不及时;
- 共享内存:共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
- 信号量: 信号量 < 0,则表明资源已被占用,进程需阻塞等待;
信号初始化为 1,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。
信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。 - **信号:**异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。(执行该信号的默认操作/捕捉信号-相应处理函数/忽略信号-但是SIGKILL和SEGSTOP不能忽略)
- **socket:**跨网络与不同主机上的进程之间通信;UDP/TCP
<4.3.怎么通过信号量实现写者读者模式?
cpp
void writer() {
while (TRUE) {
P(flag); // 1. 先抢公平钥匙(防止读者一直读)
P(wDataMutex); // 2. 再抢写数据钥匙(进入写数据)
write(); // 3. 写数据(此时没人能读或写)
V(wDataMutex); // 4. 还写数据钥匙
V(flag); // 5. 还公平钥匙
}
}
void reader() {
while (TRUE) {
P(flag); // 1. 先抢公平钥匙(和写者平等竞争)
P(rCountMutex); // 2. 保护读者计数器
if (rCount == 0) {
P(wDataMutex); // 3. 如果是第一个读者,抢写数据钥匙(阻止写者)
}
rCount++; // 4. 读者人数+1
V(rCountMutex); // 5. 释放计数器钥匙
V(flag); // 6. 释放公平钥匙(其他读者或写者可以竞争)
read(); // 7. 读数据(多个读者可同时读)
P(rCountMutex); // 8. 保护读者计数器
rCount--; // 9. 读者人数-1
if (rCount == 0) {
V(wDataMutex); // 10. 如果是最后一个读者,还写数据钥匙(允许写者)
}
V(rCountMutex); // 11. 释放计数器钥匙
}
}
<4.4.怎么避免死锁?
造成死锁的原因:双方都在等待对方释放锁。
使用资源有序分配法来破坏环路等待条件。就是双方获取锁的顺序一致。
<4.5.自旋锁和互斥锁的区别,选择?
当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。如果临界区代码执行时长小于线程切换耗时,就可以采用自旋锁。
只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。乐观锁全程并没有加锁,所以它也叫无锁编程。
<4.6.说一下select/poll/epoll的区别
多路复用/时分多路复用:一个进程分时处理多个请求;
select/poll/epoll 是如何获取网络事件的呢?在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。
- select通过系统调用select()接口,将socket列表拷贝给内核,内核遍历标注有事件发生的socket,再将列表拷贝回用户层,遍历后对有事件的连接进行处理;存储socket使用BitsMap,大小有限制,且需要两次拷贝、两次遍历;
- poll在select基础上采用了动态数组、链表的方式,突破大小的限制,但性能差不多;
- epoll调用epoll_create()在内核创建epoll对象,调用epoll_ctl()直接向内核中持久化的红黑树插入socket(便于连接到来内核快速定位),epoll采用回调机制将响应的socket插入到内核维护的就绪队列中,当用户调用epoll_wait(),就会返回就绪链表。epoll是解决c10k问题的利器!
- selcet、poll仅支持水平触发;epoll默认水平触发、但也支持边缘触发;epoll_wait()时水平触发只要缓冲区有未读完的数据就要一直唤醒,边缘触发缓冲区发生变化(如:数据到达),只会唤醒一次,之后缓冲区数据不会保留,所以用户必须读完缓冲区。
<4.7.说一下Reactor
线程池方案必须用一个主线程来轮询调用read()检查每个socket有没有事件发生,耗费CPU资源。
Reactor采用select监听解决上面问题;
单Reactor单线程存在无法利用CPU多核特性,以及单线程任务阻塞问题;
单Reactor多线程(handler交给线程池),存在瞬间高并发监听成为瓶颈。
<4.8. I/O模型
同步阻塞I/O: 用户态去调用read write,用户线程休眠被挂起,然后内核完成拷贝操作,完成后唤醒用户线程。
同步非阻塞I/O: 用户态调用read write,然后去干别的事情,内核进行拷贝,用户态通过循环的方式检查是否就绪。
异步非阻塞I/O: 用户态不调用 read write,利用一些接口让内核去调用read write,内核将用户态的I/O操作放在内核中等待执行的队列中,轮到它是内核去调用read write ,如果执行完成内核再通知用户态。
当然,调用read/write以及同步等待它调用完毕通知主执行流这个操作也可以在用户态通过开启别的线程实现。
<4.9. select/poll/epoll
**事件循环机制:**不论设计成阻塞还是非阻塞都是要循环监听的
cpp
while (1) {
// 同步等待事件
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);//-1表示阻塞等待
// 处理就绪事件
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
read(events[i].data.fd, buf, size); // 非阻塞读取
}
}
}
while (1) {
// 同步等待事件
int n = epoll_wait(epfd, events, MAX_EVENTS, 0);//非阻塞
if(){
// 处理就绪事件
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
read(events[i].data.fd, buf, size); // 非阻塞读取
}
}}
}
<4.10.互斥锁的使用有什么弊端?
- **性能开销:**锁存在竞争时,等待的协程会被挂起,唤醒挂起都涉及内核态用户态的切换(操作系统的调度);上下文切换的开销是非常大的;
- **并发度下降:**互斥锁的申请导致协程完全串行化,其它协程必须等待;
- **带来风险:**死锁、考虑锁粒度过粗、过细带来的问题;
- **如何优化:**原子变量、读写锁、sync.Map分区;如果一个全局字典需要高频访问,可以创建多个桶(例如256个),每个桶用自己的锁保护,根据key的哈希值决定放入哪个桶(sync.map);
<4.11.Linux常见命令
- ipconfig:
- ip:
- netstat:
- ping:
- ssh:
- top:
- nslookup:
- dig:
<4.12.DNS排查流程
5.计算机网络
<5.1.当键入网址后,到网页显示,其间发生了什么
1.1本机消息封装
-解析URL并构造请求报文:协议+域名+文件路径;浏览器默认采用Get方法;
-通过DNS将域名解析为IP:客户端(浏览器)先查本地缓存,没有再查操作系统缓存,仍旧没有则向本地DNS服务器发出DNS请求,如果缓存中有目标域名直接路由,没有则由根域名层层转发,然后拿到对于IP;
-先通过TCP三次握手确保双方服务可用及初始化双方序列号(双方先经过协议栈和网络链路发送SYN包建立连接)。ps:通过netstat -napt查看本机TCP连接状态
-如果HTTP头+数据大小超过MSS,TCP对数据进行分包并添加TCP头,在报头中添加源端口号(浏览器所监听的-随机生成)和目的端口号(根据不同的应用层协议)、序列号....
-TCP的连接、收发、断开都是依靠IP协议来传输数据包完成的。网络层会在数据包中加上IP报头(源IP和目的IP),如果客户端存在多个网卡,要根据路由表中的网段匹配目标IP,没有匹配的网段(比如公司内网...)就选择默认默认路由绑定的网卡作为源IP。
-根据源IP和目的IP可以判断出是否在同一子网,如果在就ARP广播目的IP获取到目标MAC,封装MAC头;如果不在同一子网,获取网关路由器的MAC地址,封装MAC头。
-物理层的网卡在数据包前添加帧的起始位置、尾部添加帧校验序列;再将数据包由数字信号转化为电信号通过网线发送出去;
1.2消息路由
-交换机是子网中连接多台主机的设备,根据MAC地址路由转发;路由器是连接多个子网的设备,根据IP地址路由转发;交换机内部有MAC地址和连接的网线端口的映射表,路由器中有网关和;---数据包先经由交换机根据MAC转发给同一子网的目标主机或者该子网的路由器网关。
-数据包到达路由器后,经过校验放到缓冲区,去掉MAC头部;查路由表并和每行记录的子网掩码&运算,如果有目标地址记录(即在同一网段)则通过该接口转发,如果没有则通过默认路由转发。
-路由表记录着四种路由:直连路由/静态路由(手动配置)/动态路由(学习得来)/默认路由;如果IP与直连路由在一网段说明找到目的子网,ARP广播用目的IP寻找MAC即可;如果是静态/动态路由,将其记录下一跳网关作为目的MAC进行转发;如果是默认路由交给上游进行路由寻址。
<5.2.内核态与用户态有什么区别?
|----------|------------------------------------------|----------------------------------------------------|
| 特性 | 用户态 (User Mode) | 内核态 (Kernel Mode) |
| 权限级别 | 低权限级。CPU 执行用户进程时所处的状态。 | 高权限级。CPU 执行操作系统内核代码时所处的状态。 |
| 访问权限 | 只能访问受限的内存空间和CPU指令集。无法直接访问硬件设备。 | 可以访问所有的内存空间、CPU指令集和硬件设备(如磁盘、网卡、键盘) |
| 执行环境 | 执行的是应用程序的代码(如浏览器、Word、你自己写的程序). | 执行的是操作系统内核的代码(如驱动程序、进程调度器、内存管理器) |
| 系统资源 | 不能直接分配系统资源(如内存、I/O端口)。需要向内核申请 | 负责管理和分配所有的系统资源(CPU时间、内存、硬件等) |
| 稳定性 | 一个用户进程崩溃,通常不会导致整个操作系统崩溃 | 内核代码如果出现严重错误(如驱动bug),很可能导致整个系统崩溃(如蓝屏、内核恐慌) |
| 虚拟地址 | 使用虚拟地址,并且每个进程都有自己独立的虚拟地址空间,互相隔离。 | 也使用虚拟地址,但拥有整个系统的全局地址空间,可以映射到任何物理内存 |
<5.3.HTTP常见状态码有哪些?
|-----------------------------|------------------------|---------------------------------|------------------------|
| 「100」 | 协议处理中的一种中间状态 | 「400 Bad Request」 | 请求的报文有错误 |
| 「200 OK」 | 成功状态码,表示一切正常 | 「403 Forbidden」 | 服务器禁止访问资源 |
| 「204 No Content」 | 成功码,响应头没有 body 数据 | 「404 Not Found」 | 请求的资源在服务器上不存在或未找到 |
| 「206 Partial Content」 | HTTP 分块下载或断点续传,返回资源的部分 | 「500 Internal Server Error」 | 笼统通用的错误码,服务器内部出错 |
| 「301 Moved Permanently」 | 永久重定向,说明请求的资源已经不存在 | 「501 Not Implemented」 | 客户端请求的功能还不支持 |
| 「302 Found」 | 临时重定向,说明请求的资源还在 | 「502 Bad Gateway」 | 服务器自身工作正常,访问后端服务器发生了错误 |
| 「304 Not Modified」 | 表示资源未修改 | 「503 Service Unavailable」 | 服务器当前很忙,暂时无法响应客户端 |
<5.4.HTTP常见字段有哪些?
host: 有了 Host 字段,就可以将请求发往「同一台」服务器上的不同网站。一台主机的某端口只能绑定一个服务(例如80端口绑定http服务),但如若该服务要托管多个网站呢?服务端的复杂路由逻辑对客户来说不太直观,所以采用域名的方式来区分客户端请求的不同服务。
Content-Length: 服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据长度。后面的字节就属于下一个回应了。
Connection: 最常用于客户端要求服务器使用「HTTP 长连接」机制,以便其他请求复用。最常用于客户端要求服务器使用「HTTP 长连接」机制,以便其他请求复用。
***Content-Type:***用于服务器回应时,告诉客户端,本次数据是什么格式。
<5.5.TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?
HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接。(从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive );同时为了避免资源浪费的情况,web 服务软件一般都会提供 keepalive_ timeout 参数,用来指定 HTTP 长连接的超时时间。------超时断开、防止连接空闲
TCP 的 Keepalive这东西其实就是 TCP 的保活机制,保活定时器超时发送报文收到确认则重置定时;对端进程崩溃内核会回收TCP连接发送FIN报文断开连接,但对方主机宕机或其他原因需要保活机制来清理异常连接。------异常断开、清理资源
<5.6.GET 和 POST 方法有什么区别?
|------|----------------------------------------|--------------------------------------------------|
| | GET(从服务器获取指定的资源) | POST(根据请求体对指定的资源做处理) |
| 请求参数 | 缀在URL后,但浏览器对URL长度有限制、URL只支持ASCII码的数据格式 | POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据 |
| 安全 | GET 方法是安全的,因为它是只读操作 | POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的 |
| 幂等 | 无论操作多少次,每次的结果都是相同的 | 多次提交数据就会创建多个资源,所以不是幂等的 |
| 是否缓存 | 会,浏览器因为其幂等安全的属性 | 不会 |
| 作为书签 | 在浏览器中 GET 请求可以保存为书签 | 不会 |
注意:幂等和安全是就语义来说,如果使用者违背语义用get去做修改操作就不会安全且幂等!
事实上,get和post都不安全,没有加密协议,数据都会在网络上裸奔!
任何请求都可以带 body 的。只是因为 RFC 规范定义的 GET 不需要用到 body。
URL 中的查询参数也不是 GET 所独有的,POST 请求的 URL 中也可以有参数的。
<5.7.HTTP的缓存技术有哪些?
强制缓存 和协商缓存;采用强制缓存则在浏览器缓存没过期时直接使用浏览器缓存,强制缓存过期则根据缓存生成的唯一标识发送给服务器比对,如果没有变化返回304表示请求的资源未被修改,那么继续采用浏览器缓存并更新字段,否则重新拉取资源。
<5.8.HTTP/1.1缺点有哪些?
无状态: 服务器不会记忆http请求的状态,每次请求都要重新校验,但是采用cookie的方式可以解决,在每次请求时携带服务器响应返回的cookie即可。(cookie指的是浏览器缓存,它一般会存:用户信息/sessionid/Token-加密的用户信息)
明文传输且不安全: HTTP/1.1是没有引入加密的,任意抓包既可以获取到,可能会有恶意篡改、拦截、伪造的中间者。
响应的对头阻塞: HTTP/1.1相较于1.0采用了长连接,也就是不用等第一条请求响应即可发送后续请求,但仍旧会造成响应队头阻塞,因为服务器必须按照接收请求的顺序发送对这些管道化请求的响应,如果由于网络问题某个请求未到达,后面响应都要等待。
服务器不能主动推送: 由http的语义设计,采用"一问一答"的请求-应答模式;当然也可以客户端定期轮询来拉取服务器新消息,效率较差;也可以升级成全双工的websocket协议。
**首部不压缩:**只压缩请求体,首部信息越多延迟越大,每次互相发送相同的首部造成的浪费较多。
<5.9.HTTPS解决了HTTP哪些问题?
HTTPS采用非对称加密+对称加密+签发证书,解决窃听、篡改、冒充风险。
如果采用对称加密 双方初始化密钥时会被窃取,静态方式内置到客户端之后被破解影响所有客户端的所有会话,如果客户端内置不同的密钥也会对服务器造成负担。
采用非对称加密+对称加密 解决初始化密钥,密钥经由公钥加密只能由私钥解密,那么无法确保客户端得到的公钥是否被掉包伪造;
采用签发证书 解决公钥被掉包的问题,将公钥注册到CR签发的证书中,证书包含(域名、公钥、数字签名)保障了证书无法被篡改、伪造。
先TCP三次握手、告知对方支持的加密组件、服务端发送证书、客户端生成密钥并用公钥加密...
<5.10.HTTPS一定可靠吗?
客户端会识别出中间人伪造的证书,只要不点击接受该证书!
HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全。
抓包工具可以捕获https消息的原理就是客户端在根证书列表导入抓包工具的证书。
<5.11.HTTP/2做了哪些优化?
Https基于Http1.1加了一层SSL/TLS层;HTTP/2在HTTPS的基础上作出性能方面的改进。
头部压缩: 如果同时发出多个请求,头一样或者相似,协议会消除重复的部分。
二进制格式: http1.1采用纯文本形式的报文,而http2采用二进制格式,增加了数据传输的效率。
并发传输: 基于TCP提出stream,一条TCP连接可以有多个stream,一个stream代表一次的请求-响应,通过streamId区分不同的请求-响应。
**服务器推送:**双方都可以建立stream,用奇和偶来区分;
**缺陷:**虽然解决了应用层的响应对头阻塞,但是传输层仍旧存在响应对头阻塞,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,若前面某个数据缺失重传/没有到达,后续数据也无法推送。
<5.12.HTTP/3做了哪些优化?
HTTP/3将传输协议换成了UDP,并采用QUIC协议,当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。
<5.13.什么是TCP?
作用:TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
<5.14.什么是TCP连接?最大TCP连接数?
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket (IP+Port)、序列号 (解决乱序问题)和窗口大小(流量控制)称为连接。
最大TCP连接数由每个进程打开文件的最大数(socket)、每个TCP连接占用内存限制。
<5.15.UDP 和 TCP 有什么区别呢?分别的应用场景是?
UDP包头只有源端口号、目标端口号、包长度(首部长度和数据长度)、校验和。
**区别:**连接、服务对象、可靠性、拥塞控制、流量控制、首部开销、传输方式、分片不同(MSS MTU);TCP常用于FTP文件传输/HTTP/HTTPS,UDP常用于广播、音视频、DNS(包总量较少)
<5.16.TCP 和 UDP 可以使用同一个端口吗?
可以。传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包;传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块;因此,TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。
<5.17.说一下TCP三次握手
- 服务端开始监听;状态由close -> listen
- 客户端发起请求,发送SYN报文,携带client_isn序列号;状态 close -> syn_sent
- 服务端收到并回复SYN+ACK报文,携带ack和server_isn序列号;状态 listen -> syn_rcvd;
- 客户端收到SYN+ACK报文,回复ACK报文携带ack序列号;状态syn_sent -> established;
- 服务端收到ACK报文;状态syn_rcvd -> established;(最后一次握手可以携带数据)
<5.18.为什么是三次握手?不是两次、四次?
TCP三次握手的目的是为了正确初始化双方序列号、Socket、窗口大小。
- 三次握手才可以阻止重复历史连接的初始化。旧的连接请求90因为客户端宕机/网络问题,客户端发起了新的连接请求100,但旧的90却又先到达,服务端会回复91向客户端,客户端收到91后依照上下文发现需要的是101,就会在第三次发送RST令服务端释放这个连接;如果没有第三次响应,服务端可能会维持很多与客户端的历史连接。
- 同步双方的初始序列号;TCP报文支持同时携带ACK和SYN,所以不需要四次
<5.19.第一/二/三次握手丢失了会发生什么?
- 第一次握手丢失,客户端迟迟收不到ack,就会超时重传,序列号保持不变;超时时间、重传次数都是写在操作系统中可以定义的;一般是每次超时时间是上次的2倍,重传次数是五次。
- 第二次握手丢失,客户端/服务端都会超时重传;
- 第三次握手丢失,服务端会超时重传;
<5.20.什么是 SYN 攻击?如何避免 SYN 攻击?
SYN攻击就是攻击者短时间内伪造不同IP的SYN报文,服务端响应SYN+ACK,但是不会收到ACK响应,久而久之就会占满半连接队列,导致无法提供服务。
- 调大 netdev_max_backlog(网卡缓存);当内核处理速度跟不上时,都放在网卡缓存中;
- 增大 TCP 半连接队列;
- 开启 tcp_syncookies;绕过半连接状态,第二次握手返回cookie,不再放入半连接;
- 减少 SYN+ACK 重传次数;快速排除无效连接;
<5.21.说一下TCP四次挥手
- 客户端向服务器发送FIN报文,状态由 establishe -> fin_wait1;
- 服务端收到FIN并回复ACK报文,状态由 establishe -> close_wait;
- 服务端向客户端发送FIN报文,状态由 close_wait -> last_ack;
- 客户端收到ACK回复报文,状态由 fin_wait1 -> fin_wait2;
- 客户端收到FIN报文并回复ACK,状态由 fin_wait2 -> time_wait;
- 服务端收到ACK断开连接,状态由 last_ack -> closed;
- 客户端等待2MSL,释放连接,状态由 time_wait -> closed;
<5.22.为什么挥手需要四次?
- 关闭连接时,客户端向服务端发送 FIN 时,仅表示客户端不再发送数据了但还能接收数据。
- 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送(所以不能向三次握手一样,将FIN与ACK一起发送),等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
<5.23.第一/二/三/四次挥手丢失了,会发生什么?
- 第一、二次挥手丢失了,都是第一次重传;
- 第三、四次挥手丢失了,都是第三次重传;
<5.24.为什么要有TIME_WAIT状态?
- 如果客户端最后一次ACK在网络中丢失了,但此时没有TIME_WAIT连接已经关闭了,那对于服务器重传的FIN将不再收到;
- 时长2MSL,确保一来一回的本次连接数据能够被消化,不影响下次连接。
<5.25.服务器出现大量 TIME_WAIT 状态的原因有哪些?
- http没有启用长连接,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。
- 大量的客户端建立完 TCP 连接后,很长一段时间没有发送数据,那么大概率就是因为 HTTP 长连接超时。
- Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。此时QPS较大的话,就会频繁关闭。
<5.26.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。一般都是由于代码逻辑错误。
<5.27.说一下TCP是如何保障可靠性的?
- 超时控制;缺失重传解决丢包问题。快速重传(收到相同的三个ACK)、SACK方法;
- 流量控制(滑动窗口);考虑到接收方处理数据的能力,不能让数据溢出对方缓冲区;
- 拥塞控制;考虑到网络拥塞,如果数据挤满网络也会影响后续发送;慢启动->拥塞避免算法->拥塞发生->快速恢复;
<5.28.IP地址有哪些分类?
|------|-----------------|-----------------|
| 类别 | 首字节范围 | 用途 |
| A | 1~126 | 超大型网络 |
| B | 128~191 | 大中型网络 |
| C | 192~223 | 小型网络(如局域网) |
| D | 224~239 | 组播(Multicast) |
| E | 240~255 | 保留(用于研究) |
| 环回地址 | 127 | 用于本机测试 |
| 默认 | 0 | 所有地址、默认路由、未分配 |
| 广播 | 255.255.255.255 | 在本网段内进行广播 |
由于分类受限,现在常用无分类地址 CIDR ;表示形式 a.b.c.d/x ,其中 /x 表示前 x 位属于网络号, x 的范围是 0 ~ 32,这就使得 IP 地址更加具有灵活性。将子网掩码和 IP 地址按位计算 AND,就可得到网络号。
子网掩码 可用于子网划分:
未做子网划分的 ip 地址:网络地址+主机地址
做子网划分后的 ip 地址:网络地址+(子网网络地址+子网主机地址)
私有 IP 地址 :这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。
公有 IP 地址是有个组织统一分配的,并且要在整个互联网范围内保持唯一(所有人可访问)。
<5.29.如何将一个UDP变成可靠的传输协议?
构建可靠性的四大核心支柱:
- 确认机制:让发送方知道接收方已经成功收到了数据包,成功接收返回ACK,如果某个序列号的包丢失,主动向发送方报告缺失的包。
- 重传机制:当发送方认为数据包可能丢失时,重新发送该数据包。(超时重传和快速重传-连续收到3个对序列号2的ACK,说明3丢失)
- 序列号:解决数据包的乱序、重复和丢包检测问题。
- 流量控制和拥塞控制:这是将"简单可靠"提升为"高效可靠"的关键;通过滑动窗口防止发送方发送的数据淹没接收方,通过拥塞控制算法防止发送方数据淹没整个网络;
实现步骤概要:
- 封装数据包:定义协议头(包含序列号、ACK号、标志位)等字段;
- 发送方逻辑:维护一个发送窗口,窗口内的包可以连续发送,设置定时器;收到ACK滑动窗口,并停止相应定时器;定时器超时或受到重复的包,重传;根据接收方窗口大小和网络拥塞状况动态调整发送窗口;
- 接收方逻辑:维护一个接收窗口,检查接受包的序列号并回复ACK,再交给应用层;
为什么已有TCP,还要实现可靠的UDP?
- 定制化与灵活性:牺牲某些特性换取更低延迟、可以设计更适合特定应用的拥塞控制算法;
- 无连接头阻塞:在TCP中,由于保证字节流顺序,如果包#2丢失,即使包#3、#4已经到达,应用层也无法读取它们,必须等待#2重传成功。这称为队头阻塞。在可靠UDP中,你可以选择将不同的数据流映射到不同的逻辑通道上,或者允许应用层先处理已到达的乱序包,从而避免这个问题。
- 多路复用与自定义路由:在一些P2P或复杂网络结构中,你可能需要实现自定义的连接管理和数据路由策略,基于UDP来实现更为方便。
<5.30.既然有了 HTTP,还要有 RPC 协议?

6.Redis
<6.1.Redis有哪些数据结构?底层是什么?API?应用场景?
- String: 底层的数据结构实现主要是SDS(简单动态字符串);和C语言的字符串有差异;
1.SDS不仅可以保存字符串,还可以存储二进制数据;
2.查询长度的时间复杂度是O(1),因为SDS有记录长度;
3.SDS有动态扩容机制,拼接字符串不会造成缓冲区溢出;设置/获取值
SET key value [EX seconds|PX milliseconds|NX|XX]
SET、GET、INCR(自增)、DECR、APPEND、STRLEN。
4.应用场景:缓存、计数器(文章阅读量、限流器)、分布式锁(SETNX) - List: 早期版本数据量小时是ziplist,大数据采用双向链表;之后采用quicklist(ziplist+双向)
LPUSH key value1 value2 # 左侧插入
RPUSH key value1 value2 # 右侧插入
应用场景:消息队列、最新消息排行(朋友圈动态) - Hash: 开始采用ziplist,然后采用的才是哈希;
HMSET key field1 value1 field2 value2
应用场景:存储结构化数据(如用户信息,每个字段可单独修改) - Set: 如果元素较少且是整数采取整数数组存储(二分查找);元素多或其他类型采用hash表
SADD key member1 member2
常用命令:SADD、SMEMBERS、SINTER(交集)、SUNION(并集)
应用场景:标签系统(文章标签)、共同好友(交际运算) - ZSet: 元素少时ziplist存value分数;多时双重存储hash(查询O(1)但不支持范围查询)+跳表(查询logn,支持范围查询);采用双重存储空间换时间;排行榜/延时队列
- ZADD key score1 member1 score2 member2
- -哈希表:哈希表中存储Key和评分,适用于O(1)复杂度去查member的评分;
- -跳表:同样存储分值和成员,如果查范围/查排名(span)都用跳表去查;
- ZADD key score1 member1 score2 member2
<6.2.Redis线程模型?
Redis是单Reactor单线程模型;epoll来进行I/O多路复用监听客户端,事件循环机制对消息进行处理;单线程模型避免了锁竞争、上下文切换、保证了原子性和缓存命中率;
Reids启动时开启三个后台线程去AOF刷盘、惰性释放内存、关闭文件(unlink/del区别);有此类事件放在队列里交给后台线程处理,不阻塞当前线程;
Redis在6.0以后采用多线程处理网络I/O(当网络硬件升级,平静出现在网络I/O);但执行命令仍旧采用单线程;
<6.3.Redis为什么那么快?
- Redis的数据存储都在内存中完成;Reids的性能瓶颈在网络带宽和机器存储,不在CPU;
- Redis采用了单线程模型,避免了上下文切换、锁竞争带来的时间消耗;
- Redis采用epoll多路复用监听处理大量客户端请求,更加高效;
<6.4.Redis如何数据持久化?
Redis有AOF追加和RDB持久化两种方式,Redis6.0之后支持混合持久化;
- **AOF:**支持执行操作后同步追加、每秒追加、定时操作系统处理缓冲区进行追加;并在数据量大时,fork子进程对每条数据进行重写生成新AOF,期间主线程执行新的指令采用写时拷贝修改内存数据,再追加到AOF缓冲区(全部的/定期清空)、重写缓冲区中(重写期间的);当重新完毕发送信号,主线程将重写缓冲区内容追加到新AOF文件后,并完成更名;
- **RDB:**AOF再恢复时需要执行命令耗费时间,二进制快照文件可以直接被加载;但是RDB是定期触发/手动触发,容易丢失期间的数据。
- **混合持久化:**发生重写时不再生成命令,而是直接生成快照...后面仍旧加载AOF缓冲区中的命令;但是对文件的可读性有影响;
<6.5.Redis 如何实现服务高可用?
- 主从复制:对于高并发请求,大多是读多写少的情形;采用一主多从模式可以将写请求到达主服务器,再异步复制到从服务器;对于请求量较大的读请求,分散在多台从服务器上。由于复制是异步的,所以主从可能会出现短暂数据不一致。
- 哨兵模式: 主从模式下,某服务器宕机/故障需要手动配置,所以Redis增加了哨兵模式。哨兵做到了监控主从服务器,并且提供主从节点故障转移功能。
1.哨兵集群(3个以上)对主节点定时PING,单个判断主观下线后发送给其它哨兵,大于2判断是客观下线后该哨兵充当leader进行主从故障转移;
2.leader在从节点中选取网络状况良好、复制进度快的从节点升级成新主节点;
3.通知其它从节点修改新主节点作为主节点;
4.将新主节点的ip地址和信息,通过「发布者/订阅者机制」通知给客户端;
4.监控旧主节点上线情况,一上线就修改它为从节点; - **切片集群:**将哈希槽分布在多个节点上,通过算法将数据映射到不同哈希槽,客户端保存每个节点对应的哈希槽范围,查询时进行路由。
- **集群脑裂:**原主库因网络状况不好被哨兵判断下线了,选举出了新主库,但原主库还一直和客户端交互,当原主库恢复和集群这边的连接时,向其它节点全量同步数据,但会丢失缓冲区的数据,解决方案可以在主库连接不到集群时,直接向客户端报错,不提供服务。
<6.6.Redis的删除过期键的策略?
- **惰性删除:**不主动检查,每次查询某键时,发现过期再进行删除。
- **定期删除:**定期从数据库中随机抽取一批键检查是否过期,进行删除。
<6.7.Redis内存满了如何淘汰数据?
- 内存满了直接不再提供服务;
- 对设置了过期时间的键值或者对全部键值,进行随机淘汰、LRU(每个redis对象结构体添加字段记录访问时间,随机取5/可配置个值,淘汰时间最久的),LFU(添加字段记录访问次数,淘汰字数最少的);

<6.8.Redis如何避免缓存雪崩、缓存击穿、缓存穿透?
- 缓存雪崩:当redis许多键同时过期,大量请求陷入数据库,导致数据库压力过大或直接宕机;我们可以采用随机设置缓存时间或者设置缓存永不过期(通过后台服务更新缓存);
- 缓存击穿:redis中某几个热点键过期,大量请求陷入数据库,导致数据库压力过大或直接宕机;当查缓存失败,查数据库时加锁(互斥锁/分布式锁)获取数据更新缓存,后续请求再判断缓存(双重查缓存提高性能),读取数据;热点数据不过期,后台服务更新;
- 缓存穿透:用户请求的数据即不在缓存也不在数据库,无法重建缓存;若查数据库为空将结果也存到缓存;采用布隆过滤器(Redis支持)映射提前数据库中的数据,过滤器查不到就返回;
<6.9如何设计一个缓存策略,可以动态缓存热点数据呢?
现在要求只缓存用户经常访问的 Top 1000 的商品:
Redis中维护一个Zset,用访问量排序;定期淘汰排名最后的200个数据,再从数据库抽取200个数据加入;定期维护1000的热点商品;
<6.10.说说常见的缓存策略?
- 先删缓存、再更新数据库:删除缓存后,在更新数据库的期间缓存读取旧值,缓存和数据库会出现长期数据不一致;
- 旁路缓存:先更新数据库、再删缓存:如果本来没有缓存,A请求去数据库拿旧值,B请求去更新数据库删缓存,A在B删缓存后更新缓存为旧值(不会出现,因为A的删除);旁路缓存策略适合读多写少,频繁删除缓存影响命中率;
- 应用与缓存交互、缓存与数据库交互:先读缓存,缓存有直接返回,没有就去查数据库来更新缓存,再返回给用户;写时先更新缓存,由缓存更新数据库;适用于本地缓存。Reids不提供与数据库交互的组件。
<6.11.如何保证缓存和数据库数据的一致性?
- 先更新缓存再更新数据库、先更新数据库再更新缓存:出现数据不一致问题(第二次操作乱序)
- 先删除缓存再更新数据库:并发写也会出现第二次数据库乱序,并发读写造成旧缓存污染
- 先更新数据再删除缓存:解决了上续问题,但又出现删除缓存操作有可能执行失败,导致旧缓存污染问题。此方案还存在缓存命中率不高的问题。
- 先更新数据再删除缓存,并对键设置过期时间,操作失败时短暂缓存污染,最终会被更新;
- 先更新数据库再更新缓存、并在更新缓存前加分布式锁,可以解决第二步操作乱序,缓存命中率也高,但是影响写入的性能;或给键加较短的过期时间,允许短暂的不一致;
- 如何保证两个操作都能执行成功?
消息队列重试机制。把删除操作给到消息队列,消息队列ACK机制确保它消费成功。
订阅 MySQL binlog,再操作缓存。

<6.12.Redis大key如何处理?
- 大key:string存储值超过10K,list/hash/set/zset存储元素个数超5000;
- 导致的问题:对大key操作时、阻塞线程,客户端也会等待过久;slot分片时,内存分布不均;大key如果是热键,造成网络阻塞;
- 在删除大key时,可用scan命令扫出部分分批删除,也可unlink异步删除;
<6.13.说一下Redis中的事务
Redis事务并不支持原子性和回滚,可能会存在执行部分成功。
- MULTI:标记事务开始,之后的命令将被放⼊队列,不会⽴即执⾏。
- EXEC:执⾏事务中的所有命令。
- WATCH:⽤于实现乐观锁,监控⼀个或多个键的变化,如果在事务执⾏之前,监控的键发⽣变化,事务将被取消。
<6.14.如何用 Redis 实现分布式锁的?
单节点Redis分布式锁(单节点宕机问题):
- 加锁:通过唯一value值区分不同客户端;采用set命令设置锁时设置NX选项,没有则创建,否则加锁失败;设置EX加上过期时间,防止客户端异常 锁一直被占用;
- 解锁:解锁时先要确认唯一key,再进行del,用Lua脚本保证原子性。
多节点Redis分布式锁(RedLock):
- 加锁:记录开始加锁的时间;开始向各个节点请求锁,超过半数成功后;计算开始时间差,要小于锁过期时间,加锁成功;
- 解锁:向所有节点发送释放锁;
<6.15.如何用Redis作异步队列?
- List:使用List的 rpush/lpush,rpop/lpop,用FIFO的队列实现生产者-消费者。
- Zset:使用Zset(有序集合),通过评分存储时间,实现对延时队列的处理;可以用zrangebyscore获取已到期的任务,适用于需要延时处理的任务;
- Pub/Sub:事件驱动(不按照程序顺序 按照外部事件发生顺序)队列,适用于事件驱动的异步任务处理;
<6.16.发布/订阅(Pub/Sub)模式是什么,有啥优缺点?
Redis 的发布/订阅(Pub/Sub)机制允许客户端发布消息到⼀个频道,同时多个客户端可以订阅该频道并接收消息。适⽤于实时通信、消息⼴播、事件通知等场景。
优点: 简单易⽤、实时性强、适合⼴播型消息、解耦系统。
缺点: ⽆消息持久化和确认机制、⽆法保证消息顺序、消息丢失、⽆法处理复杂的路由需求。
Pub/Sub 适⽤于轻量级的⼴播式消息传递场景,但对于需要⾼可靠性、持久化或复杂路由的应⽤,可能需要考虑使⽤更专业的消息队列系统(如 Kafka、RabbitMQ)。
<6.17.如何优化 Redis 性能?
- **使⽤合适的数据结构:**根据使⽤场景选择合适的 Redis 数据类型。
- **调整持久化策略:**根据需求选择RDB或AOF,或将⼆者结合使⽤,适当调整 AOF 重写策略。
- **合理配置内存:**使⽤适当的内存淘汰策略来避免内存占⽤过⾼。
- **开启持久化的异步模式:**AOF 持久化可设置为异步写⼊,避免每次写操作都等待磁盘同步。
- **使⽤ Redis 集群:**通过集群分布式部署来提升系统的吞吐量和⾼可⽤性。
<6.18.详细说说Redis为什么要引入集群?
- 解决容量瓶颈(数据分片):单个 Redis 实例的内存容量是有限的。即使服务器的物理内存很大(如 512GB),单个 Redis 进程也可能会因为巨大的内存分配和管理开销而变得不稳定。数据量无法超越单机内存上限。Cluster通过数据分片,对数据集划分给集群中的多个节点,实现了数据的水平拓展,突破单机内存限制。
- 解决性能瓶颈(负载分流):Redis是单线程模型,但其性能瓶颈最终会落到CPU和网络I/O上,所有读写请求都压在一台机器上;通过数据分片,请求也被自然地分流了;
- 解决可用性瓶颈:Cluster内置了主从复制(每个存储数据的主节点都可配置1-多个从节点)和故障自动转移(集群监测主节点宕机,将其从节点提升);
<6.19.Redis集群模式下客户端请求时有可能会产生哪些错误?
- MOVED错误-关键的重定向信号
触发场景:客户端向某一个节点查询某个键,但该键所属的哈希槽并不由这个节点负责;
根本原因:客户端缓存的集群槽位映射是旧的(集群刚重新分片);或者客户端第一次连接;
处理方式:收到MOVED后,先根据错误信息中正确的<ip:port>重新发送,再对本地槽位映射缓存相关信息进行更新; - ASK错误-迁移中的临时重定向
客户端查询的键被临时迁移,需要根据返回的<ip:port>重新发送,但不用更新槽位映射; - CLUSTERDOWN错误-集群不可用
当集群中超过半数的主节点不可达时,剩余的主节点会认为集群已失效,并进入 FAIL 状态,拒绝处理任何写命令和部分读命令。 - TRYAGAIN错误-资源不可用
在重新分片期间,针对多个键的操作(如 MSET),这些键属于同一个槽,但该槽正在迁移,导致操作无法原子性地完成。
7.MySQL
<7.1.执行一条SQL语句,期间发生了什么?
- 建立连接;连接器处理客户端连接;一般采用长连接(8个小时不使用会断开),由于长连接一直占用内存,所以Mysql最多维护150多个长连接;
- 查询缓存;解析SQL第一个字段,如果是selcet语句,就先去查询内存中的缓存,如果执行过该语句,就命中缓存。(mysql进行更新/插入操作时会删除缓存,mysql8.0版本之后去掉了缓存功能,对于更新频繁的表没什么用)
- 解析SQL;解析器检查SQL语法(from写成form报错),解析成语法树;
- 执行SQL;预处理SQL,检查查询的表是否存在;优化器主要负责将 SQL 查询语句的执行方案确定下来(选用哪个索引...);例:使用覆盖索引优化:
select id from product where id > 1 and name like 'i%';(使用name的普通索引,避免回表)
再由执行器和存储引擎交互进行查询;
<7.2.InnoDB为什么采用B+树索引结构?
hash虽然是O(1)的复杂度,但是hash不支持范围查询,因为hash无序;
对比二叉搜索树,B树的支持度大于2,降低搜索树的层数,查询效率更高;
对比B树,B+树的数据都存储在叶子节点上;

<7.3.有哪些索引?
聚簇索引、主键索引、唯一索引、单键索引、复合索引、前缀索引、
聚簇索引并不等于主键索引;当表中没有主键索引时,引擎选择唯一列,再没有就隐式生成ID列作为聚簇索引;一个表可以没有主键,但必须要有聚簇索引;
<7.4.怎么解决幻读?
- 可重复读隔离级别:
读操作(selcet):采用MVCC版本控制---当一个事务执行第一个快照读的时候,会生成read view(指向当前活跃事务数组的指针、最小活跃事务、下一个要分配的),如果介于min和max之间就会去查当前活跃事务列表,没有就说明已经提交(可读),有就说明读未提交(不可读);
写操作/当前读:能看见最新的数据库数据,如果进行操作(版本号被自己覆盖),业务会不一致(尽管数据库层一致);所以对于写操作/当前读要加行锁和间隙锁。 - 幻读:
对于没用到索引、全表查询、模糊匹配;间隙锁会失效,考虑串行化。 - 事务相比操作多加了间隙锁,平时的sql写操作只加行锁。保证原子性靠binlog
<7.5.如何优化Mysql连接?
使⽤连接池可以减少数据库连接的创建和销毁开销,常⻅的优化⽅法有:
- 设置合适的最⼤连接数( max_connections )。
- 使⽤连接池来复⽤连接。
- 使⽤⻓连接⽽⾮短连接。
- 设置连接超时时间。
<7.6.MySQL中的JOIN有⼏种类型?简要说明
- INNER JOIN:返回两个表中匹配的⾏。
- LEFT JOIN:返回左表所有⾏,右表没有匹配的⾏则为NULL。
- RIGHT JOIN:返回右表所有⾏,左表没有匹配的⾏则为NULL。
- FULL OUTER JOIN:返回两个表的所有⾏,没有匹配的⾏则为NULL。
- CROSS JOIN:返回两个表的笛卡尔积,所有可能的⾏组合。
<7.7.where 和 having 的区别?
- WHERE ⽤于过滤行(记录),它在查询执⾏的早期阶段应⽤,直接作⽤于从表中读取的数据。
- HAVING ⽤于过滤 分组后的数据。它在 GROUP BY 操作之后应⽤,⽤来对聚合结果进⾏筛选。 HAVING ⼦句通常 与聚合函数(如 COUNT() 、 SUM() 、 AVG() 等)⼀起使⽤。
<7.8.SQL的执行顺序
- FROM 2. WHERE 3. GROUP BY 4. HAVING 5. SELECT 6. ORDER BY
<7.9.MySQL中的bin log的作用是什么?
用于备份恢复、主从复制;有三种格式的日志:
statement:每一条修改数据库的SQL都会记录,主从复制中再根据SQL重现,但是像now这样的动态函数,每次执行的结果就不一样,会导致复制的数据不一样;
row:保留每一行更新的数据,记录行数据最终被修改成什么样了,但是每行数据的变化都会被记录,使得bin log文件过大;
Mixed:混合模式,从statement和row中选择合适的情况;
<7.10.MySQL死锁了,怎么办?
注意:update语句的where条件没有用到索引列,就会全表扫描;在一行行扫描的过程中,不仅给行记录加上行锁,还给行记录两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。
间隙锁和间隙锁之间是兼容的,所以select ... for update语句并不会因为该范围存在间隙锁就阻塞(间隙锁的意义只是阻止区间被插入);
可重复读隔离级别对修改操作和for update当前读都加行锁,当范围查时使用for update就会加上间隙锁,所以混用MVCC和for update就依旧会出现幻读;
- 产生死锁的必要条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件
- MySQL产生死锁的场景1:事务A更新id=1此行,持有了该行的行锁;此时事务B更新id=2,持有改行行锁;此时事务A又去操作id=2,事务B又去操作id=1;由于行锁会持有到事务结束,这两个事务就会一直阻塞下去;
- MySQL产生死锁的场景2:事务A加范围锁;事务B加相同/交叉区间范围锁;A向B范围插入数据,B向A范围插入数据;
- MySQL避免死锁:1.设置事务等待锁的超时时间,如果超时,就对该事务进行回滚,于是该锁就释放了,另一个事务可以继续执行;2.开启数据库死锁检测,主动死锁检测发现死锁后,主动回滚改动条数少的一个事务,让其它事务得以正常进行,
<7.11.常见SQL指令
- cross join,将A,B两表的每一行交叉M*N;且没有on(只有inner join、left join...有)
- datediff(A,B);计算A日期-B日期的天数,用在where子句
- round(x,y);表示对 x 取小数点后 y 位;
- create [unique] index index_name on table_name (clu1 [desc],clu2...)
<7.12.说一下MySQL的日志
- redo log:记录事务对数据页的修改,在系统崩溃后,通过redo log恢复未完成的事务;
- undo log:记录事务开始前的数据状态,用于事务回滚,保证原子操作;
- binlog:记录所有对数据库的修改操作,用于数据复制和恢复,支持主从复制;
<7.13.如何监控并优化慢SQL
- 启动慢查询日志:在配置中启用慢查询日志,并设置记录执行时间超过指定阈值的查询;
- explain分析查询计划:识别性能瓶颈;
- 优化索引:为慢查询涉及的列创建或优化索引,减少全表扫描。
<7.14.Mysql的存储引擎有哪些?
- InnoDB:支持事务、外键和行级锁定(消耗性能和资源),适合高并发写操作;默认存储引擎;
- MyISAM:不支持事务和外键,使用表级锁,但查询速度快,适合读多写少的场景;
- Memory:使用内存存储数据,速度快但服务器重启后数据丢失;
<7.15.InnoDB是如何存储数据的?
InnoDB通过表空间、页和行的结构化方式存储数据,将数据保存在磁盘上的数据文件中;
InnoDB有系统表空间和独立表空间,系统表空间用来记录各个sql表列、数据类型、主键等信息;独立表.ibd文件记录对应sql表,针对聚簇索引存储该B+树,来组织每一页(分根页、层页、叶子页)
行数据存储在叶子页中,维持双向链表的结构;如果还要再建索引,新的B+树也放在该独立表中;
InnoDB和磁盘文件交互,每次I/O通过页号*页大小定位到某字节读取一段内存,这样对磁盘文件建立了索引并且每次I/O一页16KB内容即高效又不会对内存造成压力;
<7.16.一条SQL在MySQL中的执行过程
- 连接管理。客户端通过连接器与MySQL服务器建立连接。
- 查询解析。SQL语句背发送到解析器,进行语法和语义检查并生成解析树。
- 查询优化。优化器对解析树进行优化并选择最优的执行计划。
- 执行引擎根据执行计划访问存储引擎,获取数据,将结果返回给客户端。
<7.17.MySQL中的数据排序(order by)怎么实现的?

8.消息队列
<8.1.MQ如何保证的高性能?- - Kafka为什么这么快?
高性能 == 高吞吐量、低延迟
- **批处理:**写盘批处理和网络传输批处理;不会每条消息就刷盘,而是先写到页缓存,积累到一定大小或时间时才会进行刷盘,减少磁盘 I/O次数;Producer发送消息和Consumer拉取消息时,MQ都会尝试批量进行,减少网络 I/O次数和系统调用开销;
- **顺序 I/O:**磁盘写入有顺序 I/O和随机 I/O两种方式,由于消息队列的有序性,可以采用顺序磁盘 I/O追加写的日志文件来存储消息,读通过offset进行顺序读;
- **零拷贝技术:**可以直接将数据从磁盘发送到网络套接字,不用到缓冲区再到用户态;避免了多次的数据拷贝,降低了内存和CPU开销,提高了传输效率;
- **主题分片:**将一个Topic分成多个Partition,实现水平扩展你和更高的并发,使得生产者可以并行地向不同的分区发消息;消费者从不同分区消费;
- **异步生产消费:**生产者将消息存入消息队列得到ACK,不用等待;消费者从消息队列取出消息根据当前处理能力决定消费速率,最大程度利用;
<8.2.消息队列的使用场景有哪些?
- **解耦:**多个系统可以通过网络直接调用改为经过消息队列,解耦了系统间的联系;就算一个系统挂了,也只是消息挤压在消息队列中无人消费而已,不会对其它系统造成影响;
- **异步:**对于系统调用中耗时且不需要等待结果的任务,可以全部交给消息队列完成,这样就加快了系统的访问速度,提供更好的用户体验;
- **削峰:**对于一个平时流量不高,但到了某个活动抢购时期就会涌入大量请求的系统;可以采用消息队列来削峰填谷,系统以最大能力来处理这些消息,给用户返回特定界面或者稍后通过其它方式通知其结果;
<8.3.消息重复消费怎么解决?
- 生产端避免重复生产:生产端可能因为网络抖动重复发送消息,而有些MQ框架会避免存储重复的消息,给生产端提供一个幂等的发送消息接口,存储幂等键;
- MQ避免重复存储:一些去重策略;
- 消费端避免重复消费:消费端消费+回复给MQ不是原子的,中间失败就会造成重复消费;所以只能依靠业务端,对于已经消费成功的消息,本地数据库表/Reids缓存业务标识;
<8.4.消息丢失是怎么解决的?
- 消息生产阶段:正确处理好异常情况,只要能正常收到MQ中间件的ack确认相应,就表示发送成功,所以只要处理好返回值和异常,进行消息重发,那么就不会出现消息丢失;
- 消息存储阶段:在使用时部署一个集群(Kafka),生产者在发布消息时,队列中间件通常会写多个节点,即使其中一个挂了,消息也不会丢失;将消息在多个主机上持久化;
- 消息消费阶段:消费者接收消息+消息处理之后,才会返回ack;如果只是在接收消息后返回,消费端主机宕机,就会造成消息丢失消费失败;
<8.5.消息队列的可靠性怎么保证?
- 消息持久化:像RabbitMQ可以通过配置将消息持久化进磁盘,通过设置消息+队列持久化的方式,这样在服务器宕机重启后,消息依旧可以被重新读取并处理;
- 消息确认机制:消息消费成功后,向MQ返回ACK....
- 消息重试策略:如果MQ没有收到ACK,在一定时间内会进行重传,达到一定次数仍旧失败,则将消息发送到死信队列中,以便后续人工排查或者采取其它特殊处理;
<8.6.消息队列的顺序性怎么保证?
- 有序消息处理场景的识别:首先要明确业务场景中哪些消息是需要保证顺序的。对于需要顺序处理的消息,要确保消息队列和消费者能够按照特定的顺序进行处理;
- 消息队列对顺序性的支持:部分消息队列本身提供了顺序性保障的功能。Kafka可以通过将消息划分到同一个分区,确保消息在分区内有序,保证分区间的并发,是性能与顺序保证的权衡
- 消费者顺序处理策略:消费者在处理顺序消息时,应该避免并发处理可能导致顺序打乱的情况。通过单线程/线程池对消息串行化处理,保障消息顺序消费;
<8.7.如何保证幂等写?
幂等性指的是 同一操作多次执行对系统状态的影响与一次执行的结果一致。(例如:支付接口因网络重试被多次调用,最终应确保仅扣款一次)
- 唯一标识(幂等键):客户端为每个请求生成全局唯一ID,服务端校验该ID是否已经处理;适用于场景接口调用、消息消费等;
- 数据库事务+乐观锁:
<8.8.如何处理消息队列的消息积压问题?
消息积压是因为生产者的生产速度,大于消费者的消费速度。遇到消息积压的问题,先排查是否有Bug产生,如果不是bug,先优化一下消费的逻辑,从单条处理消息改为批量处理消息;如果还是慢,我们可以考虑水平扩容,增加Topic的队列数和消费机器的数量,提升整体消费能力;
如果Bug导致几百万消息积压几小时,需要解决bug,临时紧急扩容:
1.修复consumer,恢复其消费速度,再将原consumer停掉;
2.因为一个消费者只能独占一个服务端的分区/队列来保证顺序性;所以需要创建应急Topic,将它的分区数量设置为原Topic的10倍;
3.编写一个临时的、无状态的Consumer程序,从原积压Topic中消费消息,不做任何复杂的业务处理(避免性能瓶颈),将消息均匀地(轮询)写入新的Topic的分区中;
4.同时,部署10倍数量的应急consumer,临时征用一批机器,部署修复好bug新版consumer程序;让这批consumer连接到新topic分区进行消费;
5.监控原积压Topic的堆积量,降为0时,逐步停止新Topic和consumer,连接到原topic;
<8.9.MQ如何保证数据一致性,事务消息如何实现?
MQ保证数据一致性,可以使用事务消息,在消息生产阶段多了等待生产者本地事务commit环节;
1.生产者产生消息后,发送一条半事务消息到MQ服务器;
2.MQ收到消息后,将消息持久化到存储系统,这条消息的状态是待发送状态;
3.MQ服务器返回ACK确认到生产者,此时MQ不会触发消息推送事件;
4.生产者执行本地事务;
5.如果本地事务执行成功,即commit执行结果到MQ;如果执行失败,发送rollback;
6.如果是正常的commit,MQ服务器更新消息状态为可发送,如果是rollback,即删除消息;
7.如果消息状态更新为可发送,则MQ服务器会push消息给消费者。消费者消费完就回ACK;
8.如果MQ服务器长时间没有收到生产者的commit/rollback,它会反查生产者;
<8.10.如何设计一个消息队列?
1.消息队列通过<1.consumerqueue索引文件和顺序数据文件<2.每个分区用一个文件,分区内有序来完成,零拷贝和写入时定期刷盘提高了写入的效率,读出时页缓存也提高了读的效率;索引文件保存各分区的偏移量;
2.broker和消费者的交互可以考虑使用zookeeper,进行一个轮询;获取到进行rpc调用;broker也发布成一个rpc方法,供多个生产者调用;
3.MQ消息积压时的扩容机制、消息可靠性保证(ack+死信)、消息重复处理(幂等键)、高可用设计、事务设计;
9.实习经历
<9.1.介绍一下你在腾讯的这段实习经历
我所在的项目组是负责研发公司的内容服务平台数据中台系统;
我负责开发日志文件查询模块、腾讯文档用户信息入库与查询模块、将监测系统采集到的异常资源维护到数据库中,并提供多表查询获取异常信息的接口、开发热文档模块
-
日志文件查询模块:
1.需求实现
在监测系统采集信息、流水线执行时,会产生很多日志文件,都被存储到了cos(腾讯云对象存储);用户在前端界面查看某页日志文件,分支路由前端已完成,我通过前端请求中的页码请求信息(第一次请求不携带页码,默认返回首页),以及URL,从cos中拉取该日志文件,通过计算返回该页内容;
2.性能优化
通过分析请求-响应到达时间(2秒左右),影响用户使用体验;排查发现:从cos上拉取文件接口响应速度过慢;设计缓存方案:单内存缓存方案(缓存多少行-过长行处理),考虑到日志文件大数量多,为提供缓存命中率会加重内存负担;所以采用内存缓存+硬盘缓存方案。
3.缓存方案设计
Map中最多维护20个文件列表[缓存数可以根据生产环境调整],请求到达时,根据url先查Map,存在并未过期直接获取文件名去加载文件,如果没有或过期则去cos拉取并更新缓存,返回给前端;由于同一日志文件用户会在不同页跳转查看,所以Map缓存策略是过期淘汰+超出文件个数进行淘汰,获取缓存时过期则进行删除重新拉取、更新缓存前缓存满了则排序清掉旧文件; -
腾讯文档人员信息入库与查询模块
1.需求描述
将存储在腾讯文档中人员与外包人员的信息写入数据库,并支持查询;
引入腾讯文档接口,设计Token获取、保存、刷新方案;批量从腾讯文档分页读取数据避免内存溢出;对数据进行处理后入到数据库中并建立索引,之后定期对腾讯文档自动更新到数据库(由于文档信息变动不大,全量更新未变更数据效率较差,记录上次的更新时间,再腾讯文档中筛选最新更新的数据);之后查询根据员工账号查询具体信息 -
异常信息监测入库(数据拆分)
这个需求我需要把上游监测系统采集到的每一条扫描的多条设备扫描结果(包含多条错误信息)的数据进行存储入库并提供查询API,便于资产设备组查看异常设备的相关数据;由于数据量较大(几十万条),字段较多(十几个),用一张表存储的话数据结构复杂,所以竖直切分成了三个表:对于唯一定位一次扫描任务的分支、分类、日期等字段用MD5算法生成唯一key,并统计此次扫描的每种错误类型的数量存入第一张表;第二张表不用再存储分类等字段,直接存储key即可,再用key和扫描批次用MD5生成唯一键resource_key,存储异常资源路径字段、大致错误信息;第三张表依旧存储resource_key和拆分的详细错误信息,这样也能支持查某一类错误等操作;
对数据拆分成三张表可以支持后续多种查询场景:需要查某次扫描的每种错误数量(表1)、如果之后在此表进行聚合查询就非常耗时;
查询数据库接口注意聚合第一张表的结果去查询(批量查询),逐个查询数十万数据查询会导致服务器卡死;在数据量太大导致查询慢可以考虑建立缓存。通过建立缓存,对于经常查询的分支首次查询12S左右响应,后续查询都在几十毫秒;(因为异常采集时间都是以天为单位,所以设置缓存过期时间可以是5-10Min,允许短暂数据不一致) -
热文档模块
需求设计
类似于我们平时打开博客时界面上会出现推荐阅读的博文,热文档模块就需要我们完成这样一个任务(展示每一条文档的浏览量、作者、更新时间、标题);而热文档列表是由后台人员上传,浏览量由配置浏览量+真实浏览量决定的;所以我们得提供两个接口:一个供运营人员配置的submit接口,另一个供用户读取的read接口;针对submit之后运营人员,并不用做高并发设计、针对read(用户打开该模块就会调用),所以必须考虑保护数据库;
针对业务场景,我采用的策略是:Redis+sigalflight+mysql,运营人员接口submit聚合提交操作和删除操作,更新方式采用全量更新,数据库upadate、缓存直接用set覆盖(热文档列表采用json存储);删除数据库时直接删除缓存就行;这样保证服务器是热启动的。而read接口,当用户获取热文档列表时会先去查缓存,缓存失效则去查数据库,数据库没有/有都会写回缓存,考虑到并发请求到达时获取的都是相同的热文档列表,我们就引入了signalflight组件来聚合一段时间窗口内的并发read,只放一个请求到达数据库,对数据库又加了一层保障,拿到热文档列表后,批量调用rpc服务获取各文档的真实浏览量-QueryBaseView(与配置求和)、文档的详细信息(标题、简介)、文档创建者信息(头像),考虑到多文档调用耗时(此处采用并发调用优化耗时),最终组合内容返回给前端。(需要再了解signalflight和并发调用这里)设计多级服务降级方案
1.用户请求先到Redis缓存;在submit时全量更新数据库和缓存(日后可拓展为用户行为定期更新),更新数据库失败直接返回error,更新缓存失败写入日志;99.9%的情况读缓存成功;
2.如果偶发redis缓存失效(误删...)则需要降级读数据库,此时用SingleFlight组件保护数据库,在第一个请求进入SingleFlight时间窗口,其余相同的请求等待它返回结果;
3.设置RPC超时时间,下游RPC服务响应慢或不可用(文档详情RPC超时:使用基础文档数据继续流程、浏览量RPC超时:浏览量设为0或使用配置浏览量、用户信息RPC超时:使用默认头像和用户名)
4.如果访问数据库失败或者接口超时,返回配置好的帮助文档;接口测试:
联调的时候可支撑几千QPS,配置40条热文档列表,响应时间在100MS内;
10.项目经历
<10.1集群聊天服务器
-
**网络架构:**采用Muduo网络库搭建服务器,Muduo采用多Reactor多线程的模型(主Reactor监听服务器socket,有连接事件后建立连接并将clientfd添加到子Reactor监听后续读写事件---每个Reactor都是一个独立的线程,交给线程池去处理任务)
-
聊天消息转发模块: 用户上线在本地维护OnlineMap(保存与客户端的连接),并订阅Redis通道(usrid),修改MySQL存储用户信息状态表,并用Redis记录用户在线状态作为缓存(如果用户在两个设备同时登录-低并发不考虑时间窗口的不一致),Mysql推送离线消息、好友列表、群组列表;给另一个用户发消息时,先查Redis看该用户是否在线,离线则将消息存入离线消息表,在线先查本地缓存进行转发,没有则发布到Redis中,订阅该channel的服务器收到后本地转发给该用户。
群聊消息转发模块: 获取该群聊的所有成员的信息,遍历用单聊逻辑处理。对于群聊模块也建立Redis缓存,key为groupid,value为所有群成员。
Redis设计: 创建两个上下文,一个用于发布消息和普通操作,另一个用来订阅并接收消息。因为同步订阅(rediscommand)会阻塞式等待接收消息,所以采用RedisAppendCommand追加命令的方式写入缓冲区,再调用RedisBufferWrite将缓冲区命令发送,不等待;另外专门开启线程阻塞式调用RedisGetReply对接收到的消息进行处理。cpp// 步骤1:发送 SUBSCRIBE 命令(非阻塞) redisAppendCommand(_subcribe_context, "SUBSCRIBE user:1001"); // 步骤2:确保命令已发送(非阻塞) int done = 0; while (!done) { redisBufferWrite(_subcribe_context, &done); } // 步骤3:在新线程中阻塞等待消息 void observer_channel_message() { while (redisGetReply(_subcribe_context, &reply) == REDIS_OK) { // 处理消息 } } -
业务模块
加好友/删好友业务:加好友是单向的;userid和friendid建立复合索引不唯一索引;
登录/注册业务:操作Redis、Map、Mysql
加群:groupname和userid,复合索引
创建群业务:id主键,groupname唯一索引;
离线消息表:不用考虑高并发的设计,因为只要客户端运行就可以接收消息并保存;可以应用层心跳检测客户端是否异常,也可以超时断开。
<10.2.手写Rpc框架
-
使用全流程
发布流程:callee写好proto文件,继承proto中方法实现去调用本地方法,发布服务;由于该服务继承实现了proto中的类和方法,调用provider(init)初始化Map并存储已发布的服务、方法对应其对象,调用provider(run)启动框架,将所有服务方法注册到zookeeper上,开启muduo提供网络服务,注册Onmessage回调;
调用流程:caller方写好proto文件,调用桩函数,传入channel类对象(callmethod服务发现),调用服务函数(req打包),解析传参获取返回值;callee的Muduo-OnMessage收到请求消息后解码,解析出请求的服务和方法,调用时传入req、res、newback回调,方法执行完后调用回调将res发送给caller; -
zookeeper:Wacther机制,保持心跳机制监测节点是否宕机,watcher回调会监听到节点是否宕机,父节点会受到子节点宕机的信息。
-
logger:直接硬盘写入会耗时,所以会先写到队列中,会再有线程从队列中读取去写到硬盘中;一个读线程、多个写线程,都用互斥锁控制。
-
1.protobuf
服务注册与发现:生成的 ServiceDescriptor 和 MethodDescriptor 对象(如 UserService::descriptor())被 RPC 框架用于自动注册服务和方法路由。
2.callee
在proto文件中先对服务/方法进行定义(编译生成类和描述器);依照proto中重写要发布的服务/方法(统一方法格式-便于后续传递/调用);
将服务发布在provider中(可以是多个服务),启动provider节点。
3.provider
初始化:提供Map<service_name,servie_info>, service_info{service*,Map<method_name,method_des*>),通过传入的服务对象(继承自google::protobuf::Service),可通过Service中的方法拿到Method,填入Map
启动服务:从配置中加载要ip+port,启动muduo网络库服务(基于reactor模型),注册连接和I/O事件回调,创建zookeeper节点,存储IP/PORT
OnMessage事件:收到客户端的请求事件,反序列化取出service_name、method_name、request(格式proto有定义),callmethod执行方法并绑定回调(处理返回值response,进行序列化并发送,关闭此次连接)
4.caller
用实现子类channel去初始化Stub类,在stub中调用method(底层是stub中的channel调用callmethod),读取response
5.channel
Callmethod:组织待发送的rpc请求的字符串,从ZK中解析出地址,连接并发送,解析结果
6.logger
由于日志I/O磁盘较慢,先将日志写入队列(由于有多个写入,竞争锁),用条件变量判断队列不为空时,读出来入到磁盘
<10.3.分布式聊天系统框架
-
加好友/加群/创建群/登录/注册/退出:主从架构;一台主机作为虚拟节点发布为rpc服务,通过zookeeper维护多个子节点(临时节点保障可用),service将任务轮询交给server处理;每台server就是独立运行(注册到zookeeper)-muduo库的网络模型,service阻塞式等待响应,done回调给调用方。
-
proxy:Muduo网络模型,接收客户端请求;根据请求类型调用不同的回调函数,调用对应的rpc方法,res返回给客户端,某一步返回错误在日志中可查;如果是登录/注册/退出需要操作Redis登记用户上线的服务器,并在本地Map维护客户端连接,跨机消息交给chatserver进行转发,chatserver本地缓存维护和proxy的连接;
-
一对一聊天:
-
群聊模块:获取群组成员依旧对该群组成员进行缓存,采用旁路缓存策略;
-
日志模块:发布成rpc方法,各模块可以调该方法进行写入,磁盘I/O较慢,考虑先写入一个队列中,写入元素封装成一个结构体,结构体中包含多个字段(路径、类型、内容);加互斥锁保证线程安全,后台启动一个读线程阻塞式读出进行磁盘I/O;读日志文件客户端接口则直接从文件读取;
-
1.UserService模块
UserService:虚拟服务节点,序列化协议采用protobuf,并发布成rpc方法;每一次请求到达,都会从zk更新当前可用的server节点到成员变量(service下的子节点),通过轮询从子节点列表获取socket转发。
UserServer:Muduo网络库+protobuf序列化;提供三个服务 登录/注册/注销
ZooKeeper:在启动service服务时就创建并维护状态;添加server节点时,zookeeper是秒更新,并通过超时心跳判断节点是否宕机,保证服务可用。
缺陷:因为某个server可能是新上线的,所以轮询方式不如最小负载合理。
2.ChatServer单聊模块
ChatServer:转发服务;收到(接收方id,msg),在redis缓存中用id查找host,不存在则直接将离线消息存入离线消息表中,存在则继续查看本地内存缓存;没有则添加进本地缓存并建立连接,通过socket转发该消息(如果该客户端与其它server保持连接呢?多个server转发时都会在本地去连该客户端?这个设计不太美妙)
也就是说Redis维护的是全局在线用户所登录的服务器ProxyService(userid,host),ProxyService中Map维护的是当前主机的在线用户(userid,conn),ChatServer中的Map维护的是与ProxyService的连接(host,socketfd)Tcp长连接不断开
3.ChatServer群聊模块
由于多人参与群聊,频繁拉取某群所有群成员信息会加重数据库I/O,所以添加Redis缓存,先从缓存读取,每次有人加群时删除redis即可。之后在获取群成员时先读redis再读数据库,这里的业务逻辑放在groupserver实现。群聊模块直接调用rpc服务获取群成员即可。然后再遍历群成员调用单聊模块处理消息即可。
4.日志模块
写日志模块发布成Rpc方法,供其它节点调用写入(发现Bug:对于文件写入未加锁导致乱序,考虑采用mutex,因为rpc多线程模型调用的是同一实例,互斥锁可以完成;如果跨进程对文件操作,要考虑文件锁)在不同节点下打开的是不同文件倒没必要加锁;
读日志模块还是使用的Muduo做网络部分,限制每次从文件读的大小,分块返回并添加序列号防止乱序。
11.系统设计题
<11.1设计百万级别榜单系统,实时排出前100名(考虑该赛季只加分不减分)
1.数据源与写入路径(处理高并发)
消息队列:业务逻辑层在处理完核心业务后,向Kafka或RocketMQ发送一个积分变更事件。这一步是解耦的关键,将高并发的同步写转化为异步的、削峰填谷的流。
2.榜单计算层:
- 本地内存窗口:Worker节点在内存中维护一个短时间(如1-5秒)的Map<UserId, ScoreDelta>,对这段时间内同一用户的积分变更进行聚合。这能极大减少对下游存储的操作频率。
- 窗口结束时,对这个窗口内的增量用户按聚合后的分数进行排序,取出本批次的 TopK(>100)
- 从Redis的全局ZSet中,取出当前的Top M(M可以取150,因为可能有些用户不在当前增量列表但排名靠前)
- 将这两个列表(当前内存Top K + Redis中的Top M)在内存中合并,计算出新的全局Top 100。这个合并操作的数据量很小(最多K+M条),速度极快。
3.存储层(Redis的Zset存全量+MySQL持久存储)
- Redis:
- MySQL:
4.查询路径(针对高并发读):
<11.2.讲一下限流算法
https://www.nowcoder.com/exam/interview/93214233/test?paperId=63848228&order=0