HashLfuCache
声明:代码来自代码随想录知识星球
KHashLfuCache
是高并发场景下 LFU 缓存的优化版本:通过 “哈希分片” 解决了单 LFU 的 “全局锁竞争” 和 “单点负载过高” 问题,同时完整保留了 LFU“按访问频次淘汰数据 + 老化机制” 的核心特性,且通过分片容量拆分(非额外扩容)实现优化,适合需要高并发访问且依赖 “频次敏感” 淘汰策略的场景(如高频数据查询、热点内容缓存等)。
KHashLfuCache代码如下:
// 并没有牺牲空间换时间,他是把原有缓存大小进行了分片。
template<typename Key, typename Value>
class KHashLfuCache
{
public:KHashLfuCache(size_t capacity, int sliceNum, int maxAverageNum = 10): sliceNum_(sliceNum > 0 ? sliceNum : std::thread::hardware_concurrency()), capacity_(capacity){size_t sliceSize = std::ceil(capacity_ / static_cast<double>(sliceNum_)); // 每个lfu分片的容量for (int i = 0; i < sliceNum_; ++i){lfuSliceCaches_.emplace_back(new KLfuCache<Key, Value>(sliceSize, maxAverageNum));}}void put(Key key, Value value){// 根据key找出对应的lfu分片size_t sliceIndex = Hash(key) % sliceNum_;lfuSliceCaches_[sliceIndex]->put(key, value);}bool get(Key key, Value& value){// 根据key找出对应的lfu分片size_t sliceIndex = Hash(key) % sliceNum_;return lfuSliceCaches_[sliceIndex]->get(key, value);}Value get(Key key){Value value;get(key, value);return value;}// 清除缓存void purge(){for (auto& lfuSliceCache : lfuSliceCaches_){lfuSliceCache->purge();}}private:// 将key计算成对应哈希值size_t Hash(Key key){std::hash<Key> hashFunc;return hashFunc(key);}private:size_t capacity_; // 缓存总容量int sliceNum_; // 缓存分片数量std::vector<std::unique_ptr<KLfuCache<Key, Value>>> lfuSliceCaches_; // 缓存lfu分片容器
};
KHashLfuCache
的实现涉及多个 C++ 核心知识点:
- 模板编程(泛型编程)(用 template 定义泛型类,支持任意键值类型)
- 智能指针(std::unique_ptr)(管理动态 KLfuCache 生命周期,实现独占所有权的内存安全)
- 标准库容器(std::vector)(存储 KLfuCache 分片,提供动态数组高效访问管理)
- 哈希函数与映射(std::hash)(将 Key 映射为整数,实现 key 到分片的均匀分布)
- 线程库相关(std::thread::hardware_concurrency)(获取 CPU 核心数(默认分片数),优化并发性能)
- 动态内存管理(new 与对象生命周期)(动态创建 KLfuCache 分片,配合智能指针安全管理)
- 函数重载(get 方法重载,适配 “传出参数” 和 “直接返回值” 场景)
- 类的封装与访问控制(private 修饰成员 / 辅助函数,隐藏细节,仅暴露 put/get 等接口)
- 类型转换(static_cast)(实现数值安全转换,确保分片容量计算精度)
- 标准库数学函数(std::ceil)(分片容量向上取整,确保总容量不小于 capacity_)
一、类的核心定位与私有成员变量
先明确类的 “数据存储与配置载体”—— 私有成员变量定义了缓存的总容量、分片数量、分片容器,是后续所有逻辑的基础。
private:size_t capacity_; // 总容量int sliceNum_; // 切片数量std::vector<std::unique_ptr<KLfuCache<Key, Value>>> lfuSliceCaches_; // 切片LRU缓存
capacity_
:缓存的总容量(用户传入,所有分片容量之和不小于此值)。例:若总容量 = 100,分片数 = 5,则每个分片容量至少 20,确保整体存不超总限制。sliceNum_
:缓存的分片数量(决定并发性能的关键参数)。逻辑:高并发场景下,分片数越多,锁竞争粒度越小(后续构造函数会自动适配硬件并发数)。lfuSliceCaches_
:存储所有 LFU 分片的容器,核心特性:- 用
std::vector
:支持动态扩容、随机访问(通过索引快速定位分片); - 用
std::unique_ptr<KLfuCache>
:自动管理分片的生命周期(无需手动delete
,避免内存泄漏),且unique_ptr
的 “独占所有权” 特性适配 “分片独立管理” 的逻辑(每个分片仅被容器持有)。
- 用
二、构造函数:初始化分片
构造函数是 “创建分片、分配容量” 的入口,决定了分片的数量和每个分片的大小,直接影响缓存的并发性能和容量控制。
KHashLfuCache(size_t capacity, int sliceNum, int maxAverageNum = 10): sliceNum_(sliceNum > 0 ? sliceNum : std::thread::hardware_concurrency()), capacity_(capacity)
{size_t sliceSize = std::ceil(capacity_ / static_cast<double>(sliceNum_)); // 每个分片的大小for (int i = 0; i < sliceNum_; ++i){lfuSliceCaches_.emplace_back(new KLfuCache<Key, Value>(sliceSize, maxAverageNum));}
}
1. 分片数量 sliceNum_
的确定
- 逻辑:优先使用用户传入的
sliceNum
(若sliceNum > 0
);若用户未传或传负数,则自动取std::thread::hardware_concurrency()
(返回 CPU 核心数,如 8 核 CPU 返回 8)。 - 目的:让分片数匹配硬件并发能力,避免 “分片过多导致资源浪费” 或 “分片过少仍有锁竞争”。
- 例:8 核 CPU 默认分 8 片,多线程访问时,若线程操作不同分片,仅竞争对应分片的锁(而非全局锁)。
2. 分片容量 sliceSize
的计算
- 代码关键:
std::ceil(capacity_ / static_cast<double>(sliceNum_))
- 先将
sliceNum_
转为double
:避免整数除法的精度丢失(如总容量 = 100,分片数 = 3,整数除法100/3=33
,总容量仅 99,用double
计算得33.333
); - 用
std::ceil
向上取整:确保每个分片容量足够,总容量之和≥用户传入的capacity_
(如 100/3 向上取整为 34,3 个分片总容量 = 102≥100)。
- 先将
- 目的:避免 “总容量不足”(若用向下取整,总容量可能小于用户预期)。
3. 创建分片并加入容器
- 代码:
lfuSliceCaches_.emplace_back(new KLfuCache<Key, Value>(sliceSize, maxAverageNum))
new KLfuCache(...)
:在堆上创建单个 LFU 分片(KLfuCache
是你之前实现的 “带老化机制的 LFU 缓存”),传入 “分片容量sliceSize
” 和 “老化阈值maxAverageNum
”;emplace_back
:直接在vector
尾部构造unique_ptr
,比push_back
更高效(避免临时对象拷贝);- 生命周期:
unique_ptr
接管new
创建的KLfuCache
,当KHashLfuCache
销毁时,vector
会自动销毁所有unique_ptr
,进而调用KLfuCache
的析构函数,无内存泄漏。 emplace_back(new ...)
是unique_ptr
接收裸指针的标准用法,由智能指针自动管理生命周期,无需担心内存泄漏。
三、核心方法 1:put
新增 / 更新数据(哈希映射 + 分片调用)
put
方法的核心是 “通过哈希找到对应分片,再调用分片的put
”,不直接处理数据,仅做 “路由”。
void put(Key key, Value value)
{// 根据key找出对应的lfu分片size_t sliceIndex = Hash(key) % sliceNum_;lfuSliceCaches_[sliceIndex]->put(key, value);
}
1. 步骤 1:计算 key 对应的分片索引 sliceIndex
- 依赖私有辅助函数
Hash(key)
:将key
转为size_t
类型的哈希值; - 取模操作
% sliceNum_
:将哈希值映射到[0, sliceNum_-1]
的范围,确保索引不超出vector
的大小; - 目的:让不同 key 均匀分布到不同分片(哈希函数的随机性保证分布均匀),避免 “某一分片 key 过多导致负载集中”。
2. 步骤 2:调用分片的put
方法
- 代码:
lfuSliceCaches_[sliceIndex]->put(key, value)
- 通过
vector
的随机访问([]
)快速定位分片(时间复杂度 O (1)); - 调用分片自身的
put
方法:后续逻辑完全由KLfuCache
处理(如判断 key 是否存在、更新值 / 频次、满容淘汰等);
- 通过
- 线程安全:因每个
KLfuCache
内部有std::mutex
,多线程调用同一分片的put
会自动加锁,不同分片则无竞争。
四、核心方法 2:get
查询数据(重载适配不同场景)
get
有两个重载版本,核心逻辑与put
一致 ——“找分片→调分片的get
”,仅返回形式不同。
1. 版本 1:带传出参数,返回 “是否命中”
bool get(Key key, Value& value)
{// 根据key找出对应的lfu分片size_t sliceIndex = Hash(key) % sliceNum_;return lfuSliceCaches_[sliceIndex]->get(key, value);
}
- 逻辑:通过哈希找到分片后,调用分片的
get
方法,将查询到的value
存入传出参数(引用Value& value
),返回bool
值(true
= 命中,false
= 未命中); - 优势:用户可明确知道 “是否查到数据”,避免 “未命中时误把默认值当有效数据”。
2. 版本 2:直接返回 value,未命中返回默认值
Value get(Key key)
{Value value;get(key, value);return value;
}
- 逻辑:内部调用版本 1 的
get
,若命中则value
为有效数据,若未命中则value
为默认初始化值(如int
为 0,string
为空); - 适用场景:用户不关心 “是否命中”,仅需 “有数据就用,没数据就用默认值” 的场景。
- 不对外暴露‘是否命中’的结果
五、辅助函数:Hash
计算 key 的哈希值
私有Hash
函数是 “key 分片” 的核心,负责将任意类型的Key
转为size_t
类型的哈希值,供后续取模用。
private:// 将key转换为对应hash值size_t Hash(Key key){std::hash<Key> hashFunc;return hashFunc(key);}
- 逻辑:利用 C++ 标准库的
std::hash
模板(已支持int
、string
等基础类型),创建hashFunc
对象,调用hashFunc(key)
生成哈希值; - 扩展性:若
Key
是自定义类型(如自定义结构体),需手动实现std::hash
的特化(否则编译报错),确保自定义Key
能被哈希。 - 例:
Key
为string
时,hashFunc("user1")
会生成一个唯一的size_t
值,再% sliceNum_
得到分片索引。
六、辅助方法:purge
清空所有缓存
purge
用于 “批量清空所有分片的缓存数据”,适用于 “缓存重置” 场景(如系统重启前清理缓存)。
// 清除缓存
void purge()
{for (auto& lfuSliceCache : lfuSliceCaches_){lfuSliceCache->purge();}
}
- 逻辑:遍历
lfuSliceCaches_
中的所有分片,调用每个分片的purge
方法(KLfuCache
的purge
会清空自身的nodeMap_
和freqToFreqList_
); - 目的:因每个分片独立存储数据,需逐个清空,确保 “所有分片数据都被清理”,而非仅清理部分。
输入输出流程:
场景 1:输入新键值对(put 新数据,缓存未满)
KHashLfuCache::put(路由 key 到对应分片)->
KLfuCache::put(加锁保证线程安全,判断 key 为新键)->
KLfuCache::putInternal(处理新键核心逻辑)->
KLfuCache::addToFreqList(将新节点加入频次 1 的链表)->
KLfuCache::addFreqNum(增加总访问次数,计算平均频次)->
(若平均频次未超限,流程结束)
场景 2:输入新键值对(put 新数据,缓存已满)
KHashLfuCache::put(路由 key 到对应分片)->
KLfuCache::put(加锁保证线程安全,判断 key 为新键)->
KLfuCache::putInternal(处理新键核心逻辑,检测缓存满)->
KLfuCache::kickOut(淘汰分片内最不常访问节点)->
KLfuCache::removeFromFreqList(将被淘汰节点从原频次链表移除)->
KLfuCache::decreaseFreqNum(减少总访问次数,更新平均频次)->
KLfuCache::addToFreqList(将新节点加入频次 1 的链表)->
KLfuCache::addFreqNum(增加总访问次数,计算平均频次)->
(若平均频次未超限,流程结束)
场景 3:查询已存数据(get 已存在的 key)
KHashLfuCache::get(带传出参数,路由 key 到对应分片)->
KLfuCache::get(加锁保证线程安全,查询到 key 命中)->
KLfuCache::getInternal(更新命中节点的访问频次)->
KLfuCache::removeFromFreqList(将节点从原频次链表移除)->
KLfuCache::addToFreqList(将节点加入新频次的链表)->
KLfuCache::addFreqNum(增加总访问次数,计算平均频次)->
(若平均频次超限)KLfuCache::handleOverMaxAverageNum(触发老化机制)->KLfuCache::removeFromFreqList(老化时移除节点)->
KLfuCache::addToFreqList(老化后加入新频次链表)->
KLfuCache::updateMinFreq(重新计算最小频次)->
KHashLfuCache::get(返回命中的 value,流程结束)