当前位置: 首页 > news >正文

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++ 核心知识点:

  1. 模板编程(泛型编程)(用 template 定义泛型类,支持任意键值类型)
  2. 智能指针(std::unique_ptr)(管理动态 KLfuCache 生命周期,实现独占所有权的内存安全)
  3. 标准库容器(std::vector)(存储 KLfuCache 分片,提供动态数组高效访问管理)
  4. 哈希函数与映射(std::hash)(将 Key 映射为整数,实现 key 到分片的均匀分布)
  5. 线程库相关(std::thread::hardware_concurrency)(获取 CPU 核心数(默认分片数),优化并发性能)
  6. 动态内存管理(new 与对象生命周期)(动态创建 KLfuCache 分片,配合智能指针安全管理)
  7. 函数重载(get 方法重载,适配 “传出参数” 和 “直接返回值” 场景)
  8. 类的封装与访问控制(private 修饰成员 / 辅助函数,隐藏细节,仅暴露 put/get 等接口)
  9. 类型转换(static_cast)(实现数值安全转换,确保分片容量计算精度)
  10. 标准库数学函数(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模板(已支持intstring等基础类型),创建hashFunc对象,调用hashFunc(key)生成哈希值;
  • 扩展性:若Key是自定义类型(如自定义结构体),需手动实现std::hash的特化(否则编译报错),确保自定义Key能被哈希。
  • 例:Keystring时,hashFunc("user1")会生成一个唯一的size_t值,再% sliceNum_得到分片索引。

六、辅助方法:purge 清空所有缓存

purge 用于 “批量清空所有分片的缓存数据”,适用于 “缓存重置” 场景(如系统重启前清理缓存)。

// 清除缓存
void purge()
{for (auto& lfuSliceCache : lfuSliceCaches_){lfuSliceCache->purge();}
}
  • 逻辑:遍历lfuSliceCaches_中的所有分片,调用每个分片的purge方法(KLfuCachepurge会清空自身的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,流程结束)

http://www.dtcms.com/a/449931.html

相关文章:

  • XSLFO 流:深入解析与实际应用
  • vscode 一键导出导入所有插件
  • 网站登录验证码显示不出来软件网站排名
  • 找人做一个网站要多少钱包头移动的网站建设
  • 网站设计师培训学校建网站建设网站
  • Linux 目录结构,远程登录 CentOS 服务器 和 远程文件传输 详解(Xshell, PuTTY, Xftp)
  • 外贸网站样式修改wordpress登陆后台
  • FFN、CNN和RNN对比
  • springboot3 怎么用@DS 实现多数据源
  • 平湖市住房建设局网站安全证四川省建设厅官方网站
  • 利用CodeBuddy CLI 辅助Unity游戏开发,提高开发效率
  • MATLAB计算标准径流指数(Standard Runoff Index,SRI)
  • pion/webrtc v4.1.5 更新详情与改动说明
  • 招聘代做网站a做爰网站
  • Http 上传压缩包需要加0\r\n\r\n
  • 鼠标网站模板欧洲cn2 vps
  • 题解:P14174 【MX-X23-T4】卡常数
  • 吉林市建设厅网站公司app开发收费价目表
  • 个人网站 空间 多少够浦口区教育局网站集约化建设
  • 第四十五章 ESP32S3 Flash 模拟 U 盘实验
  • 如何建设网站济南兴田德润团队怎么样照片制作软件免费
  • LeetCode算法日记 - Day 64: 岛屿的最大面积、被围绕的区域
  • 北京建设网站网站怎么做网站软件
  • 国外做健康的网站微信公众号用什么开发
  • 广州网站建设实力乐云seo江门市专业做网站公司
  • VLA论文阅读2
  • Java基础加强12-异常、泛型
  • 用花生棒做网站快吗在线建站网站
  • 建网站中企动力网页设计推荐使用路径
  • 【机器学习】混淆矩阵(confusion matrix)TP TN FP FN