腾讯 C++ 后台开发 一面总结

腾讯后台开发一面面试题及规范答案(25题)

1. 介绍一下epoll的工作原理

epoll是Linux下高性能的IO多路复用机制,它的核心原理是使用红黑树存储需要监听的文件描述符,使用就绪链表存储已就绪的事件。当文件描述符就绪时,通过回调机制将其加入就绪链表,epoll_wait只需要遍历就绪链表即可。相比select和poll,epoll不需要每次都传递整个文件描述符集合,避免了大量内存拷贝,并且使用mmap共享内核和用户空间的内存。它只返回就绪的文件描述符,时间复杂度是O(1),而select需要遍历所有文件描述符,复杂度是O(n)。另外epoll支持的文件描述符数量没有限制,而select限制在1024个。

2. epoll的ET和LT触发模式有什么区别?

LT是水平触发模式,只要文件描述符处于就绪状态,epoll_wait就会持续通知,如果一次没有处理完数据,下次调用仍会通知,编程比较简单,是默认模式。ET是边缘触发模式,只在状态变化时通知一次,必须一次性读完所有数据,否则剩余数据不会再通知,需要配合非阻塞IO使用,循环读取直到返回EAGAIN。ET模式性能更高,因为减少了系统调用次数,但编程复杂度增加,容易遗漏事件。一般对性能要求高且能保证正确处理的场景用ET,否则用LT更安全。

3. 进程间通信有哪些方式?

常见的进程间通信方式有管道、消息队列、共享内存、信号量、信号和Socket。管道是半双工通信,只能用于父子进程或兄弟进程,分为匿名管道和命名管道。消息队列是消息的链表,存储在内核中,可以实现消息优先级,生命周期随内核。共享内存是最快的IPC方式,多个进程直接访问同一块物理内存,但需要配合信号量或互斥锁同步。信号量主要用于进程同步而非数据传递,通过P/V操作实现互斥和同步。信号是异步通知机制,携带信息少,只能传递信号类型。Socket可用于不同主机间通信,功能最强大但开销较大。性能上共享内存最快,其次是管道和消息队列,Socket最慢。

4. 什么是信号量的虚假唤醒问题?如何解决?

虚假唤醒是指线程在没有被显式唤醒的情况下从等待状态返回。产生原因主要是操作系统的实现细节,某些系统调用可能导致线程被意外唤醒,或者多个线程等待同一个条件变量,一个线程被唤醒后修改了条件,其他线程醒来时条件已不满足。解决方案是使用while循环而不是if判断条件,线程被唤醒后重新检查条件是否满足,条件不满足则继续等待。比如在生产者消费者模型中,消费者应该用while循环检查缓冲区是否为空,而不是用if判断一次,这样即使被虚假唤醒,也会重新检查条件并继续等待。这是多线程编程的一个重要原则,永远使用while循环检查条件。

5. 多线程同步有哪些方式?

常见的线程同步机制有互斥锁、读写锁、条件变量、信号量、自旋锁、原子操作和屏障。互斥锁是最基本的同步原语,保证同一时刻只有一个线程访问共享资源,适合保护临界区。读写锁允许多个读者同时访问,写者独占,适合读多写少的场景。条件变量用于线程等待某个条件成立,必须配合互斥锁使用,适合生产者消费者模型。信号量控制同时访问资源的线程数量,可以实现互斥或同步,适合资源池管理。自旋锁是忙等待不放弃CPU,适合锁持有时间极短的场景,避免上下文切换开销。原子操作是硬件级别的原子性保证,是无锁编程的基础,适合简单的计数器和标志位。屏障让多个线程在某个点同步,所有线程到达后才继续执行。选择时简单互斥用Mutex,读多写少用RWLock,等待条件用Condition Variable,资源计数用Semaphore,极短临界区用Spinlock,简单变量用Atomic。

6. 介绍一下平衡二叉树

平衡二叉树也叫AVL树,是一种自平衡的二叉搜索树。它的核心特性是任意节点的左右子树高度差不超过1,保证树的高度为O(log n),使得查找、插入、删除操作都是O(log n)时间复杂度。平衡因子定义为左子树高度减去右子树高度,取值范围是-1、0、1,超出范围需要通过旋转调整。有四种旋转操作:LL旋转是左子树的左子树过高,对失衡节点右旋;RR旋转是右子树的右子树过高,对失衡节点左旋;LR旋转是左子树的右子树过高,先对左子树左旋再对根节点右旋;RL旋转是右子树的左子树过高,先对右子树右旋再对根节点左旋。AVL树的优点是严格平衡,查找性能稳定,缺点是插入删除时旋转次数多,维护成本高。

7. 红黑树的应用场景有哪些?为什么很多地方用红黑树而不是AVL树?

红黑树广泛应用于C++ STL的map和set、Linux内核的进程调度和虚拟内存管理、Java的TreeMap和TreeSet、Nginx的定时器管理、epoll存储文件描述符等场景。相比AVL树,红黑树是近似平衡而非严格平衡,最长路径不超过最短路径的2倍,插入删除最多旋转3次,效率更高。虽然AVL树严格平衡,查找性能略优,但需要频繁调整,维护成本高。实际应用中大多数场景是插入删除和查找混合,纯查询场景较少,红黑树的近似平衡已经足够,查找性能差距不大,都是O(log n)。从工程实践角度,红黑树实现更简洁,性能更稳定可预测,性价比更高。如果是查询密集型应用比如数据库索引可以用AVL树,插入删除频繁的场景比如STL容器用红黑树,通用场景推荐红黑树。

8. Redis的内存淘汰策略有哪些?

Redis提供8种内存淘汰策略。noeviction是默认策略,不淘汰任何数据,写入操作返回错误,只能执行读操作和删除操作。allkeys-lru从所有key中使用LRU算法淘汰最久未使用的,是最常用的策略,适合通用缓存场景。allkeys-lfu从所有key中使用LFU算法淘汰访问频率最低的,是Redis 4.0新增的。allkeys-random从所有key中随机淘汰。volatile-lru从设置了过期时间的key中使用LRU淘汰,适合部分数据需要持久化的场景。volatile-lfu从设置了过期时间的key中使用LFU淘汰。volatile-random从设置了过期时间的key中随机淘汰。volatile-ttl淘汰即将过期的key,优先淘汰TTL最小的。LRU淘汰最久未使用的,LFU淘汰访问频率最低的,LFU更精准但实现复杂。通用缓存推荐allkeys-lru,热点数据用allkeys-lfu,部分持久化用volatile-lru。

9. 如果Redis的所有key都没有设置过期时间,内存写满了会怎么处理?

这取决于配置的内存淘汰策略。如果是noeviction策略,写入操作会失败,返回OOM错误,只能执行读操作和删除操作,服务基本不可用。如果是volatile开头的策略,因为没有设置过期时间的key,无法找到可淘汰的key,写入操作同样会失败,效果等同于noeviction。如果是allkeys开头的策略,可以正常淘汰数据,allkeys-lru会淘汰最久未使用的key,allkeys-lfu会淘汰访问频率最低的key,allkeys-random会随机淘汰key。实际生产中建议合理设置maxmemory,不要设置为物理内存的100%,预留20-30%给系统,选择合适的淘汰策略推荐allkeys-lru或allkeys-lfu,给缓存数据设置合理的TTL避免大量永久key,监控内存使用率达到80%时告警,提前扩容或清理,业务层面控制缓存数据量,定期清理无用数据。

10. 介绍一下你做过的高并发订单处理系统项目

这是一个电商平台的订单处理系统,日均处理订单量达到200万,峰值QPS在秒杀场景下能达到2万。我主要负责核心处理引擎的设计和实现,技术栈是C++后端加MySQL存储加Redis缓存加RabbitMQ消息队列。系统最大的挑战是在秒杀场景下数据库成为瓶颈,出现大量超时和库存超卖问题。我们采用了多级缓存架构,将库存信息缓存到Redis,使用Lua脚本保证原子性扣减库存,引入消息队列异步处理订单创建,削峰填谷,数据库层面做了读写分离和索引优化。通过这些优化,将系统QPS从2000提升到2万,订单处理延迟从500毫秒降到50毫秒以内,同时保证了99.99%的可用性。在数据一致性方面,我们通过消息队列异步同步Redis和数据库,并设置定时对账任务来保证最终一致性。

11. 你的订单系统是如何做压力测试和性能优化的?

压力测试方面,我们首先设定了明确的目标,QPS要达到2万,P99响应时间小于200毫秒,错误率小于0.1%。准备了独立的测试环境,配置接近生产环境,准备了100万条测试数据。使用wrk工具进行HTTP接口压测,自研脚本模拟秒杀场景,用Prometheus监控系统指标。测试场景包括基准测试单接口性能,负载测试逐步增加压力找瓶颈,峰值测试模拟高峰期流量,稳定性测试持续运行24小时。监控的指标包括应用层的QPS、响应时间、错误率,系统层的CPU、内存、磁盘IO,数据库的慢查询、连接数、锁等待,Redis的命中率和内存使用。性能优化方面,数据库层面添加了联合索引,优化SQL避免全表扫描,做了读写分离,使用连接池复用连接。缓存层面将热点数据缓存到Redis,使用pipeline批量操作减少网络往返,任务配置信息缓存到本地内存。代码层面使用对象池复用对象减少内存分配,日志异步写入避免阻塞主流程,批量操作减少数据库访问次数。架构层面做了订单分片按ID哈希分散到不同处理节点,使用消息队列异步解耦。最终QPS从2000提升到2万,P99延迟从500毫秒降到150毫秒,CPU使用率从80%降到40%。

12. 消息队列发送失败时如何处理?如何保证消息不丢失?

消息发送失败时我们采用重试机制,失败后立即重试1到3次,使用指数退避策略,重试间隔逐渐增加比如1秒、2秒、4秒、8秒,设置最大重试次数避免无限重试。如果重试仍然失败,会将消息写入本地文件或数据库,通过定时任务扫描并重新发送,必要时人工介入处理。降级方案是消息队列不可用时直接写数据库,后续通过补偿机制处理。保证消息不丢失需要从三个层面考虑。生产者端使用同步发送等待broker确认,配置消息持久化写入磁盘后才返回成功,发送前先存本地确认后删除。消息队列端配置消息持久化写入磁盘,使用副本机制多副本冗余比如Kafka的replication,采用同步刷盘策略虽然性能差但可靠。消费者端使用手动提交offset处理完成后再提交,做好幂等性设计重复消费不影响结果,失败消息进入死信队列。另外还需要监控消息发送失败率、消息堆积情况和消费延迟,设置定时对账任务比对上下游数据,发现不一致后补偿处理。核心原则是生产者确认加重试加本地存储,Broker持久化加副本,消费者手动提交加幂等性,系统层面监控加补偿。

13. Redis集群挂了如何保证业务不受影响?如何设计兜底方案?

Redis高可用需要多层次的方案。第一层是Redis自身高可用,使用

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务