腾讯IEG 运营开发工程师 二面

面试基本信息

  • 公司: 腾讯IEG(互动娱乐事业群)
  • 职位: 运营开发工程师(日常实习)
  • 面试轮次: 二面(技术面)
  • 面试形式: 线上视频面试(腾讯会议)
  • 面试时长: 约50分钟
  • 面试官: 技术leader,比较友善专业

面试问题与回答

问题1:水杯量水(逻辑思维题)

面试官问题

“你手上有两个水杯,都是没有刻度的,一个三升,还有一个是五升。你还有大量的水,怎么用最快的方式量出四升的水?”

我的回答

“首先先往这个三毫升的水里面装满,三毫升的杯子里面装满水,然后再倒到这个五毫升的水里面,然后再把这个就是现在5毫升的杯子里面有三毫升的水了。然后再再把这个三毫升的装满。装满以后再把这个3毫升的杯子里面水倒满,倒到5毫升的杯子里面倒满。然后5毫升的杯子满了,那3毫升的杯子里面就只剩一毫升的水了。然后再把5毫升的杯子里面水全都倒掉,然后再把这个三毫升杯子里面的那个一毫升的水给倒到5毫升的杯子里面,最后再把这个3毫升的杯子装满,然后装满然后再倒到5毫升的杯子里面,这样就可以实现了。”

标准/建议回答

这个问题回答得还不错。具体步骤是:

  1. 先把3升杯子装满,倒入5升杯子(5升杯子里有3升水)
  2. 再把3升杯子装满,继续倒入5升杯子直到满(3升杯子里剩1升水)
  3. 把5升杯子的水全部倒掉,把3升杯子里的1升水倒入5升杯子
  4. 再把3升杯子装满,倒入5升杯子(1+3=4升)

这样就得到了4升水。

总结反思

  • 思路正确,步骤也对
  • 表达稍微有些啰嗦,可以更简洁些

问题2:博弈论问题(苹果抢夺游戏)

面试官问题

“在一个课桌上有100个苹果。我和你轮流拿,每人每次拿的苹果数量是1到5。你来设计一个策略,保证第100个苹果,也就是最后一个苹果一定是你能拿到的。”

我的回答

“我要确保我拿完以后,最后剩下的苹果数模六是等于一的…比如说我有N个苹果,然后我第一次拿四个,然后之后对方无论拿多少个,无论拿多少个,比如说对方拿七个,那我都一定拿6减7个。这样就一定能保证我会最后获胜。”

标准/建议回答

这个是经典的博弈论问题。正确的策略是:

首先分析获胜条件:要想拿到最后一个苹果,就要保证轮到对方拿的时候,剩余苹果数是6的倍数。

因为每次最多拿5个,最少拿1个,所以如果剩6个苹果轮到对方,无论他拿几个(1-5个),我都能通过拿对应数量的苹果(5-1个)来保证拿到最后一个。

具体策略:

  1. 100 ÷ 6 = 16…4,所以第一次我拿4个,剩96个
  2. 之后无论对方拿几个(1-5个),我都拿对应的数让两人这轮总共拿6个
  3. 这样每轮结束后,剩余苹果数都是6的倍数
  4. 最终轮到对方时剩6个,我必胜

总结反思

  • 思路方向对了,但一开始说成了"模5等于1"
  • 经过面试官提示逐步找到了正确答案
  • 这类题目需要从终局往前推理

问题3:编程题(最长连续递增子序列)

面试官问题

“写个算法求出字符串的最长连续递增子序列,要把这个子序列输出出来,字符串是A到Z的小写字母。”

我的回答

“我印象中是用DP,然后如果说发现他这一个右边那个元素,就是小于前面的一个元素了,那就是需要回溯一下,然后继续重新进行那个DP。”

标准/建议回答

其实这题用DP可能把问题复杂化了,有更简单的思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

func longestIncreasingSubstring(s string) string {
if len(s) == 0 {
return ""
}

maxLength := 1
maxStart := 0
currentLength := 1
currentStart := 0

for i := 1; i < len(s); i++ {
if s[i] > s[i-1] { // 连续递增
currentLength++
} else { // 重新开始
if currentLength > maxLength {
maxLength = currentLength
maxStart = currentStart
}
currentLength = 1
currentStart = i
}
}

// 检查最后一段
if currentLength > maxLength {
maxLength = currentLength
maxStart = currentStart
}

return s[maxStart : maxStart+maxLength]
}

func main() {
// 测试用例
test := "abcxyzdeghi"
result := longestIncreasingSubstring(test)
fmt.Printf("输入: %s\n", test)
fmt.Printf("最长连续递增子序列: %s\n", result)
}

思路就是:设置一个最大长度记录器,遍历字符串,如果下一个字符比前一个大就继续,否则重新开始计数,并更新最大值。

总结反思

  • 方向想复杂了,其实是比较基础的算法题
  • 现场没有写出代码,说明基础算法需要加强
  • 应该多练习这类字符串处理题目

问题4:项目经验(抖音商城系统)

面试官问题

“您这边有做过比较有价值的一个项目,能够两分钟给我介绍一下背景,还有你在里面做的一些事情?”

我的回答

“比较有技术含量的应该是这个抖音商城。这个项目最大的挑战是Casbin权限管理框架的缓存一致性问题。我定义了两个Casbin对象,但是这两个对象并没有真的去同步他们的缓存数据。最后解决的方法就是把整个项目中Casbin只允许初始化一次,使用单例模式解决了这个问题。”

标准/建议回答

项目介绍可以更结构化一些:

  1. 项目背景:这是抖音青训营的团队项目,基于微服务架构开发的电商系统,我主要负责整体架构设计和技术选型。

  2. 技术架构:使用Go语言+微服务架构,集成了服务发现、配置中心、消息队列、分布式缓存等组件,还实现了CICD自动化部署。

  3. 技术难点:主要是Casbin权限管理的缓存一致性问题。Casbin在内存中维护策略缓存,当我创建多个Casbin实例时,出现了缓存不同步的问题。最终通过单例模式确保全局只有一个Casbin实例,解决了缓存一致性问题。

  4. 其他亮点:还实现了Redis+MySQL的分级存储、Canal+MQ的数据同步、基于Redis Lua脚本的分布式锁等。

总结反思

  • 项目经验还算丰富,但表达可以更有条理
  • 对技术细节的理解还不够深入

问题5:高并发场景(库存超卖问题)

面试官问题

“在高并发情况下,比如说库存是100,但是高并发大家一起过来买,有可能会出现超卖的情况。这种问题你当时在设计的时候怎么解决?”

我的回答

“我们使用了Redis的Lua脚本,然后去给库存去进行加锁…使用了Redis里面的SET NX命令进行加锁,因为Redis本身是单线程的,所以一旦对库存进行加锁以后,别的请求其实是会被拒绝的。”

标准/建议回答

超卖问题确实是高并发场景下的经典问题,解决方案有几种:

  1. Redis原子操作:使用Redis的DECR命令,因为Redis是单线程的,可以保证原子性。先检查库存是否充足,再减库存。

  2. Lua脚本:把检查库存和减库存的逻辑写在一个Lua脚本里,Redis会原子性地执行整个脚本:

Lua脚本:

1
2
3
4
5
if redis.call('get', 'stock') >= tonumber(ARGV[1]) then
return redis.call('decrby', 'stock', ARGV[1])
else
return -1
end

Go语言调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"strconv"
)

func decreaseStock(rdb *redis.Client, quantity int) (int64, error) {
luaScript := `
if redis.call('get', KEYS[1]) >= tonumber(ARGV[1]) then
return redis.call('decrby', KEYS[1], ARGV[1])
else
return -1
end
`

ctx := context.Background()
result, err := rdb.Eval(ctx, luaScript, []string{"stock"}, quantity).Result()
if err != nil {
return -1, err
}

return result.(int64), nil
}

func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

// 尝试减少库存
remaining, err := decreaseStock(rdb, 1)
if err != nil {
fmt.Printf("减库存失败: %v\n", err)
return
}

if remaining == -1 {
fmt.Println("库存不足")
} else {
fmt.Printf("减库存成功,剩余库存: %d\n", remaining)
}
}
  1. 数据库层面:使用乐观锁(版本号)或悲观锁(SELECT FOR UPDATE)。

  2. 分布式锁:使用Redis实现分布式锁,但性能会有损失。

其中Redis + Lua脚本是比较好的方案,既保证了原子性,性能也不错。

总结反思

  • 知道用Redis和Lua脚本的思路是对的
  • 但对具体实现细节不够清楚
  • 需要深入理解Redis原子操作的原理

问题6:监控告警系统

面试官问题

“你们做了一些监控和告警的功能,监控哪些指标?监控日志是监控的什么指标?”

我的回答

“我们是直接接入了OpenTelemetry,监控QPS指标,但QPS我们没有做告警。还有监控日志,日志监控只是为了去排查问题…我们告警主要是黑名单这一块,使用Redis进行QPS限制,如果一个用户一秒内请求超过十次,就会调用黑名单功能禁用这个用户。”

标准/建议回答

监控告警系统通常包括以下几个方面:

  1. 应用层监控

    • QPS/TPS:每秒请求数/事务数
    • 响应时间:平均响应时间、P99响应时间
    • 错误率:4xx、5xx错误比例
    • 接口可用性
  2. 系统层监控

    • CPU使用率、内存使用率
    • 磁盘IO、网络IO
    • 数据库连接数
  3. 业务层监控

    • 用户访问量、转化率
    • 业务操作成功率
    • 资金安全相关指标
  4. 日志监控

    • 错误日志数量和频率
    • 关键业务操作日志
    • 安全相关日志(登录失败、异常访问等)

告警策略应该基于阈值设置,比如错误率超过5%、响应时间超过1秒等。

总结反思

  • 对监控的理解比较浅层,主要停留在工具使用层面
  • 缺乏对监控体系的系统性理解
  • 应该学习更多监控告警的最佳实践

面试整体感受

  • 难度评价: 适中,主要考察基础能力和项目经验
  • 面试官风格: 比较友善,会给提示引导思考
  • 题目类型: 逻辑思维+编程+项目经验,比较全面
  • 准备建议: 需要加强基础算法和深入理解项目技术细节

面试结果

  • 当场反馈: 面试官表示技术项目还是很扎实的
  • 后续流程: 表示还在招聘中,需要等对比结果
  • 个人感受: 整体发挥一般,算法题没答出来比较遗憾

经验总结

做得好的地方

  • 项目经验比较丰富,技术栈涉及面广
  • 逻辑思维题思路清晰,能在提示下找到答案
  • 对微服务、分布式有一定理解

需要改进的地方

  • 基础算法能力需要加强,编程题完全没写出来
  • 对项目中技术细节的理解不够深入
  • 表达可以更有条理,回答更简洁

准备建议

  1. 算法刷题:LeetCode中等难度题目要熟练掌握
  2. 项目深挖:对简历中的项目要能深入到源码层面
  3. 基础巩固:Redis、MySQL、微服务等核心技术要深入理解
  4. 系统设计:多了解高并发、分布式系统的经典问题和解决方案

知识点复习

  • 水杯量水等经典逻辑题
  • 博弈论基础问题
  • 字符串处理算法(滑动窗口、双指针)
  • Redis原子操作和Lua脚本
  • 分布式系统中的缓存一致性问题
  • 监控告警系统设计
  • 高并发场景下的常见问题(超卖、缓存击穿等)