Files
examination/topics/interview-prep/distributed-microservice/code_reading.json
T
wonder 0f68a64829
Deploy Examination / deploy (push) Successful in 10s
feat: add interview-prep topic group with 250 questions (6 subtopics, 5 question types)
Subtopics:
- distributed-microservice: 45 questions (分布式微服务架构)
- message-queue: 45 questions (消息队列)
- k8s-observability: 45 questions (K8s与可观测性)
- go-java-concurrency: 45 questions (Go/Java并发模型)
- database-advanced: 35 questions (数据库进阶)
- ai-engineering: 35 questions (AI工程实践)

Question types: single_choice, true_false, fill_blank, short_answer, code_reading
2026-09-09 16:36:27 +08:00

296 lines
21 KiB
JSON

{
"topic": "distributed-microservice",
"type": "code_reading",
"schema_version": "1.0.0",
"generated": "2026-09-09T00:00:00+08:00",
"questions": [
{
"id": "cr-001",
"type": "code_reading",
"difficulty": 3,
"tags": [
"microservice",
"distributed-lock"
],
"question": "阅读以下基于 Redis 的分布式锁实现代码,回答子问题。",
"explanation": "本题考查 Redis 分布式锁的核心原理,包括 SET NX EX 原子操作、过期续期、以及 Lua 脚本保证解锁的原子性。",
"code": "package lock\n\nimport (\n \"context\"\n \"time\"\n \"github.com/go-redis/redis/v8\"\n)\ntype RedisLock struct {\n client *redis.Client\n key string\n value string\n ttl time.Duration\n}\nfunc (l *RedisLock) Lock(ctx context.Context) (bool, error) {\n return l.client.SetNX(ctx, l.key, l.value, l.ttl).Result()\n}\nvar unlockScript = redis.NewScript(`\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"del\", KEYS[1])\n end\n return 0\n`)\nfunc (l *RedisLock) Unlock(ctx context.Context) error {\n _, err := unlockScript.Run(ctx, l.client, []string{l.key}, l.value).Result()\n return err\n}\nfunc (l *RedisLock) AutoRenew(ctx context.Context, stopCh <-chan struct{}) {\n ticker := time.NewTicker(l.ttl / 3)\n defer ticker.Stop()\n for {\n select {\n case <-ticker.C:\n l.client.Expire(ctx, l.key, l.ttl)\n case <-stopCh:\n return\n }\n }\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "代码中解锁操作使用 Lua 脚本而不是先 GET 再 DEL 的两步操作,最主要的原因是什么?",
"options": {
"A": "减少网络往返次数,提升性能",
"B": "保证 GET 判断和 DEL 删除的原子性,防止误删其他线程的锁",
"C": "Lua 脚本可以绕过 Redis 的单线程限制",
"D": "Redis 不支持 GET 和 DEL 的组合命令"
},
"answer": "B",
"explanation": "先 GET 再 DEL 的两步操作不是原子的:在 GET 之后、DEL 之前,锁可能刚好过期被其他客户端获取,此时当前客户端会误删别人的锁。Lua 脚本在 Redis 单线程中原子执行,确保 GET 和 DEL 在同一事务中完成,避免误删。"
},
{
"index": 2,
"type": "short_answer",
"question": "代码中 AutoRenew 方法使用 ttl/3 作为续期间隔,而非在锁过期后才续期。请分析这样设计的两个主要目的。",
"answer": "1) 防止业务未完成但锁已过期导致并发冲突;2) 留有余量避免续期过于频繁造成性能浪费",
"keywords": [
"防止锁过期",
"业务未完成",
"并发冲突",
"余量",
"性能"
],
"explanation": "使用 ttl/3 作为续期间隔有两个目的:第一,在锁自然过期前多次续期,确保业务执行期间锁始终有效,防止锁提前过期导致其他客户端获取锁产生并发冲突;第二,选择 ttl/3 而非更短的间隔,是在续期及时性和 Redis 请求频率之间的平衡,避免过度消耗 Redis 资源。"
},
{
"index": 3,
"type": "single_choice",
"question": "value 字段使用 UUID 作为唯一标识的主要作用是什么?",
"options": {
"A": "用于区分不同的锁实例",
"B": "作为锁的密钥,防止外部猜测",
"C": "确保只有持有该 value 的客户端才能解锁,实现锁的安全释放",
"D": "用于在 Redis 中存储锁的创建时间"
},
"answer": "C",
"explanation": "value 是锁持有者的唯一标识。在解锁时,Lua 脚本会先比较 value 是否匹配,只有当前持有者才能成功释放锁。这防止了一个客户端释放另一个客户端持有的锁(例如锁已过期被新客户端获取后,旧客户端仍在尝试解锁的场景)。"
}
],
"source": null,
"related": []
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 4,
"tags": [
"microservice",
"circuit-breaker"
],
"question": "阅读以下熔断器状态机实现代码,回答子问题。",
"explanation": "本题考查熔断器的三态转换机制(Closed / Open / Half-Open),以及 failureThreshold、successThreshold、timeout 等参数的作用。",
"code": "package circuit\nimport (\n \"sync\"\n \"time\"\n)\ntype CircuitBreaker struct {\n mu sync.Mutex; state string; failureCount int\n failureThreshold, successCount, successThreshold int\n lastFailureTime time.Time; timeout time.Duration\n}\nfunc (cb *CircuitBreaker) Execute(fn func() error) error {\n cb.mu.Lock()\n if cb.state == \"open\" {\n if time.Since(cb.lastFailureTime) > cb.timeout {\n cb.state = \"half-open\"; cb.successCount = 0\n } else {\n cb.mu.Unlock()\n return ErrCircuitOpen\n }\n }\n cb.mu.Unlock()\n err := fn()\n cb.mu.Lock()\n defer cb.mu.Unlock()\n if err != nil {\n cb.failureCount++\n cb.lastFailureTime = time.Now()\n if cb.failureCount >= cb.failureThreshold {\n cb.state = \"open\"\n }\n } else if cb.state == \"half-open\" {\n cb.successCount++\n if cb.successCount >= cb.successThreshold {\n cb.state = \"closed\"; cb.failureCount = 0\n }\n } else {\n cb.failureCount = 0\n }\n return err\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "当熔断器处于 open 状态且 timeout 时间已过,下次请求进入 Execute 时会发生什么?",
"options": {
"A": "直接返回错误,不做任何请求",
"B": "状态转为 half-open,执行实际请求进行探测",
"C": "状态直接转为 closed,恢复正常请求",
"D": "重置 failureCount 并重新开始计数"
},
"answer": "B",
"explanation": "代码中明确处理了该场景:当 state == \"open\" 且 time.Since(lastFailureTime) > timeout 时,将状态设为 half-open 并重置 successCount,然后放行实际请求。这是熔断器的半开探测机制,用于测试下游服务是否已恢复。"
},
{
"index": 2,
"type": "single_choice",
"question": "在 half-open 状态下,连续收到 successThreshold 次成功调用后熔断器如何转换?",
"options": {
"A": "保持 half-open 状态继续探测",
"B": "转为 open 状态",
"C": "转为 closed 状态,同时重置 failureCount",
"D": "转为 closed 状态,但保留 failureCount"
},
"answer": "C",
"explanation": "代码中当 state == \"half-open\" 且 successCount >= successThreshold 时,将 state 设为 \"closed\" 并将 failureCount 重置为 0。这表示下游服务已恢复正常,熔断器关闭,恢复正常流量。"
},
{
"index": 3,
"type": "short_answer",
"question": "代码中 open 状态下判断 timeout 后才放行探测请求,而不是直接拒绝所有请求。请说明这种设计的两个好处。",
"answer": "1) 为下游服务提供恢复窗口,避免永久阻断;2) 通过 half-open 状态渐进式恢复流量,避免雪崩",
"keywords": [
"恢复窗口",
"half-open",
"渐进",
"雪崩",
"探测"
],
"explanation": "第一,设置 timeout 后在 open 状态等待一段时间再探测,为下游服务提供了自我恢复的时间窗口,而不是永远拒绝所有请求。第二,通过 half-open 状态只放行部分请求进行探测,验证下游是否恢复,成功足够多次才切回 closed,这样可以渐进式恢复流量,避免突然大量请求冲击尚未完全恢复的服务造成雪崩。"
}
],
"source": null,
"related": []
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 5,
"tags": [
"microservice",
"distributed-transaction",
"idempotent"
],
"question": "阅读以下基于 Saga 模式的分布式事务实现代码,回答子问题。",
"explanation": "本题考查 Saga 模式的核心原理,包括正向执行链、补偿回滚、幂等设计等分布式事务关键概念。",
"code": "package saga\nimport (\n \"context\"\n \"fmt\"\n)\ntype Step struct {\n Name string\n Execute func(ctx context.Context, data *TxData) error\n Compensate func(ctx context.Context, data *TxData) error\n}\ntype TxData struct {\n OrderID string; Amount float64\n Results map[string]interface{}\n}\ntype Saga struct { steps []Step }\nfunc (s *Saga) Run(ctx context.Context, data *TxData) error {\n executed := make([]int, 0, len(s.steps))\n for i, step := range s.steps {\n if err := step.Execute(ctx, data); err != nil {\n fmt.Printf(\"Step [%s] failed: %v\\n\", step.Name, err)\n s.compensate(ctx, data, executed)\n return fmt.Errorf(\"saga aborted at %s: %w\", step.Name, err)\n }\n executed = append(executed, i)\n }\n return nil\n}\nfunc (s *Saga) compensate(ctx context.Context, data *TxData, executed []int) {\n for i := len(executed) - 1; i >= 0; i-- {\n step := s.steps[executed[i]]\n if err := step.Compensate(ctx, data); err != nil {\n fmt.Printf(\"Compensation for [%s] failed: %v\\n\", step.Name, err)\n }\n }\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "compensate 方法使用倒序遍历 executed 切片来执行补偿,这样设计的核心原因是?",
"options": {
"A": "倒序遍历的性能比正序遍历更好",
"B": "保证补偿操作按照与正向执行相反的顺序执行,维持数据一致性",
"C": "只有倒序才能访问切片的最后一个元素",
"D": "Go 语言的限制,不支持正序遍历 map"
},
"answer": "B",
"explanation": "Saga 模式的核心原则是补偿操作必须按正向操作的反序执行。例如:先扣库存再创建订单,补偿时应先取消订单再恢复库存。倒序遍历保证了这个语义,确保数据状态能够正确回滚。这类似于数据库事务日志的 undo 操作。"
},
{
"index": 2,
"type": "short_answer",
"question": "代码中补偿失败时仅打印日志而不抛出异常。请分析这样设计的原因,以及生产环境中应如何处理补偿失败。",
"answer": "补偿失败属于不可自动恢复的异常,需要人工介入;生产中应记录失败详情到持久化存储并触发告警通知",
"keywords": [
"人工介入",
"持久化",
"告警",
"重试",
"补偿日志"
],
"explanation": "补偿操作可能因网络抖动、下游服务不可用等原因失败,此时 Saga 已无法自动回滚,继续重试可能陷入死循环。代码选择记录日志而不是中断,是因为补偿失败需要人工介入处理。生产环境中应:1) 将补偿失败的详细信息(步骤名、参数、错误信息)写入持久化存储(如数据库或消息队列);2) 触发告警通知运维人员;3) 提供手动重试或回滚的运维工具。"
},
{
"index": 3,
"type": "single_choice",
"question": "TxData 中的 Results map 在 Saga 执行过程中起什么作用?",
"options": {
"A": "存储所有步骤的配置参数",
"B": "在步骤之间传递上下文数据,使后续步骤可以使用前序步骤的执行结果",
"C": "记录每个步骤的执行耗时",
"D": "缓存 RPC 调用的返回值以实现幂等"
},
"answer": "B",
"explanation": "TxData.Results 是一个共享的上下文数据结构。在 Saga 的正向执行链中,每个步骤可以将执行结果写入 Results,后续步骤通过读取 Results 获取前序步骤的产出。例如创建订单步骤将 orderID 写入 Results,扣款步骤就可以使用该 orderID 进行关联。这是 Saga 实现步骤间数据传递的标准方式。"
}
],
"source": null,
"related": []
},
{
"id": "cr-004",
"type": "code_reading",
"difficulty": 3,
"tags": [
"microservice",
"rate-limiting"
],
"question": "阅读以下基于令牌桶算法的限流器实现代码,回答子问题。",
"explanation": "本题考查令牌桶限流算法的核心机制,包括令牌生成速率、桶容量、以及突发流量处理。",
"code": "package ratelimit\nimport (\n \"sync\"\n \"time\"\n)\ntype TokenBucket struct {\n mu sync.Mutex; tokens float64; maxTokens float64\n refillRate float64; lastRefill time.Time\n}\nfunc NewTokenBucket(maxTokens, refillRate float64) *TokenBucket {\n return &TokenBucket{tokens: maxTokens, maxTokens: maxTokens,\n refillRate: refillRate, lastRefill: time.Now()}\n}\nfunc (tb *TokenBucket) Allow() bool {\n tb.mu.Lock()\n defer tb.mu.Unlock()\n tb.refill()\n if tb.tokens >= 1 { tb.tokens--; return true }\n return false\n}\nfunc (tb *TokenBucket) refill() {\n now := time.Now()\n tb.tokens += now.Sub(tb.lastRefill).Seconds() * tb.refillRate\n if tb.tokens > tb.maxTokens { tb.tokens = tb.maxTokens }\n tb.lastRefill = now\n}\nfunc (tb *TokenBucket) WaitN(n int) bool {\n tb.mu.Lock()\n defer tb.mu.Unlock()\n tb.refill()\n if tb.tokens >= float64(n) {\n tb.tokens -= float64(n)\n return true\n }\n return false\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "refill 方法中将 tokens 限制在 maxTokens 以下,这个上限的作用是什么?",
"options": {
"A": "防止浮点数溢出导致程序崩溃",
"B": "防止空闲期间累积过多令牌,限制突发流量上限",
"C": "确保每次 refill 后至少有一个令牌",
"D": "限制令牌桶的并发访问数量"
},
"answer": "B",
"explanation": "maxTokens 限制了令牌桶的最大容量,即允许累积的令牌上限。如果没有这个限制,空闲时间越长累积的令牌越多,恢复流量时会出现远超正常速率的突发请求,对下游服务造成冲击。maxTokens 限制了突发流量的上限,是令牌桶算法的流量整形特性。"
},
{
"index": 2,
"type": "single_choice",
"question": "该限流器采用惰性 refill 策略(在 Allow/WaitN 时才计算新增令牌),而非后台定时器持续补充。这种设计的优势是?",
"options": {
"A": "计算精度更高,不会丢失任何令牌",
"B": "无需后台 goroutine,减少资源开销,无请求时不消耗 CPU",
"C": "支持更细粒度的限流配置",
"D": "允许在多线程环境下避免使用互斥锁"
},
"answer": "B",
"explanation": "惰性 refill 在每次请求到来时才根据时间差计算应该补充的令牌数,不需要启动后台 goroutine 定时补充。这样在没有请求时完全不消耗 CPU 和内存资源,实现简单高效。虽然理论上可能有极微小的时间偏差,但在实际限流场景中完全可以接受。"
},
{
"index": 3,
"type": "short_answer",
"question": "WaitN 方法允许一次性消耗 n 个令牌。请说明这种设计在什么业务场景下有实际意义,并举一个具体例子。",
"answer": "适用于批量处理或权重不同的请求场景,例如:上传大文件消耗3个令牌、普通API消耗1个令牌",
"keywords": [
"批量",
"权重",
"大文件上传",
"不同成本",
"请求分级"
],
"explanation": "不同的 API 接口消耗的系统资源不同,权重限流可以让高成本操作消耗更多令牌。例如一个文件上传接口可能需要占用更多带宽和存储资源,消耗 3 个令牌;而一个简单的查询接口只消耗 1 个令牌。这比简单的 QPS 限流更精细,能更好地保护后端服务资源。"
}
],
"source": null,
"related": []
},
{
"id": "cr-005",
"type": "code_reading",
"difficulty": 4,
"tags": [
"microservice",
"service-discovery",
"config-center"
],
"question": "阅读以下服务注册与健康检查的实现代码,回答子问题。",
"explanation": "本题考查微服务架构中服务注册与发现的核心机制,包括服务注册、健康检查、实例剔除等。",
"code": "package discovery\nimport (\n \"sync\"\n \"time\"\n)\ntype ServiceInstance struct {\n ID string; Name string; Address string\n Port int; Status string\n}\ntype Registry struct {\n mu sync.RWMutex\n services map[string][]*ServiceInstance\n heartbeat map[string]time.Time; ttl time.Duration\n}\nfunc (r *Registry) Register(inst *ServiceInstance) {\n r.mu.Lock()\n defer r.mu.Unlock()\n r.services[inst.Name] = append(r.services[inst.Name], inst)\n r.heartbeat[inst.ID] = time.Now()\n}\nfunc (r *Registry) Heartbeat(id string) {\n r.mu.Lock()\n defer r.mu.Unlock()\n r.heartbeat[id] = time.Now()\n}\nfunc (r *Registry) evictLoop() {\n ticker := time.NewTicker(r.ttl / 2)\n defer ticker.Stop()\n for range ticker.C {\n r.mu.Lock()\n for _, instances := range r.services {\n for _, inst := range instances {\n if time.Now().Sub(r.heartbeat[inst.ID]) > r.ttl {\n inst.Status = \"DOWN\"\n }\n }\n }\n r.mu.Unlock()\n }\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "evictLoop 的巡检间隔设置为 ttl/2 而非 ttl,这样设计的目的是?",
"options": {
"A": "提高代码的可读性",
"B": "确保在实例心跳过期后最多经过 ttl/2 时间就能被发现并标记为 DOWN",
"C": "减少内存占用",
"D": "避免与 Heartbeat 方法产生死锁"
},
"answer": "B",
"explanation": "如果巡检间隔等于 ttl,则一个实例心跳过期后可能要等接近一个 ttl 的时间才会被发现和标记 DOWN,这意味着服务发现最长可能有 2×ttl 的延迟。使用 ttl/2 作为巡检间隔,可以在实例过期后最多 ttl/2 时间内就将其标记为不可用,将最长发现延迟控制在 1.5×ttl 以内。"
},
{
"index": 2,
"type": "single_choice",
"question": "代码中 heartbeat map 使用实例 ID 作为 key,而非 ServiceInstance 指针。这种设计在什么场景下会产生问题?",
"options": {
"A": "当两个不同服务的实例使用相同的 ID 时会导致数据覆盖",
"B": "当实例被 Deregister 后,heartbeat 中的记录不会自动清理,造成内存泄漏",
"C": "无法支持实例的端口动态变更",
"D": "会导致 RWMutex 的读写竞争加剧"
},
"answer": "B",
"explanation": "代码中 Register 方法同时写入 services 和 heartbeat,但缺少对应的 Deregister 方法来删除 heartbeat 中的记录。当实例下线后,services 列表可能通过某种方式清理,但 heartbeat map 中对应的 ID 和时间戳会一直保留,造成内存泄漏。生产环境中需要实现完整的注销逻辑,在实例下线时同步清理 heartbeat 记录。"
},
{
"index": 3,
"type": "short_answer",
"question": "代码中 evictLoop 只是将实例标记为 DOWN 而不直接删除,请分析这种设计的两个原因。",
"answer": "1) 保留实例信息便于监控和排查问题;2) 防止短暂网络抖动导致的误删,实例恢复心跳后可重新UP",
"keywords": [
"监控",
"排查",
"网络抖动",
"误删",
"恢复",
"最终一致"
],
"explanation": "第一,保留 DOWN 状态的实例信息便于运维监控——可以统计服务可用率、发现不健康实例并排查根因,如果直接删除就丢失了这些信息。第二,短暂的网络抖动可能导致心跳暂时丢失,但实例本身是健康的。标记 DOWN 而不删除,实例下次心跳到来时可以恢复 UP 状态,避免因瞬时抖动导致实例被永久剔除和重新注册的开销。"
}
],
"source": null,
"related": []
}
]
}