主题:最新大厂C++面试真题合集,大厂面试百日冲刺 day6
闲鱼后端
TCP 和 UDP 的区别
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,它保证数据正确性和顺序。UDP(用户数据报协议)是一种无连接的协议,提供快速、尽最大努力交付的传输方式,但不保证数据包的顺序或可靠性。简而言之,TCP强调可靠性,而UDP强调速度。
TCP 保证可靠性的机制有哪些
可靠性的主要机制:
-
三次握手:建立连接前的同步过程,确保双方准备好进行数据传输。
-
序列号和确认应答:每个TCP包都有序列号,接收方回送确认包告知发送方数据已接收。
-
数据重传:丢失或错误的数据包会被重新发送。
-
流量控制:使用滑动窗口机制,调整发送速率以匹配接收方的处理能力。
-
拥塞控制:根据网络状况动态调整数据发送的速率,防止网络拥塞。
-
数据校验:使用校验和检测数据在传输过程中的任何变化。
滑动窗口的原理是什么?如何保证消息的顺序?
滑动窗口原理是分配一个固定数量的序列号给发送方的未确认消息(即窗口大小),控制发送方在收到确认之前可以发送的消息总数,从而调节发送速度。为了保证消息顺序,接收方按照消息的序列号顺序处理消息,不按序到达的消息会被缓存起来,直到缺失的消息补齐后再一起处理。这个过程确保了即使消息在传输过程中出现乱序,也能在最终处理时恢复正确的顺序。
HTTPS 用的对称加密还是非对称加密?原理?建立连接的过程?
HTTPS 使用非对称加密来建立安全连接,然后使用对称加密来传输数据。
原理是:
-
浏览器请求HTTPS连接并获取服务器的公钥。
-
浏览器验证服务器证书的有效性(由证书颁发机构签名)。
-
使用非对称加密,浏览器生成一个对称加密的密钥(会话密钥),并用服务器的公钥加密它,发送到服务器。
-
服务器使用自己的私钥解密,获取对称会话密钥。
-
双方使用这个会话密钥进行对称加密通信,确保数据的安全传输。
你了解的网络攻击方式有哪些?SYN攻击的防范方法?
网络攻击方式包括:
-
DoS/DDoS攻击:通过大量请求耗尽目标资源。
-
Man-in-the-Middle攻击:攻击者拦截和篡改双方通信。
-
Phishing:通过假冒网站或通信骗取用户信息。
-
SQL注入:在数据库查询中注入恶意SQL代码。
-
跨站脚本(XSS)攻击:在网页中注入恶意脚本。
-
Malware:使用恶意软件进行攻击。
SYN攻击(一种DoS攻击)防范方法包括:
-
SYN Cookies:仅在确认客户端响应后分配资源。
-
包过滤:过滤掉异常的或非法的SYN包。
-
连接限制:限制打开SYN请求的速率或数量。
-
超时设置:降低半开连接的超时时间。
epoll、poll、select 区别?epoll 为什么高效?
-
select:它使用位掩码来表示监视的描述符集合,但最大能处理的描述符数目有限制(通常是1024)。
-
poll:它通过一个链表来存储网络描述符,解决了最大描述符限制问题,但每次调用poll都需要遍历整个链表,效率较低。
-
epoll:可以理解为变种的poll,它与poll类似,都是使用链表来存储网络描述符,但每当有事件触发时,它只需要获取已经就绪的事件就可以返回了,因此处理的事件少则效率越高。
epoll之所以高效:
-
回调机制:epoll使用回调的方式,只关注活动的socket,不必每次都轮询所有的被监听的socket。
-
scalability:epoll具有较好的扩展性,可以处理的socket描述符数量比select和poll都要大。
-
减少用户和内核空间数据拷贝:在触发方式下,epoll仅使用必要的数据拷贝,降低了内核与用户空间之间复制数据的次数。
MySQL 支持的隔离级别?最常用什么?
四种事务隔离级别:
-
READ UNCOMMITTED(读未提交)
-
READ COMMITTED(读已提交)
-
REPEATABLE READ(可重复读)
-
SERIALIZABLE(串行化)
最常用的隔离级别是 REPEATABLE READ(可重复读),因为它平衡了性能与一致性,同时也是MySQL的默认隔离级别。
MySQL 索引是主要通过什么数据结构实现?为什么用 B+ 树?
MySQL索引主要是通过B+树数据结构实现的。
原因:
-
高效的读写:对于数据库读写操作频繁的场景,B+树的平衡性能较好,读写性能稳定。
-
范围查询优势:B+树支持范围查询,hash表只支持精确查询,而二叉树效率低下。
-
磁盘读写优化:B+树的节点大小通常和磁盘扇区大小相同,这样可以最大化磁盘I/O的效率。
-
减少磁盘I/O次数:B+树的分支因子大,树的层级较低,可以减少在查询过程中磁盘I/O的次数。
虚函数实现机制
C++中的虚函数通过虚函数表来实现。每一个使用虚函数的类都有一个对应的虚函数表,其中存储了指向类的虚函数的指针。每一个对象实例包含一个指向其类的虚函数表的指针(vptr),当调用一个对象的虚函数时,会通过这个指针在虚函数表中查找相应的函数实现进行调用,从而实现多态性。这种机制允许在运行时根据对象的实际类型来动态绑定方法,而不是在编译时。
构造函数和析构函数能否声明为虚函数
构造函数不能声明为虚函数,因为构造函数是在对象创建时调用的,此时虚函数表还没有建立,无法支持虚函数的调用机制。
析构函数可以且在某些情况下应该声明为虚函数,特别是在基类中,这样可以保证通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免资源泄漏。
const 和 define 区别
-
const定义的常量有数据类型,而define是预处理器指令,替换文本,没有数据类型。
-
const有作用域限制,define一直到文件结束或者被#undef指令取消。
-
const常量通常存储在程序的数据段中,define只是简单替换,不分配存储空间。
-
const常量在调试中可见,因为它们是符号化的,而define替换后的代码通常难以调试。
-
const是在编译阶段使用,define发生在预处理阶段,编译器还未参与。
struct 和 class 的区别
struct和class的主要区别在于默认的访问权限和继承类型:
-
默认访问权限:struct中的成员默认是公开(public)的,而class中的成员默认是私有(private)的。
-
默认继承类型:使用struct继承默认是公开(public)继承,而使用class继承默认是私有(private)继承。
昆仑万维开发
4层和7层负载均衡的区别
4层负载均衡工作在传输层,主要关注IP地址和端口,使用TCP/UDP协议进行流量分发。而7层负载均衡工作在应用层,可以处理更高级的协议如HTTP/HTTPS,并且可以基于URL、cookies等因素来进行智能分发。总的来说,4层负载均衡更关注网络层面,7层负载均衡则提供更细粒度的流量管理能力。
你了解哪些负载均衡算法
负载均衡算法主要有以下几种:
-
轮询:请求按顺序轮流分配到不同的服务器上。
-
加权轮询:类似轮询,但服务器有不同的权重。
-
最少连接:请求被发送到连接数最少的服务器。
-
加权最少连接:考虑服务器权重和连接数,选择最优服务器。
-
源地址哈希:根据请求源地址的哈希结果分配请求。
-
URL哈希:根据请求的URL哈希结果来分配请求。
-
最少响应时间:选择响应时间最短的服务器。
-
最少带宽:选择当前带宽消耗最少的服务器。
brpc 和 grpc 对比
brpc支持多种协议,如HTTP、HTTPS、HTTP2、Thrift等,而gRPC主要基于HTTP/2。
gRPC官方支持更多的编程语言,包括C++, C#, Dart, Go, Java, Kotlin, Node, Objective-C, PHP, Python, Ruby,而brpc主要集中在C++支持上。
gRPC提供的功能更全面,包括流控、身份验证、双向流等。
是否在项目中用过 tcmalloc、jemalloc
tcmalloc是Google的一种内存分配器,相比于glibc的默认分配器,它拥有更高的性能与更低的内存碎片,这对于那些频繁进行小块内存分配释放的应用来说非常有用。
而jemalloc则是Facebook开发,设计出来专门用于处理多线程扩展性问题的内存分配器。它同样也比默认的glibc分配器在某些场景下拥有更好的表现。
malloc 的底层原理了解吗,涉及到哪些系统调用
malloc是标准C库中用于动态内存分配的函数。其底层原理涉及到以下几个系统调用:
-
brk:改变数据段的末尾地址,用于增加或减少数据段的大小。
-
sbrk:增加进程的程序断点,实际上是对brk的封装。
-
mmap:创建一个新的映射区域,用于分配大块内存。
malloc通常是通过上述系统调用来向操作系统请求内存,上层的内存分配器使用这些系统调用来管理内存的分配和释放,以及维护内存的数据结构,如空闲列表或者内存池等,来优化内存的使用和减少内存碎片。
mmap 的原理
mmap是一种将文件或者设备的内容映射到进程的地址空间的机制。其基本原理如下:
-
地址空间映射:mmap通过创建一个文件或设备内容与进程虚拟地址空间之间的对应关系,允许进程直接访问这片内存空间。这意味着对这部分内存的读写直接反映到了被映射文件或设备上,无需通过read或write系统调用。
-
延迟加载:mmap采用需求分页的方式,实际内存页只有在访问时才被映射到物理内存中,这代表mmap可以有效管理和使用内存,减少不必要的物理内存占用。
-
共享与私有:mmap支持共享映射和私有映射。共享映射允许不同进程共享同一片内存区域,对这片区域的修改会反映到所有映射了该区域的进程中。私有映射则为每个进程创建映射的副本,进程间的修改互不影响。
-
页表:操作系统利用页表来管理这种映射关系,确保进程可以通过虚拟地址访问到正确的物理内存或文件内容。
string 的底层实现、vector 的底层实现
string的底层实现:
-
动态数组:string在底层实际上是一个动态数组,用于存储字符序列。
-
自动扩容:当字符串内容超过当前分配的内存时,string会自动进行扩容(通常是倍增策略)并拷贝原有数据到新的内存区域。
-
短字符串优化(SSO):对于短字符串,为了减少动态内存分配的开销,string会在对象内预留一小段空间,直接在其中存储字符数组和结尾的空字符’\0’,避免了堆分配。
-
复制写时优化(COW,现代C++标准中已废除):在一些旧的实现中,多个string对象可以共享同一个字符数组,只有在修改时才会进行真正的拷贝,以减少内存使用和提升效率。
vector的底层实现:
-
动态数组:vector内部同样使用动态数组来存储数据,它支持随机访问。
-
自动扩容:当插入的元素超过vector当前的容量时,它会自动重新分配更大的内存空间,并将原有元素复制或移动到新的地址。
-
空间预留:vector提供reserve方法,允许手动指定容量,减少不必要的重新分配和数据拷贝。
结构体内存对齐相关
结构体内存对齐是为了提高CPU访问效率而采取的方式。
-
对齐规则:结构体的每个成员变量在内存中的起始地址应该是其类型大小的整数倍(例如,int通常是4字节,它的地址应该是4的整数倍)。
-
结构体总大小:结构体的总大小为其最大成员类型大小的整数倍。如果不满足,会在结构体末尾进行填充。
-
按序排列:结构体成员变量之间可能存在填充(padding),以保证后续成员的地址对齐。
-
pragma pack(n):可以通过#pragma pack(n)来指定结构体以n字节对齐。n一般为2的幂次。
sizeof 一个 string 变量 的结果
sizeof 一个 string 变量通常返回的是 string 类型固定的大小,而不是字符串内容的长度。这个大小包括 string 对象中管理字符串所需的所有成员的大小,如指针、长度和容量等,具体的大小取决于实现(例如具体的编译器和其版本)。在某些实现中可能会有短字符串优化(SSO),在这种情况下,小的字符串可以直接存储在这个固定大小的内存之中,而无需动态分配。
std::move,应用场景 和 std::forward 区别
std::move 应用场景
-
主要用于将对象标记为可移动,使之可以被移动构造或移动赋值函数使用。
-
应用于转移资源的所有权,避免不必要的复制,例如移动大型容器或独占所有权的对象(如std::unique_ptr)。
std::move 和 std::forward 的区别:
-
std::move 无条件地将一个对象转为右值引用,从而允许资源的移动操作。
-
std::forward 用于完美转发,在模板函数中,它可以保持参数的值属性(即左值保持左值,右值保持右值),通常与右值引用模板参数结合使用。
左值和右值
左值(Lvalue)是指表达式结束后依然存在的持久对象,通常能出现在赋值语句的左边。
右值(Rvalue)是指表达式结束就不再存在的临时对象,主要用于初始化左值,常出现在赋值语句的右边。
左值可以取地址,而非临时的右值不可以。
移动构造函数、复制构造函数的区别
复制构造函数
-
创建一个对象的副本。
-
完成成员逐个复制(深复制)。
-
原始对象和新对象都拥有自己的独立资源。
移动构造函数
-
通过接管另一个对象的资源来创建对象(资源转移)。
-
执行“浅复制”,只是将资源的所有权从源对象转移。
-
源对象不再拥有这些资源或处于有效但未定义的状态。
push_back 和 emplace_back 对比
push_back
-
接受一个元素作为参数,并将其复制(或移动)到容器末尾。
emplace_back
-
使用所提供的参数直接在容器末尾构造元素。
-
避免了额外的复制或移动操作。
push_back 可以传入 std::move 的变量吗
push_back 可以接受通过 std::move 传递的变量,这样可以将变量的资源移动到容器中,而不是复制。
push_back 一个 std::move 的变量 与 直接 emplace_back 性能对比
push_back一个std::move的变量和直接使用emplace_back的性能是相似的。因为它们都是在进行移动操作,而非复制操作。
shared_ptr 以引用形式传入参数会有什么问题
将shared_ptr以引用形式传递不会增加对象的引用计数,这可能会导致以下问题:
-
若函数内部存储或复制了这个引用,而在函数外部原shared_ptr已经被销毁,那么这个存储的引用将成为悬空引用,导致未定义的行为。
-
函数调用者可能会期望传递的shared_ptr对象会因函数调用而共享其所有权,但实际上却没有发生。
C++ 的4种类型转换
C++ 提供了四种类型转换操作符:
-
static_cast:用于非多态类型的转换,安全性较高的类型转换。
-
dynamic_cast:用于多态类型的转换,主要用于基类和派生类之间的安全转换。
-
const_cast:用于去除或添加const或volatile属性。
-
reinterpret_cast:用于进行低层次的重新解释类型转换,可能是不安全的。
explicit 关键字的作用,使用场景
explicit关键字用来阻止C++编译器执行隐式类型转换,要求类型转换必须明确无误。使用explicit的典型场景包括:
-
防止构造函数的隐式调用,确保只能显式地进行对象初始化。
-
防止具有单个参数的构造函数或转换操作符被编译器自动用作隐式类型转换。
协程和线程的区别
-
线程由操作系统内核管理, 协程一般由用户空间的库管理。
-
线程切换包括内核空间和用户空间的上下文切换,开销较大;协程切换仅包含用户空间的上下文切换,开销较小。
-
线程的调度由操作系统内核控制;协程的调度由程序员或库控制,拥有更细粒度的控制能力。
-
大量线程会增加系统负担,而大量协程对系统负担较小,可以实现更高的并发性能。
共享内存如何使用,原理
共享内存的使用通常包括以下步骤:
-
创建共享内存段:一个进程创建一个特定大小的共享内存段。
-
将共享内存段附加到进程的地址空间:需要通信的进程将这个共享内存段映射到它们的地址空间。
-
使用共享内存:进程可以通过指针和内存操作读写这个内存段。
-
分离共享内存段:进程用完后,将共享内存从自己的地址空间分离。
-
删除共享内存段:所有进程都不再需要时,最后一个使用的进程应删除共享内存段。
共享内存的原理是提供一块能被多个进程访问的内存区域。操作系统确保这块内存区域在多个进程间是可见的,从而实现非常快速的数据交换,因为它避免了数据的复制操作,直接在物理内存上进行读写。需要注意的是,由于多个进程可以同时访问,通常需要用同步机制(如信号量)来避免竞争条件。
虚拟内存和物理内存在什么时候进行转换
虚拟内存和物理内存进行转换的时候称为“页映射”,这个过程由操作系统的内存管理单元 (MMU) 控制。当一个程序试图访问虚拟内存地址时,如果这个地址已经映射到物理内存,MMU将直接转换访问;如果没有映射,操作系统将会触发一个缺页中断(page fault),然后加载相应的数据到物理内存,并更新页表以反映新的映射关系,之后访问可以继续进行。这个转换过程对程序是透明的。
core_dump 如何处理
处理core dump通常包括以下步骤:
-
启用Core Dump:确保操作系统配置允许产生core dump文件。
-
重现问题:让程序再次运行并产生崩溃,如果可重现。
-
收集Core Dump:确保收集到core dump文件,通常在程序崩溃时自动生成。
-
使用调试器:使用如gdb等调试器加载core dump文件和相应的程序执行文件。
-
分析堆栈跟踪:查看崩溃时的函数调用堆栈跟踪,寻找崩溃原因。
-
检查变量和内存状态:审查崩溃时的变量值和内存状态,寻找问题根源。
-
定位并修复代码:根据分析结果定位到源代码中产生问题的位置,并进行修复。
-
重新部署并测试:修复问题后重新编译、部署程序,并进行测试确保问题已解决。
孤儿进程和僵尸进程是什么,怎么处理
孤儿进程:当一个父进程结束,而它的一个或多个子进程还在运行时,那些子进程将成为孤儿进程。孤儿进程会被init进程(进程ID为1)自动领养,init进程会负责调用wait()来收集它们的退出状态。
僵尸进程:当一个子进程结束,在其父进程没有调用wait()或waitpid()获取子进程的状态信息之前,该子进程将变成僵尸进程。僵尸进程占用资源很小,但如果大量积累可能导致进程ID耗尽。
处理方法:
-
孤儿进程不需要手动处理,它们会被操作系统处理。
-
僵尸进程需由父进程通过调用wait()或waitpid()来处理和回收资源。如果父进程不做处理,可以试图用SIGCHLD信号提醒父进程处理僵尸进程,或者重启父进程。如果僵尸进程的父进程已经结束,那么这些僵尸进程将由init进程回收。
计算机网络中的长连接和短连接
长连接:在客户端和服务端建立连接后,如果不主动断开,这个连接将会保持活动状态,多次通信可以复用同一个连接。长连接减少了频繁建立和断开连接的开销,适用于需要频繁交换数据的场景。
短连接:每次通信都需要新建一个连接,数据传输完成后立即断开。短连接适用于请求-响应模式的通信,适合偶尔交换数据的场景。相较于长连接,它更简单,但在高频通信时会增加额外的开销。
TIME_WAIT 和 CLOSE_WAIT 的区别
TIME_WAIT:是TCP连接被动关闭(接收到对方发送的FIN报文)并发送了ACK后的状态。此状态通常等待一段时间以确保对方接收到了确认的ACK报文,通常是2MSL(最长报文段生存时间)。
CLOSE_WAIT:是当一方接收到对方的FIN报文,发送了ACK响应,但本地应用程序还没有调用close关闭连接时的状态。这意味着等待本地程序关闭连接。
美团软件开发(后端方向)
栈的概念和应用
栈是一种后进先出(LIFO, Last In First Out)的数据结构,只能在一端(栈顶)进行添加数据(压栈)和移除数据(弹栈)的操作。
应用:
-
函数调用的管理:保存函数返回点和局部变量。
-
表达式求值:编译器中用于计算后缀表达式。
-
撤销操作:编辑器中的撤销操作通常用栈实现。
-
页面导航历史:浏览器后退按钮的实现。
堆的概念和应用,对堆的理解
堆是一种通常被实现为二叉树的数据结构,它满足父节点的键值总是大于或小于子节点的键值(最大堆或最小堆)。它允许快速地插入新数据和删除最大或最小元素。
应用:
-
优先队列:快速访问和删除最高或最低优先级的元素。
-
堆排序:利用堆的性质进行高效的排序操作。
-
图算法:比如在Dijkstra和Prim算法中用于快速查找最小边。
-
动态数据集合的管理:动态地添加和删除集合中的元素。
哈希表的概念和应用
哈希表是一种通过哈希函数将键(key)映射到表中一个位置来访问记录的数据结构,以支持快速的插入和查找操作。
应用:
-
字典:存储键值对,允许快速的键查询。
-
缓存:例如网页缓存或数据缓存,提供快速的数据检索。
-
数据库索引:提高数据库查询效率。
-
集合:实现集合数据结构,进行快速的成员检查。
哈希表碰撞的概念以及如何解决
哈希表碰撞是指两个不同的键经过哈希函数处理后得到相同的哈希值,导致它们在哈希表中定位到同一个索引位置。
解决方法包括:
-
链接法:在哈希表的每个索引位置维护一个链表,发生碰撞时将元素添加到链表中。
-
开放地址法:发生碰撞时,按照某种探测序列在哈希表中寻找空位。
-
双重哈希:使用第二个哈希函数来决定探测序列。
-
增大哈希表:扩展哈希表的大小以减少碰撞概率。
哈希表的种类
哈希表的种类主要包括:
-
直接寻址表:当键的范围很小且连续时,可以直接使用键作为数组索引。
-
链地址法哈希表:通过链表来解决冲突,每个索引处存储一个链表。
-
开放寻址哈希表:在表内寻找空闲位置来解决冲突,常见的探测方式有线性探测、二次探测和双重哈希。
-
完全哈希表:先用一个哈希函数将键分配到多个桶中,然后对每个桶使用另一个哈希函数进行再哈希,以减少冲突。
-
一致性哈希表:特别适用于分布式系统,通过一致性哈希算法保证节点的增减对系统影响最小。
OSI七层模型
-
物理层:负责原始数据传输,如电缆、光纤等。
-
数据链路层:处理数据帧的寻址、错误检测和改错。
-
网络层:负责数据包的路由和转发。
-
传输层:确保数据完整性和有效传输,如TCP/UDP协议。
-
会话层:管理用户会话和对话控制。
-
表示层:处理数据的表示、加密和压缩。
-
应用层:为应用软件提供网络服务。
对称加密和非对称加密
对称加密使用同一密钥进行数据的加密和解密,速度快,适合大量数据加密。
非对称加密使用一对密钥,即公钥和私钥;公钥用于加密数据,私钥用于解密。它更为安全,但速度较慢,适用于加密少量数据或安全性要求高的场景。
TCP当中的粘包和拆包的概念以及如何解决这两类问题
TCP中的粘包和拆包是因为TCP是基于字节流的,没有固定边界:
-
粘包:多次发送的数据被合并成一次接收。
-
拆包:一次发送的数据被分成多次接收。
解决方法主要包括:
-
定长包:每个包的大小固定,接收方按固定长度接收数据。
-
包头加长度:每个包的开始部分增加表示数据长度的信息,接收方先读取这部分信息,再根据长度读取整个包。
-
结束符:在包尾添加特殊的结束字符,接收方通过这个字符判断包的终点。
MySQL中varchar和char的概念以及区别
varchar 和 char 是MySQL中用于存储字符串的两种数据类型。
-
char 是固定长度的字符串,当存储的字符串长度小于定义的长度时,会使用空格填充余下的空间;检索时会去除这些空格。适合存储长度几乎不变的数据。
-
varchar 是可变长度字符串,只占用必要的存储空间(加上一个额外的用于记录长度的字节或两个字节)。适合存储长度可变的数据。
区别在于存储方式和空间效率。char 适合存储长度固定的字段,而 varchar 更节省空间,用于长度不固定的字段。
删除表的方式有哪几种
删除MySQL表的主要方式有以下几种:
-
DROP TABLE 命令:直接删除表结构及其数据,无法恢复。
-
TRUNCATE TABLE 命令:删除表中的数据,重置自增计数器,但保留表结构。
-
DELETE FROM 命令:删除表中的全部或部分数据行,可以使用 WHERE 子句指定条件,保留表结构和自增计数器的当前值。
MySQL的索引类型
MySQL中的主要索引类型包括:
-
PRIMARY KEY(主键索引):唯一标识表中的每行数据,不能为NULL。
-
UNIQUE(唯一索引):确保数据列中的每个值都是唯一的。
-
INDEX(普通索引):加速数据的检索速度。
-
FULLTEXT(全文索引):用于全文搜索。
-
SPATIAL(空间索引):用于地理数据存储。
-
FOREIGN KEY(外键索引):用于表之间的链接,维护数据的完整性。
MySQL的行锁有哪些特点和类型
MySQL的行锁特点包括:
-
粒度小,仅锁定单个或部分行,减少锁竞争。
-
支持高并发事务处理。
-
可以防止脏读、不可重复读和幻读(取决于事务隔离级别)。
MySQL行锁的类型主要有:
-
共享锁(S锁):允许事务读一行数据。
-
排他锁(X锁):允许事务删除或更新一行数据。
-
意向锁:表明事务想要在表中的某些行上获取共享锁或排他锁,分为意向共享锁(IX)和意向排他锁(IS)。
MySQL中死锁的产生原因和处理机制
产生原因:
-
多个事务同时锁定不同资源后,又请求对方已锁定的资源,形成循环等待。
-
索引不足导致全表扫描,增加锁竞争。
-
插入、更新操作导致的锁升级。
-
事务隔离级别过高,增加锁的范围和时间。
处理机制:
-
超时机制:设置事务等待锁的最大时间,超时后事务自动回滚。
-
死锁检测:MySQL会不断检测事务等待图中是否存在环,发现死锁立即回滚其中一个事务来破坏环。
-
主动回滚:用户可以选择主动回滚事务来解除死锁。
-
减少锁粒度:优化SQL,减少一次事务中锁定的数据量,降低死锁发生概率。
操作系统中内存分页和分段的概念以及区别
内存分页和分段是操作系统中两种不同的内存管理机制。
分页(Paging):
-
概念:将物理内存分为固定大小的页,虚拟内存也分为同样大小的页。一个页表用于映射虚拟页到物理页。
-
区别:透明于用户,用户程序看不到分页的存在。
分段(Segmentation):
-
概念:将虚拟内存划分为逻辑上的段,每个段有其意义(如函数、数组等),段表实现虚拟段到物理内存的映射。
-
区别:分段是可见的,即用户定义了段的大小和内容。
二者区别主要在于:
-
分页是物理内存的映射技术,而分段是逻辑上对内存的划分。
-
分页有固定大小,分段大小不一,依据程序逻辑结构而定。
-
分段能够更好地反映程序的逻辑结构,而分页主要解决内存碎片和扩展问题。
-
分页管理简单,CPU 加载时不必了解数据意义,分段管理复杂但方便程序片段的保护和共享。
操作系统中I/O多路复用的概念和工作原理
I/O多路复用是指操作系统中可以在一个进程内同时处理多个Socket网络I/O操作的技术。它允许多个I/O操作同时阻塞在一个系统调用上,从而提高处理并发I/O的效率。
工作原理如下:
-
应用程序将需要监听的文件描述符(通常是socket)注册到I/O多路复用函数(如select、poll、epoll等)上。
-
当有数据到达或连接关闭等事件发生时,操作系统将唤醒阻塞在I/O多路复用函数上的进程或线程。
-
应用程序遍历多路复用函数提供的就绪文件描述符列表进行相关处理。
I/O多路复用的优势在于使用单个线程处理多个I/O事件,减少了线程的创建和销毁开销,提高了I/O并发处理能力。
redis的内存淘汰机制
Redis的内存淘汰机制是指当Redis的内存使用达到上限时,如何选择淘汰部分数据以保证新的写入操作的机制。以下是Redis提供的一些主要的内存淘汰策略:
-
No-eviction:不进行任何回收操作,新写入操作会被拒绝。
-
AllKeys-Random:在所有键中随机选择进行淘汰。
-
AllKeys-LRU:选择在所有键中最少被使用的键进行淘汰。
-
AllKeys-LFU:选择在所有键中使用频率最低的键进行淘汰。
-
Volatile-Random:在设置了过期时间的键中随机选择进行淘汰。
-
Volatile-LRU:选择在设置了过期时间的键中,最少被使用的键进行淘汰。
-
Volatile-LFU:选择在设置了过期时间的键中,使用频率最低的键进行淘汰。
-
Volatile-TTL:选择在剩余存活时间(TTL)最短的键进行淘汰。
设计模式有哪些
-
创建型模式:涉及对象的创建机制,帮助创建对象的方式提供灵活性。
-
单例(Singleton)
-
抽象工厂(Abstract Factory)
-
建造者(Builder)
-
工厂方法(Factory Method)
-
原型(Prototype)
-
结构型模式:关注类和对象的组织,以实现更大的结构。
-
适配器(Adapter)
-
桥接(Bridge)
-
组合(Composite)
-
装饰(Decorator)
-
外观(Facade)
-
享元(Flyweight)
-
代理(Proxy)
-
行为型模式:关注对象之间的职责分配及其算法。
-
责任链(Chain of Responsibility)
-
命令(Command)
-
解释器(Interpreter)
-
迭代器(Iterator)
-
中介者(Mediator)
-
备忘录(Memento)
-
观察者(Observer)
-
状态(State)
-
策略(Strategy)
-
模板方法(Template Method)
-
访问者(Visitor)
分布式锁的原理
分布式锁是一种在分布式系统中协调不同节点间共享资源访问的机制。它确保在同一时间只有一个节点可以对特定资源进行修改或访问。
原理如下:
-
独占性:任何时候,只能有一个客户端持有锁。
-
锁定机制:客户端请求锁定特定的资源,如果该资源没有被锁定,则获得锁。
-
超时机制:锁可以设置过期时间,防止死锁。
-
解锁机制:持锁的客户端完成操作后释放锁,或锁到期自动释放。
-
互斥性:当资源被加锁时,其他客户端不能获取锁。
实现分布式锁通常使用的方法包括:
-
基于数据库的锁:利用数据库的原子性操作来实现;
-
基于缓存的锁:例如使用Redis的SETNX操作;
-
基于Zookeeper的锁:利用Zookeeper的临时有序节点。
重排链表,规定使用原地算法
-
使用快慢指针找到链表中点。
-
从中点将链表分为两个部分,并反转第二部分链表。
-
将两部分链表合并,使之交错。
class Solution { public: void reorderList(ListNode* head) { if (!head || !head->next) return; // 使用快慢指针找到中点 ListNode *slow = head, *fast = head; while (fast->next && fast->next->next) { slow = slow->next; fast = fast->next->next; } // 反转后半部分链表 ListNode *prev = nullptr, *curr = slow->next, *temp; while (curr) { temp = curr->next; curr->next = prev; prev = curr; curr = temp; } // 将链表的前半部分和反转后的后半部分交替连接起来 ListNode *first = head, *second = prev; while (second) { temp = first->next; first->next = second; first = temp; temp = second->next; second->next = first; second = temp; } // 防止形成环 if (first) first->next = nullptr; } };
最后顺便给大家分享一下,民族企业大厂前后端测试捞人,待遇给的还不错,感兴趣的可以来试试!