腾讯云CSIG 后端开发工程师 二面

面试基本信息

  • 公司: 腾讯云 AI产品中心
  • 职位: 后端开发工程师
  • 面试轮次: 二面(技术面)
  • 面试形式: 线上视频面试(可选择是否开摄像头)
  • 面试时长: 约70分钟
  • 面试官: 技术团队负责人,主要负责AI平台机器学习平台相关产品

团队介绍

面试官介绍了团队情况:

  • 隶属于腾讯云AI产品中心
  • 主要做AI平台和机器学习平台相关产品
  • 具体工作偏向底层支撑,优化GPU、CPU算力的使用效率
  • 让平台更高效地使用计算资源

面试问题与回答

问题1:项目介绍 - Go Cache分布式缓存系统

面试官问题

“简单介绍一下你的基础情况和项目,我看你简历上有个Go Cache项目,说一下这个项目的背景”

我的回答

“Go Cache是一个分布式缓存系统,挂载在服务端和数据库中间。可以设置各种数据源,通过缓存机制减少数据库访问,提高QPS。项目中使用了single flight机制解决缓存击穿问题,将多个相同请求合并成一个,减少网络IO次数。”

标准/建议回答

项目介绍可以更系统一些。首先说明项目的定位和解决的问题:Go Cache是一个分布式缓存中间件,主要解决高并发场景下数据库压力过大的问题。

然后介绍架构设计:采用主从架构,主节点负责接收客户端请求和协调,从节点负责数据存储和数据源访问。支持数据分片,类似Redis集群。

最后说明核心优化:实现了single flight机制解决缓存击穿,当多个相同请求同时到达时,只有一个真正发起数据获取,其他请求等待并共享结果。

面试官追问

  • single flight的时间窗口和失败处理
  • 主从节点的具体职责划分
  • 分布式体现在哪里

总结反思

  • 对项目的核心机制理解清楚
  • 但对故障处理场景考虑不足
  • 应该准备更详细的架构图和异常处理方案

问题2:项目介绍 - 抖音商城系统

面试官问题

“再介绍一下你的抖音商城项目,这个项目是什么背景?”

我的回答

“这是参加字节跳动青训营的项目,是完整的商城系统。实现了服务发现、服务注册、配置中心、分布式缓存、分布式锁等。使用Canal监听MySQL binlog实现数据同步,保证MySQL、Redis、ElasticSearch之间的弱一致性。”

标准/建议回答

可以从系统设计角度更好地介绍这个项目。首先说明这是一个微服务架构的电商系统,实现了完整的商城核心功能。

技术架构方面:

  1. 微服务治理:服务发现、注册、配置中心
  2. 数据层:MySQL主库 + Redis缓存 + ElasticSearch搜索
  3. 可观测性:集成OpenTelemetry
  4. 数据一致性:Canal + 消息队列实现最终一致性

核心亮点是数据同步方案:通过Canal监听MySQL binlog,将数据变更发送到消息队列,消费者更新Redis和ES,实现了业务代码解耦和数据最终一致性。

面试官追问

  • 为什么选择ElasticSearch而不是MySQL做搜索
  • 数据一致性要求和Canal的作用

问题3:MySQL性能优化

面试官问题

“如果MySQL查询很慢,有什么办法定位和优化?”

我的回答

“首先用explain语句查看是否使用了索引,检查key字段。然后看查询语句是否有导致优化器选择全表扫描的问题,比如使用了OR。可能需要建索引,但在数据量大的时候建索引会比较耗时。”

标准/建议回答

MySQL查询优化可以从几个层面来分析:

  1. SQL层面:使用EXPLAIN分析执行计划,重点看type、key、rows、filtered字段。检查是否走索引,是否有filesort、temporary等

  2. 索引层面:分析慢查询日志,检查where条件、order by、group by字段是否有适当索引。考虑联合索引的最左匹配原则

  3. 系统层面:查看MySQL服务器状态,包括连接数、锁等待、IO状况等

  4. 具体优化手段

    • 重写SQL,避免函数、OR、前缀模糊查询
    • 添加覆盖索引减少回表
    • 分库分表处理大数据量
    • 读写分离分担压力

面试官追问

  • 如何设计索引,考虑哪些因素
  • 联合索引的使用场景

问题4:分布式锁实现

面试官问题

“如果用MySQL实现分布式锁,应该怎么做?用Redis呢?”

我的回答

"MySQL实现分布式锁我觉得不太适合,因为速度比较慢。但要实现的话也可以,我想到有两种方式。第一种是用事务,让所有请求都执行同一个insert语句。因为insert会对整个表加锁,或者说对ID的间隙进行加锁,这时其他请求就会被阻塞。然后在事务执行过程中可以添加业务代码,最后业务代码执行完再提交事务,这样就实现了锁。第二种是避免用insert,可以使用select xxx for update这种方式实现加锁。

Redis的话用SET NX命令,因为Redis单线程保证原子性。需要注意删除锁时的安全性,用lua脚本保证原子性。"

标准/建议回答

MySQL分布式锁

1
2
3
4
5
-- 方案1:基于唯一索引
INSERT INTO lock_table (lock_key, owner, expire_time) VALUES ('resource_id', 'unique_id', NOW() + INTERVAL 30 SECOND);

-- 方案2:基于行锁
SELECT * FROM lock_table WHERE lock_key = 'resource_id' FOR UPDATE;

优点是强一致性,缺点是性能较低,适合对一致性要求极高的场景。

Redis分布式锁

1
2
3
4
5
6
7
8
9
# 加锁
SET lock_key unique_value PX 30000 NX

# 解锁(lua脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

优点是性能高,缺点是可能存在锁丢失的极端情况。

面试官追问

  • lua脚本的问题和替代方案
  • Redis大key问题和解决方案

问题5:Redis内存评估和缓存策略

面试官问题

“如何评估Redis缓存需要多少内存?比如1万个key,每个value 1KB,需要多少内存?”

我的回答

“1万个key,每个1KB,大概需要几十兆内存。”

标准/建议回答

Redis内存评估需要考虑多个方面:

  1. 数据本身:1万 × 1KB = 10MB
  2. key的存储:假设key平均20字节,1万 × 20B = 200KB
  3. Redis数据结构开销:每个key-value对大约有40-50字节的元数据开销
  4. 内存碎片:通常占用额外10-20%空间
  5. 持久化开销:RDB/AOF可能需要额外内存

所以实际需要大约:10MB + 0.2MB + 0.5MB + 2MB = 13-15MB左右。

对于热点数据管理:

  • 可以通过统计访问频率识别热点
  • 使用LRU、LFU等淘汰策略
  • 定期刷新热点数据,避免业务高峰期回源

面试官追问

  • 热点数据变化时的处理策略

问题6:Redis单线程模型

面试官问题

“Redis说是单线程,它是怎么支持高并发的?在多核系统上怎么利用多核优势?”

我的回答

“Redis的单线程是指命令执行是单线程,实际上是多线程的。单线程避免了上下文切换开销,而且操作的是内存速度很快。多核系统可以开多个Redis实例在不同端口。”

标准/建议回答

Redis的单线程指的是命令处理是单线程的,但整个系统是多线程的:

  1. 主线程:处理客户端请求,执行命令
  2. 后台线程:处理持久化、内存回收、集群数据同步等

单线程的优势:

  • 避免线程切换开销
  • 避免锁竞争
  • 简化内存模型
  • 基于内存操作,CPU不是瓶颈

高并发支持:

  • 基于IO多路复用(epoll/kqueue)
  • 非阻塞IO,单线程处理多个连接
  • 内存操作速度极快

多核利用:

  • 单实例主要受内存和网络IO限制
  • 可以部署多实例利用多核
  • Redis 6.0引入多线程处理网络IO

面试官追问

  • 主要性能瓶颈在哪里
  • Redis内部请求处理流程

问题7:网络协议和HTTPS

面试官问题

“你了解HTTP/2吗?HTTPS的机制是什么?如果要拦截HTTPS请求应该怎么做?”

我的回答

“HTTP/2好像是在长连接方面做了优化。HTTPS是在HTTP基础上加了密钥交换和加密。拦截需要有客户端信任的CA证书。”

标准/建议回答

HTTPS机制

  1. 证书验证:客户端验证服务器证书合法性
  2. 密钥协商:通过RSA或ECDHE等算法协商对称密钥
  3. 数据加密:使用协商的对称密钥加密传输数据

HTTPS中间人拦截
需要作为中间代理:

  1. 客户端信任代理的根证书
  2. 代理为目标域名动态生成证书
  3. 与客户端建立HTTPS连接(使用代理证书)
  4. 与服务器建立HTTPS连接(验证真实证书)
  5. 解密客户端数据,处理后转发给服务器

这种方案常用于企业网关、安全审计等场景。

面试官追问

  • 网络包传输的完整流程
  • 从应用层到物理层的处理过程

问题8:Go语言协程和调度

面试官问题

“Go语言的协程和线程有什么区别?协程什么时候会让出CPU?”

我的回答

“协程比线程轻量,不需要和内核态交互。进程是资源分配单位,线程是调度单位,协程在线程之上。协程超过10毫秒会被强制调度,或者阻塞时主动让出。”

标准/建议回答

协程与线程的区别

  1. 创建开销

    • 线程:需要内核态操作,栈空间默认8MB
    • 协程:用户态创建,初始栈2KB,动态增长
  2. 调度方式

    • 线程:抢占式调度,内核控制
    • 协程:协作式调度,用户态控制
  3. 上下文切换

    • 线程:涉及内核态切换,保存完整寄存器状态
    • 协程:只需保存少量寄存器,开销很小

协程让出CPU的时机

  1. 主动让出:调用runtime.Gosched()、channel操作阻塞、mutex获取失败
  2. 被动抢占:运行时间过长(10ms),系统调用返回时检查
  3. 系统调用:进入系统调用时,M会与P分离

面试官追问

  • 线程切换的具体开销
  • for循环会不会一直占用CPU

问题9:编程题 - 并发安全的Map

面试官问题

“用Go语言实现一个并发安全的map,包含put、get、delete方法”

我的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type SafeMap struct {
mu sync.RWMutex
data map[string]interface{}
}

func (m *SafeMap) Put(key string, value interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}

func (m *SafeMap) Get(key string) (interface{}, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
value, ok := m.data[key]
return value, ok
}

func (m *SafeMap) Delete(key string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, key)
}

面试官追问

  • 为什么使用读写锁?
  • 还能怎么优化?

标准/建议回答

读写锁的优势:在读多写少的场景下,多个goroutine可以同时获取读锁,提高并发性能。

进一步优化方案:

  1. 分段锁:将map分成多个segment,每个segment独立加锁,减少锁竞争
  2. 无锁实现:使用atomic包和CAS操作
  3. 使用sync.Map:Go标准库提供的并发安全map

分段锁实现思路:

1
2
3
4
5
6
7
8
9
type SegmentedMap struct {
segments []segment
mask uint32
}

type segment struct {
mu sync.RWMutex
data map[string]interface{}
}

面试整体感受

  • 难度评价: 适中,主要考查基础扎实程度和项目经验
  • 面试官风格: 专业友善,会深入追问细节
  • 题目类型: 项目经验、基础知识、编程能力并重
  • 准备建议: 重点准备项目细节、常见中间件原理、Go语言特性

面试结果

  • 当场反馈: 面试官没有给出明确反馈
  • 后续流程: 需要等待HR通知后续安排
  • 个人感受: 部分问题回答不够深入,特别是网络协议和系统底层部分

经验总结

做得好的地方

  • 项目介绍比较清晰,single flight等核心技术点表达准确
  • Redis、MySQL基础知识掌握较好
  • 编程题实现思路正确

需要改进的地方

  • 对故障场景和边界情况考虑不足
  • 网络协议底层原理掌握不够深入
  • 系统设计的全局思维需要加强

准备建议

  1. 项目经验:准备详细的架构图,考虑各种异常场景的处理
  2. 基础知识:深入理解常用中间件的底层原理和源码
  3. 系统设计:多练习分布式系统设计,考虑性能、一致性、可用性权衡
  4. 编程能力:熟练掌握常见数据结构和算法的并发安全实现

知识点复习

  • 分布式缓存设计原理
  • MySQL索引优化和查询分析
  • Redis内存管理和持久化机制
  • HTTPS/TLS协议详解
  • Go语言GMP调度模型
  • 分布式锁的实现和对比
  • 网络协议栈和数据包传输流程