一、总体架构
面试官:
先说一下你这个项目里的内存池整体架构是什么?别只说"有个池子",要讲清楚层次和设计目标。
应聘者:
这个项目里的内存池是一个"全局分桶 + 空闲链表 + 批量扩容 "的设计,核心目标是减少频繁 malloc/free 的系统开销,尤其是 key/value、节点、响应缓冲区这些小对象的频繁申请释放。
整体上有三层:
cpp
kvs_malloc / kvs_free
↓
mem_pool_alloc / mem_pool_free
↓
free_lists[256] + chunks + 统计信息
其中:
kvs_malloc()是统一入口kvs_free()是统一回收入口- 真正的池子逻辑在
src/mempool.c - 对外接口在
include/mempool.h - 在
src/kvstore.c里通过#if MEM切换到内存池或系统 malloc
我这个实现不是自己造一个完整的通用 allocator,而是针对项目里的典型对象做了一个简化版内存池,优先服务小对象和高频对象。
二、内存块怎么设计的
面试官:
内存池里的"块"是怎么设计的?用户拿到的内存长什么样?
应聘者:
每个分配出来的小块,前面都带一个头部 mem_block_t,然后后面才是用户真正可用的内存。
结构是这样的:
cpp
[mem_block_t 头部][用户可用内存]
mem_block_t 里主要记录:
next:空闲链表指针size:块大小magic:校验标记,防止误释放kind:小块还是大块class_idx:属于哪个桶reserved:预留字段
这样设计的好处是:
free时能从用户指针倒推出块头- 能区分这块内存是池子分配的还是系统大块
- 能快速知道应该挂回哪个空闲链表
三、为什么要分桶
面试官:
为什么要搞 16 字节对齐和 256 个桶?
应聘者:
因为项目里的对象大小分布比较离散,但常见请求大多还是小对象。
我这里采用的是:
- 16 字节对齐
16 ~ 4096共 256 个规格
比如:
- 16B -> bucket 0
- 32B -> bucket 1
- 48B -> bucket 2
- ...
- 4096B -> bucket 255
这样做是为了:
- 减少外部碎片
- 简化分配逻辑
- 提高复用率
- 避免每次都找"最合适的块"造成复杂度上升
这更像一个"固定粒度 slab"思路,而不是精细化的 buddy allocator。
四、实际怎么运行
面试官:
具体说说运行流程。比如申请一个 32 字节的小块,整个过程是怎样的?
应聘者:
以 32 字节为例,流程是:
cpp
kvs_malloc(24)
↓
mem_pool_alloc(24)
↓
向上对齐到 32
↓
算出 idx = 1
↓
看 free_lists[1] 有没有空闲块
↓
如果没有,mem_pool_grow(1, 32)
↓
一次申请 64 个 32B 块
↓
切块后头插进 free_lists[1]
↓
从链表头取一个块返回给用户
所以用户最后拿到的是:
cpp
[block头部后面的那一段用户空间]
不是整个块头。
释放的时候则反过来:
cpp
用户指针 ptr
↓
倒推一个 mem_block_t
↓
检查 magic
↓
如果是小块,头插回对应 free_list
五、mem_pool_grow() 在干什么
面试官:
批量扩容函数 mem_pool_grow() 的逻辑你讲一下,尤其是头插法为什么这么做。
应聘者:
mem_pool_grow() 的职责是:当某个桶空了,就一次性向系统申请一大块连续内存,然后切成多个小块,挂到对应的空闲链表里。
它的关键步骤是:
-
计算单块大小:
cppone_block_size = sizeof(mem_block_t) + block_size; -
申请一批:
cpptotal_size = one_block_size * MEMPOOL_BATCH_COUNT; -
申请一个
mem_chunk_t记录这次大申请 -
申请真正的大内存
memory -
把
memory切成 64 个小块 -
每个小块都头插进
g_pool.free_lists[idx]
头插法就是:
cpp
block->next = g_pool.free_lists[idx];
g_pool.free_lists[idx] = block;
它的特点是:
- O(1) 插入
- 不用找尾巴
- 代码简单
- 分配时也是从头取,天然匹配
六、kvs_malloc / kvs_free 的意义
面试官:
你为什么还要包一层 kvs_malloc 和 kvs_free?直接用 mem_pool_alloc 不行吗?
应聘者:
包这一层的价值是统一入口和可切换性。
现在在 src/kvstore.c 里:
cpp
void *kvs_malloc(size_t size) {
#if MEM
return mem_pool_alloc(size);
#else
return malloc(size);
#endif
}
cpp
void kvs_free(void *ptr) {
#if MEM
mem_pool_free(ptr);
#else
free(ptr);
#endif
}
这样做的好处:
- 业务代码不用关心底层是不是内存池
- 可以通过宏一键切换
- 方便调试和对比性能
- 持久化、网络、数据结构都能统一使用
也就是说,内存池不是独立给某一个模块服务的,而是整个服务端统一的内存入口。
七、哪些地方在用它
面试官:
这个池子具体服务了哪些地方?只是 key/value 吗?
应聘者:
不是。它服务的是整个项目里所有通过 kvs_malloc/kvs_free 走的路径。
典型包括:
array里的 key/valuehash的节点、key/valuerbtree的节点、key/valueskiplist的节点、key/value/forward- 网络响应缓冲区
- 持久化临时 buffer
- 读写命令时的请求副本
也就是说,池子不是只管数据存储本体,而是覆盖了整个服务运行时。
八、为什么要有 magic
面试官:
magic 字段是干什么的?这不是多此一举吗?
应聘者:
不是多此一举,它是一个轻量级的安全校验。
mem_pool_free() 里会先检查:
cpp
if (block->magic != MEMPOOL_MAGIC) return;
作用是:
- 防止错误指针被误释放
- 防止普通
malloc的地址被当成池子块处理 - 在调试阶段更容易定位问题
尤其是这种项目里,数据结构经常嵌套分配,magic 能帮你早点发现"对象生命周期乱了"的 bug。
九、它有哪些局限
面试官:
这个内存池现在有什么不足?
应聘者:
有几个明显局限:
-
不是线程安全的
现在是全局池,没有加锁,多线程下需要保护 free_list 和统计信息。
-
大对象直接走系统 malloc
这不是 bug,是设计选择,但如果超大正文很多,池子收益会下降。
-
没有真正的内存回收策略
chunks只负责生命周期结束时统一释放,不会在运行中主动缩回去。 -
没有按业务对象定制尺寸
现在是通用 16B 粒度,比较朴素,后面可以做热点 size class 优化。
-
没有页级整理/碎片压缩
还没有 Redis 那种 active defrag 思路。
十、和 Redis 的差别
面试官:
你这个和 Redis 比,有什么差别?
应聘者:
Redis 不主要靠自己写的"通用内存池",而是更依赖底层 allocator,比如 jemalloc,同时在对象层做压缩:
- 小字符串可能走
embstr - 整数直接走
int - 紧凑集合编码减少分配
- 还会做主动碎片整理
我这个项目更像是:
- 自己实现一个小型通用内存池
- 统一承接服务里大部分小对象
- 对象层还没做到 Redis 那么激进的压缩
所以我这版更适合学习 allocator 原理,Redis 更适合学习工程级内存管理策略。
十一、一个具体例子
面试官:
你举个完整例子,说清楚一个 key/value 从申请到释放,再到保存,是怎么走的。
应聘者:
比如执行:
cpp
SET name Tom
写入时
cpp
协议解析
↓
kvs_array_set / kvs_hash_set / kvs_rbtree_set / kvs_skiplist_set
↓
kvs_malloc(key)
↓
kvs_malloc(value)
↓
对象进入容器
删除时
cpp
DEL name
↓
查找对应对象
↓
kvs_free(key)
↓
kvs_free(value)
↓
kvs_free(node or table slot)
保存时
cpp
SAVE
↓
persist_save_all
↓
遍历所有引擎
↓
把 key/value 序列化进临时 buffer
↓
io_uring 写盘
↓
rename 成正式文件
注意:
SAVE 不会释放业务对象,它只是把内存里的内容"拍成照片"写到文件里。
十二、怎么总结这个项目的内存池
面试官:
最后一句话总结一下你的内存池。
应聘者:
这是一个面向 KV 服务的轻量级分桶内存池,采用 16B 粒度、256 个大小类、批量扩容、头插空闲链表和块头标记的设计,统一通过 kvs_malloc/kvs_free 接入业务层,主要优化频繁小对象申请释放的场景,同时为持久化、网络和容器对象提供统一的内存管理入口。
追问式面试
1. 面试官:为什么 mem_pool_free() 不能直接 free(ptr)?
应聘者:
因为 ptr 不是块头地址,而是用户拿到的地址。
在 9.1-kvstore\\src\\mempool.c里,真正释放时要先:
cpp
mem_block_t *block = ((mem_block_t *)ptr) - 1;
也就是说,要先倒推回头部,再判断它是不是池子分配出来的。
如果直接 free(ptr):
- 对小块来说,
ptr根本不是系统malloc返回的原始地址 - 会导致非法释放
- 也破坏了池子的空闲链表管理
2. 面试官:为什么要有 magic 字段?
应聘者:
magic 是防误判的保护。
在 mem_pool_free() 里会检查:
cpp
if (block->magic != MEMPOOL_MAGIC) return;
作用是:
- 防止把非池子内存误当成池子块
- 防止双重释放或脏指针误挂回链表
- 调试时能更快定位问题
这个字段不增加太多成本,但能显著提高健壮性。
3. 面试官:为什么要有 cached_count,还要遍历链表统计一次?
应聘者:
cached_count 是运行时维护的计数,遍历链表是校验手段。
在 9.1-kvstore\\src\\mempool.c的 mem_pool_dump() 里,实际上做了两套统计:
- 平时增减维护的
g_pool.cached_count - 现场遍历
free_lists[i]算出来的total_cached
这样可以检查:
- 计数逻辑有没有漏加、漏减
- 链表有没有被破坏
- 某个地方是不是误释放了
换句话说,cached_count 是"平时记账",遍历是"审计"。
4. 面试官:为什么 mem_pool_grow() 一次申请 64 个小块?这个数字怎么来的?
应聘者:
64 是一个经验值,不是标准答案。
在 9.1-kvstore\\src\\mempool.c 里:
cpp
#define MEMPOOL_BATCH_COUNT 64u
它的目的就是平衡两件事:
- 批量太小:系统
malloc调用太频繁 - 批量太大:一次性占太多内存,浪费会更明显
64 是一个比较保守、好理解、适合学习阶段的值。
如果后面压测发现某些桶特别热,可以把它改成"按桶自适应"。
5. 面试官:为什么要对齐到 16 字节?
应聘者:
因为 16 字节对齐能兼顾三件事:
- 简化桶划分
- 保持结构体和指针访问友好
- 减少一些低效的奇怪尺寸
在 9.1-kvstore\\src\\mempool.c 里:
cpp
return (size + MEMPOOL_ALIGN - 1u) & ~(MEMPOOL_ALIGN - 1u);
这会把任意 size 向上补到 16 的倍数。
比如:
- 1 -> 16
- 17 -> 32
- 33 -> 48
这样桶的编号也很好算。
6. 面试官:为什么小块释放后不还给系统?
应聘者:
因为这个池子的目标就是"复用"。
小块释放后只是挂回对应 free_list,以后再申请时直接拿来用。
如果每次都还给系统,就又变成 malloc/free 了,池子的意义就没了。
这个策略特别适合:
- 频繁创建/销毁的 key/value
- 网络缓冲区
- 持久化临时缓冲区
也就是典型的"短命、小尺寸、重复出现"的对象。
7. 面试官:那大对象为什么还是走系统 malloc?是不是内存池失效了?
应聘者:
不是失效,而是分工不同。
当前池子只覆盖 <=4096 的小对象。
大对象走系统 malloc,原因有三个:
- 大对象大小变化太大,不适合固定桶
- 如果硬塞进池子,容易造成严重浪费
- 学习阶段先把高频小对象优化好,收益更直接
所以正确理解是:
- 小对象靠池子
- 大对象靠系统分配器
这和 Redis 的思路也类似,不是所有东西都强行池化。
8. 面试官:chunks 链表到底是干嘛的?
应聘者:
chunks 是"记录池子向系统申请过的整块大内存"的管理链表。
在 9.1-kvstore\\src\\mempool.c 里:
cpp
mem_chunk_t *chunks;
它的意义是:
- 方便程序退出时统一释放整批内存
- 不需要单独记住每个小块
- 避免遍历所有 free_list 逐个 free
free_list 管的是"小块复用",chunks 管的是"来源回收"。
9. 面试官:如果一个指针不是你池子分配出来的,kvs_free 会怎样?
应聘者:
mem_pool_free() 会先检查 magic。
如果不是池子内存,它直接返回,不做危险动作。
这是一种保守策略。
它不会帮你兜底所有错误,但能避免把乱指针继续挂进链表导致更严重的破坏。
10. 面试官:这个内存池在并发下安全吗?
应聘者:
当前版本不是线程安全的。
因为全局共享:
g_pool.free_listsg_pool.chunkscached_count / active_count
这些字段在并发 alloc/free 时都需要保护。
如果以后扩展到多线程,至少要加:
- 全局锁
- 或每个桶一把锁
- 更进一步可以做 thread-local cache
现在这个版本更适合单线程或者外层已经串行化的场景。
11. 面试官:为什么 SAVE 要先写 .tmp 再 rename?
应聘者:
为了避免半写文件污染正式快照。
在 9.1-kvstore\\src\\persist.c 里:
cpp
tmp_path = "%s.tmp"
write tmp
rename(tmp, path)
12. 面试官:你这个池子最核心的价值到底是什么?
应聘者:
是把项目里大量"小而频繁"的分配统一收口,减少系统分配次数,并且让释放路径更可控。
它真正服务的是:
- 节点对象
- key/value 缓冲
- 网络请求/响应缓冲
- 持久化临时 buffer
所以它不是"万能池",而是"针对 KV 服务工作负载的高频对象池"。
13. 面试官:如果让我继续优化,你最想改什么?
应聘者:
我会优先改两点:
-
把
hash/rbtree/skiplist的节点再压实- 减少多次
kvs_malloc - 尽量把 node/key/value 放进一块连续内存
- 减少多次
-
给热点桶做统计和自适应
- 看哪些 size class 最热
- 再决定 batch size 和桶策略
这两项比盲目扩大池子更有意义。
补充:
1. 面试官:为什么你的 free list 用单链表,不用双链表?
应聘者:
因为这个场景只需要两件事:头插和头删,单链表就够了。
在内存池里:
- 分配时:从头部取一个块
- 释放时:把块头插回链表头
这两个操作都只需要 O(1),不需要访问前驱节点,所以没必要用双链表。
单链表更省空间,mem_block_t 头部也更轻。
2. 面试官:为什么 magic 字段比单纯看 size 更可靠?
应聘者:
因为 size 只能说明"像不像某种块",不能证明"是不是这块池子里的块"。
magic 的作用是做身份校验:
cpp
if (block->magic != MEMPOOL_MAGIC) return;
它能防止:
- 非池子内存误 free
- 脏指针被当成合法块
- 头部被破坏后继续挂回链表
所以 magic 是一种低成本的防御机制。
3. 面试官:mem_pool_align() 那句位运算为什么能对齐?
应聘者:
它的作用是把任意 size 向上补到 16 的倍数:
cpp
(size + 15) & ~15
例如:
- 1 -> 16
- 17 -> 32
- 33 -> 48
原理是:
- 先加上
align - 1 - 再把低位清零
这样可以把 size 映射到固定桶里。
4. 面试官:mem_pool_grow() 为什么要先申请 mem_chunk_t,再申请真正的大内存?
应聘者:
因为 mem_chunk_t 是管理节点,不是用户数据。
它的职责是记住这一整块大内存的来源,方便程序退出时统一释放:
cpp
chunk->memory = memory;
chunk->next = g_pool.chunks;
g_pool.chunks = chunk;
这样销毁池子时,只要顺着 chunks 走,就能把系统申请过的所有大块内存统一回收掉。
5. 面试官:为什么 free_lists[idx] 用头插法,不用尾插法?
应聘者:
因为头插最简单,复杂度最低。
头插只需要:
cpp
block->next = g_pool.free_lists[idx];
g_pool.free_lists[idx] = block;
尾插则需要遍历到尾部,复杂度是 O(n)。
在内存池里,释放动作很频繁,所以释放路径必须尽量短。
6. 面试官:你的池子为什么适合小对象,不适合大对象?
应聘者:
因为池子最擅长处理的是:
- 大小固定
- 频繁申请释放
- 分配模式重复
而大对象通常:
- 尺寸变化大
- 单次占用高
- 复用概率低
如果硬把大对象也纳入固定桶,会导致:
- 空间浪费
- 桶设计复杂化
- 复用收益不高
所以我这里把 4096 以上的大对象直接交给系统 allocator,这是更合理的分层。
7. 面试官:你这个池子和 Redis 的思路到底差在哪?
应聘者:
Redis 更偏向"对象层压缩 + 底层 allocator 优化 ",我这个项目更偏向"自己实现一个通用小对象池"。
Redis 会尽量减少分配次数,比如:
- 小字符串用
embstr - 整数直接用整数编码
- 紧凑结构用
listpack之类
我的实现则是:
- 所有业务对象统一走
kvs_malloc - 小对象走我自己的池子
- 大对象直接走系统分配
Redis 更工程化,我这个更适合学习 allocator 原理。
8. 面试官:你的内存池现在最大的风险是什么?
应聘者:
两个:
-
不是线程安全的
全局
g_pool没有锁,多线程下会出问题。 -
对象内部指针的生命周期要手动管理
kvs_free(node)不会自动释放node->key、node->value,所以业务层必须自己维护好层次释放顺序。
9. 面试官:cached_count 和实际链表长度为什么要双重维护?
应聘者:
这是为了做一致性校验。
cached_count是运行时记账,快- 遍历链表是现场审计,准
如果两者不一致,说明:
- 某个地方漏减或漏加
- 链表被破坏
- 释放/分配逻辑有 bug
这个设计在调试阶段很有价值。
11. 面试官:你怎么证明这个池子真的有用?
应聘者:
看三个指标:
system_alloc_count是否明显下降reuse_count是否明显上升- 实际运行中的延迟是否更稳定
如果业务里大量小对象反复申请释放,那么池子能明显减少系统调用和堆碎片。
12. 面试官:一句话总结你的内存池设计。
应聘者:
这是一个面向 KV 服务的小对象分桶内存池,采用 16 字节对齐、256 个规格、批量扩容和头插空闲链表的方式,统一服务业务对象、网络缓冲和持久化临时对象,重点优化高频小内存申请释放场景。
继续压力测试
1. 面试官:为什么你这个内存池要做成全局的,而不是每个模块一个池子?
应聘者:
因为这个项目的对象类型很多,但生命周期模式很相似,都是频繁申请、频繁释放的小对象。做成全局池可以统一管理,减少重复实现,也能让统计更完整,像 cached_count、reuse_count 这些信息都能集中观察。
如果每个模块单独一个池,虽然隔离性更强,但会增加复杂度,还会导致不同池之间出现"有的很空、有的很忙"的资源不均衡问题。
2. 面试官:为什么池子用 16 字节对齐,而不是 8 或 32?
应聘者:
16 字节是一个比较平衡的粒度。
如果太小,比如 8 字节,会导致桶更多,管理更碎;如果太大,比如 32 字节,小对象浪费会明显变大。
16 字节对齐对结构体、指针和常见字符串长度都比较友好,也方便用位运算快速计算桶编号。
3. 面试官:mem_pool_class_index() 为什么是 (size / 16) - 1,不会有问题吗?
应聘者:
它只在 size 已经向上对齐到 16 的前提下使用,所以 16 会映射到 0,32 会映射到 1,依次类推。
如果不先对齐,确实会错,比如 17 直接算就会得到 0,但实际应该进入 32B 桶。所以正确流程一定是先 mem_pool_align() 再算桶号。
4. 面试官:mem_block_t 为什么要把 magic、kind、class_idx 都放进去?
应聘者:
这几个字段分别解决不同问题:
magic:判断这是不是池子分配的块kind:判断是小块还是大块class_idx:释放时知道挂回哪个 free list
它们让 free 路径能自描述,不需要额外查表。
5. 面试官:为什么 mem_pool_grow() 是先申请管理节点,再申请大内存?
应聘者:
因为 mem_chunk_t 的职责只是记录"大内存来源",方便最后销毁。
如果先申请大内存,再申请管理节点,万一管理节点申请失败,会多出一块无法登记的内存,回收就麻烦。现在这个顺序更稳妥。
6. 面试官:你这里的 chunks 和 free_lists 分别负责什么?
应聘者:
free_lists 负责"可复用的小块池",按规格分桶。
chunks 负责"所有从系统申请来的大块来源记录",用于销毁时统一释放。
一个管复用,一个管回收来源,不是同一个职责。
7. 面试官:为什么 mem_pool_free() 对小块不直接 free,而是挂回链表?
应聘者:
因为内存池的核心目标就是复用。
如果每次释放小块都还给系统,那就退化成普通 malloc/free 了,池子的价值就没了。
挂回链表后,下次申请同规格对象时可以直接复用,减少系统调用和碎片。
8. 面试官:你怎么保证 mem_pool_free() 不会把非法指针挂回链表?
应聘者:
靠 magic 校验。
释放时先从用户指针倒推回块头,再检查 block->magic 是否等于 MEMPOOL_MAGIC。
如果不是,就直接返回,不参与链表操作。这样能防止误释放破坏池子结构。
9. 面试官:为什么 mem_pool_dump() 要做链表遍历校验?
应聘者:
因为运行时维护的计数可能出错,而链表遍历是现场真实状态。
双重检查可以帮助发现:
- 计数有没有漏加漏减
- 链表有没有断
- 释放逻辑有没有问题
这个对调试特别有价值。
10. 面试官:你的大对象为什么直接走系统 malloc?这和池子的目标冲突吗?
应聘者:
不冲突,这是分层设计。
池子最适合固定规格、频繁复用的小对象;大对象通常变化大、复用少、尺寸不稳定,硬纳入池子反而会浪费严重。
所以小对象池化,大对象交给系统 allocator,这是合理的职责划分。
12. 面试官:如果项目以后变成多线程,你这套池子会出什么问题?
应聘者:
现在这套不是线程安全的。
共享的 g_pool.free_lists、g_pool.chunks 和统计字段在并发下都会有竞争条件,可能导致链表损坏。
如果要支持多线程,至少要加全局锁,或者按桶加锁;更进一步可以做 thread-local cache,减少竞争。
13. 面试官:为什么 mem_pool_destory() 是在 dest_kvengine() 里最后调用?
应聘者:
因为前面的容器销毁会先释放自己持有的节点、key、value 等对象,最后再统一销毁内存池。
如果先销毁池子,后面容器再尝试 kvs_free,就会访问已经失效的池子状态。
所以顺序必须是:先容器,后池子。
15. 面试官:如果让你总结这个内存池设计的核心价值,你会怎么说?
应聘者:
它的核心价值是把项目里高频出现的小对象分配统一收口,减少系统 malloc/free 的频率,降低碎片,提高复用率,并且通过 magic、kind、class_idx 和统计信息,让分配和释放路径更可控、更容易调试。
3. 面试官:那小块到底什么时候真正 free 掉?
应聘者:
在 mem_pool_destory() 里统一释放。
流程是:
小块释放 -> 回到 free_list
池子销毁 -> 遍历 chunks
-> free(chunk->memory)
-> free(chunk)
也就是说,小块平时只是"缓存",最后由 chunks 统一还给系统。
7. 面试官:你这个内存池本质上更像什么?
应聘者:
更像一个简化版 slab / freelist allocator。
特点是:
- 固定粒度对齐
- 按规格分桶
- 空闲链表复用
- 批量扩容
- 退出时统一销毁
它不是一个完整通用 allocator,但足够支撑这个 KV 项目的高频小对象场景。
8. 面试官:你在项目里为什么还要保留系统 malloc/free?
应聘者:
因为池子只适合小且固定的对象。
大对象,比如超长 value 或者很大的临时 buffer,直接走系统分配更合理。
所以这里是分层设计:
- 小对象:池子
- 大对象:系统 allocator
这不是"池子失效",而是"池子只做它最擅长的部分"。
9. 面试官:你的持久化层为什么也会用 kvs_malloc?
应聘者:
因为持久化层也有大量临时对象,比如:
persist_buf_t扩容tmp_path- load 时的
key_buf、value_buf - response 缓冲
这些都是短命对象,走 kvs_malloc 也能受益于池子,减少系统分配次数。
10. 面试官:persist_save_all() 为什么要先统计 record 数量?
应聘者:
因为文件头里要写 record_count。
保存时先统计总记录数,后面加载时就能准确知道要读多少条。
它的流程是:
统计各引擎条数
↓
写文件头
↓
遍历所有记录
↓
写到临时 buffer
↓
落盘
这样文件格式自描述更强,恢复时也更稳。
11. 面试官:为什么 SAVE 要写 .tmp 再 rename?
应聘者:
这是为了防止半写文件污染正式快照。
如果直接写 kvstore.data,写到一半崩了,正式文件就坏了。
先写 .tmp,成功后 rename 成正式文件,更接近原子替换,安全性高很多。
12. 面试官:为什么启动恢复顺序一定是先 kvstore.data 再 kvstore.aof?
应聘者:
因为 kvstore.data 是全量基线,kvstore.aof 是基线之后的增量。
如果顺序反了,后读的快照会把前面回放的增量覆盖掉,恢复就错了。
所以正确顺序是:
先快照
再增量
13. 面试官:你这个内存池和 Redis 的差距在哪里?
应聘者:
Redis 更偏向工程化:
- 底层依赖 jemalloc 这类 allocator
- 小字符串做
embstr - 整数直接内嵌
- 紧凑结构减少分配
- 还有主动碎片整理
我这个更像教学版的通用小对象池,适合理解 allocator 原理,但工程细节没 Redis 那么深。
14. 面试官:这个池子最明显的局限是什么?
应聘者:
三个:
- 不是线程安全的
- 大对象还是靠系统 malloc
- 还没做更细的热点优化,比如按业务对象定制块布局
15. 面试官:如果让你下一步优化,你会先改哪里?
应聘者:
我会先改数据结构对象的分配方式,把 hash / rbtree / skiplist 里多次 kvs_malloc 的地方尽量压成一块连续内存。
这样能进一步减少碎片,也更像 Redis 的对象布局思路。