为什么这道题是一个好的面试题
这道题来自 Arthur O’Dwyer 的博客文章,核心思想是:给你一个真实项目的源码,要求你扩展它添加一个新功能
这与真实的日常工作完全一致——你需要接手不熟悉的代码库,理解其结构,找到扩展点,然后实现功能。
相比 LeetCode 式的算法题,这道题更能考察程序员的实际工程能力:
| LeetCode 算法题 | Memcached 扩展题 |
|---|---|
| 考察解题技巧 | 考察代码阅读能力 |
| 与实际工作脱节 | 贴近真实工作场景 |
| 只需要一个人完成 | 需要理解整体架构 |
| 没有历史包袱 | 需要处理既有代码的各种细节 |
考察技术应该像考察艺术家一样——看作品集,而不是问一些与实际工作无关的刁钻问题
Memcached
Memcached 是一个内存键值存储数据库(in-memory key-value store)。
为什么需要 Memcached?
传统的 PHP 应用是"请求-响应"模式:每次请求触发一个 PHP 脚本,处理完后就结束。脚本执行期间存储的变量状态无法保留到下一次请求。
解决方案是将状态存储到外部数据库(如 MySQL/PostgreSQL)。但对于简单应用,完整的关系型数据库是杀鸡用牛刀。
Memcached 的设计目的:
- 存储简单的键值对
- 数据存储在内存中,读写速度极快
- 支持设置过期时间(默认 3600 秒 = 1 小时)
- 支持原子操作(increment/decrement)避免竞态条件
基本使用
# 启动 memcached 服务./memcached# 使用 telnet 连接测试telnet localhost11211# 存储数据:set <key> <flags> <exptime> <bytes>setfull_name0360010John Smith# 读取数据:get <key>get full_name# 递增/递减操作(针对数值)setage03600237incr age1# 返回 38decr age1# 返回 37题目要求
实现一个新命令mot(multiply):与incr类似,但执行乘法而非加法。
mot <key> <delta>将指定键的值乘以 delta,然后返回新值。
实施步骤
1. 下载源码
# 创建工作目录mkdirmemcached_challengecdmemcached_challenge# 下载 2013 年版本的 memcached(与原博客保持一致)curl-Ohttps://www.youtube.com/redirect?q=https%3A%2F%2Fmemcached.org%2Ffiles%2Fmemcached-1.4.17.tar.gz2. 编译安装
tar-xzfmemcached-1.4.17.tar.gzcdmemcached-1.4.17# 查看配置选项./configure--help# 编译./configuremake-j3Memcached 源码规模:
- 总代码量约 27,000 行
- C 代码不到 10,000 行
- 依赖很少,是一个非常简单的项目
3. 分析源码结构
在 C 语言中,命令解析通常的实现方式:
// 伪代码展示命令处理的典型模式while(true){charline[256];read_line(line);// 按空格分割获取命令char*cmd=parse_command(line);switch(cmd){case"get":handle_get();break;case"set":handle_set();break;case"incr":handle_incr();break;case"decr":handle_decr();break;}}注意到:搜索字符串字面量"incr"可以快速定位命令分发代码。
grep-rn"incr"--include="*.c"--include="*.h"memcached-1.4.17/找到的线索:
t/目录下有 Perl 测试文件- 存在
process_arithmetic_command函数
4. 使用 Ctags 导航代码
# 生成标签文件find.-name"*.c"-o-name"*.h"|xargsctags# 在 Vim 中跳转到函数定义# 按 Ctrl-] 跳转到光标下词语的定义# 按 Ctrl-t 返回5. 核心代码分析
process_arithmetic_command 函数
// 原始实现(伪代码)voidprocess_arithmetic_command(conn*c,token_t*tokens,size_tntokens,bool increment){// 从 tokens 中提取 key 和 deltachar*key=tokens[1].value;uint64_tdelta=strtoull(tokens[2].value,NULL,10);// 调用 add_delta 执行实际操作add_delta(c,item,delta,increment);}命令分发
在memcached.c中找到:
// 命令分发部分if(ntokens==4&&strcmp(tokens[COMMAND_TOKEN].value,"incr")==0){process_arithmetic_command(c,tokens,ntokens,true);// true = increment}elseif(ntokens==4&&strcmp(tokens[COMMAND_TOKEN].value,"decr")==0){process_arithmetic_command(c,tokens,ntokens,false);// false = decrement}6. 实现方案
问题分析
原代码使用布尔值表示操作类型:
true= incrementfalse= decrement
要添加乘法,需要将布尔值改为枚举类型:
// 修改前voidprocess_arithmetic_command(conn*c,token_t*tokens,size_tntokens,bool increment);// 修改后typedefenum{OP_DECREMENT=0,OP_INCREMENT=1,OP_MULTIPLY=2}arithmetic_op_t;voidprocess_arithmetic_command(conn*c,token_t*tokens,size_tntokens,arithmetic_op_top);修改点 1:函数签名
// memcached.h 或对应头文件typedefenum{OP_DECREMENT=0,OP_INCREMENT=1,OP_MULTIPLY=2}arithmetic_op_t;voidprocess_arithmetic_command(conn*c,token_t*tokens,size_tntokens,arithmetic_op_top);修改点 2:命令分发
// 在 memcached.c 的命令处理部分添加elseif(ntokens==4&&strcmp(tokens[COMMAND_TOKEN].value,"mot")==0){process_arithmetic_command(c,tokens,ntokens,OP_MULTIPLY);}修改点 3:process_arithmetic_command 函数内部
将if (increment)改为switch (op):
switch(op){caseOP_DECREMENT:res=do_add_delta(c,item,delta,false,&cas);/* 更新 decrement 统计 */break;caseOP_INCREMENT:res=do_add_delta(c,item,delta,true,&cas);/* 更新 increment 统计 */break;caseOP_MULTIPLY:// 乘法的实现逻辑res=do_multiply(c,item,delta,&cas);/* 更新 multiply 统计 */break;}修改点 4:do_add_delta 函数支持乘法
// 原始 add_delta 调用 do_add_delta// do_add_delta(c, item, delta, is_incr, &cas);// 需要添加 do_multiply 函数uint64_tdo_multiply(conn*c,item*it,uint64_tdelta,uint64_t*cas){uint64_tvalue=item_get_cas(it);value=value*delta;// ... 存储回 itemreturnvalue;}修改点 5:add_delta 函数的签名和实现
需要让add_delta接受arithmetic_op_t类型,或者创建新函数multiply_item。
7. 测试验证
# 启动 memcached./memcached-d-p11211# 连接测试telnet localhost11211# 设置初始值setage03600237# 递增incr age1# 返回: 38# 乘法(新命令)mot age10# 返回: 380添加统计支持
Memcached有统计机制,可以追踪各种操作的成功/失败次数。
统计相关结构
// stats.h 或对应文件structstats{uint64_tincr_hits;uint64_tincr_misses;uint64_tdecr_hits;uint64_tdecr_misses;// 需要添加:uint64_tmul_hits;uint64_tmul_misses;};需要修改的统计相关位置
- threadlocal_stats 聚合:合并各线程的统计
- server_stats 输出:返回给客户端的统计信息
- stats 命令处理:添加新的统计项输出
- 测试文件:更新预期输出
// 添加 mul_hits 和 mul_misses 到各个统计相关位置测试统计
# 查看统计stats# 应该看到新增的统计项:STAT mul_hits1STAT mul_misses0运行测试
# 运行所有测试maketest# 或者直接运行测试脚本perl t/*.t注意:添加新统计后,原有测试可能会失败,因为测试中硬编码了预期的统计输出。需要更新测试文件以适应新的统计项。
完整补丁
最终实现的修改包括:
- 添加 mul 命令解析:在命令分发部分添加
mot命令的处理 - 修改 process_arithmetic_command:将布尔参数改为枚举类型
- 实现乘法逻辑:在
do_add_delta或新函数中实现乘法 - 添加统计支持:在 stats 结构中添加
mul_hits和mul_misses - 更新统计处理:在所有统计相关位置添加新统计项的处理
思维过程
思维方式:
科学方法:提出假设 → 测试验证
- 假设命令通过字符串匹配分发 → 搜索 “incr” 验证
代码导航策略:
- 使用 ctags/ctags 快速跳转函数定义
- 理解调用栈,从上到下追踪
分而治之:
- 先实现核心功能(命令能被解析)
- 再完善周边功能(统计、测试)
避免过早优化:
- 统计功能不是核心需求,先跳过
- 只在主功能完成后才处理
持续沟通-授之以渔:
- 不断解释自己在做什么
- 记录的思维过程
代码质量观察
对 memcached 代码的一些评价:
优点:
- 代码规模小,易于理解
- 核心逻辑清晰
可改进之处:
- 添加新统计需要修改 7-8 个地方
- 函数命名与实际功能不完全匹配(如
add_delta实际上做的是 increment/decrement) - 测试中使用硬编码的预期值,添加新功能时需要手动修改
建议:更好的设计是使用枚举表示所有算术操作,而不是只有 increment/decrement 两个布尔选项。
结论
模拟code场景:
- 需要阅读和理解陌生代码
- 需要找到合适的扩展点
- 需要处理代码的历史包袱
- 需要关注边界情况和错误处理
快速定位和解决问题的能力。完整地添加统计支持则需要更多时间,因为需要理解整个统计系统的运作方式。