技术文章大纲:C++实现具备LFU的缓存系统
引言
简要介绍LFU(Least Frequently Used)缓存淘汰策略的概念及其应用场景。说明实现LFU缓存系统的必要性,例如在数据库、Web服务器等高频访问系统中优化性能。
LFU缓存的核心原理
解释LFU策略的基本逻辑:根据数据项的访问频率决定淘汰顺序,访问频率最低的数据优先被移除。对比LRU(Least Recently Used)策略,突出LFU在特定场景下的优势。
数据结构设计
分析实现LFU缓存所需的关键数据结构:
- 哈希表(
unordered_map):存储键值对,实现O(1)时间复杂度的查找。 - 红黑树(
map)维持频率桶,相同频率的键用链表存储 - 一个迭代器类型(
unordered_map<::iterator>),用于指向节点在链表中的位置
//核心数据结构
std::unordered_map<Key, std::pair<Value, int>>KeyValueFreq;//哈希表,存储键值和访问次数
std::map<int, std::list<Key>>freqKeys;//红黑树维持频率桶,相同频率的键用链表存储
std::unordered_map<Key, typename std::list<Key>::iterator>KeyIterator;//指向节点在链表中的位置
int min_freq;//记录最小频率
具体实现步骤
-
定义节点结构体
template<typename Key,typename Value>可以灵活的适用于不同参数。 -
核心类设计(LFUCache类)
- 成员变量:容量(capacity)、当前最小频率(min_freq)、键到节点的哈希表(keyValueFreq)、频率到节点链表的哈希表(freqKeys)。
- 方法:
get(获取数据)、put(插入/更新数据)、私有方法updata_min_frequency_after(更新min_freq的值)increase_frequency(增加键的频率)remove_min_frequence(移除最小频率)。void updata_min_frequency_after(int r) { if (r != min_freq) { return; } if (freqKeys.empty()) { min_freq = 1;//重置 } else { auto f = freqKeys.begin(); min_freq = f->first; } } //内部辅助方法 增加键的频率 void increase_frequency(const Key& K) { int oldFreq = KeyValueFreq[K].second;//获取键的旧频率 auto& oldKey = freqKeys[oldFreq];//获取键在频率桶的位置 oldKey.erase(KeyIterator[K]);//移除它 if (oldKey.empty()) {//如果旧桶空了 freqKeys.erase(oldFreq);//清除桶 if (oldFreq == min_freq) {//如果恰好是最小频率,则更新最小频率 updata_min_frequency_after(oldFreq); } } int newFreq = oldFreq + 1; KeyValueFreq[K].second = newFreq; freqKeys[newFreq].push_front(K);//将新频率键插入链表前 KeyIterator[K] = freqKeys[newFreq].begin();//更新指示器位置 } //内部辅助方法 移除最小频率 void remove_min_frequence() { //找最小频率桶的最后一个键(LFU+LRU) auto minfreqkeys = freqKeys[min_freq];//找最小频率桶 Key removed = minfreqkeys.back();//找最小频率桶的最后一个键 //从所有数据结构中开始删除 minfreqkeys.pop_back(); KeyValueFreq.erase(removed); KeyIterator.erase(removed); }
-
关键操作实现
get操作:若键存在,更新节点频率并返回值;否则返回-1。put操作:若键存在则更新值并调频;若不存在且缓存已满,淘汰最小频率的尾节点,再插入新节点。- printDubeg操作是打印函数。
- 每个操作都有加锁处理
- 需要注意的是,这里是方法重写,因为get操作和put还用于LRU和ARC的实现。
void put(const Key& K, const Value& V) override { //重写PUT函数
std::lock_guard<std::mutex>lock(mutex_);
printDebug("PUT", K);
auto it = KeyValueFreq.find(K);
if (it != KeyValueFreq.end()) {//判断是否有K
it->second.first = V;
increase_frequency(K);
}
else {//如果没有就需要插入,还需判断储存空间
if (KeyValueFreq.size() >= capacity) {
remove_min_frequence();
}
KeyValueFreq[K] = { V,1 };//在哈希表中插入
freqKeys[1].push_front(K);//在频率桶中更新
KeyIterator[K] = freqKeys[1].begin();//更新KeyIterator
min_freq = 1;
}
}
std::optional<Value>get(const Key& K)override {//重写get方法
std::lock_guard<std::mutex>lock(mutex_);
auto it = KeyValueFreq.find(K);
if (it != KeyValueFreq.end()) {
increase_frequency(K);
printDebug("Get hit", K);
return it->second.first;
}
printDebug("Get miss", K);
return std::nullopt;
}
复杂度分析
- 时间复杂度:
get和put操作均为O(1)。 - 空间复杂度:O(n),n为缓存容量。
优化内容
- 并发支持:通过加锁(如
std::mutex)实现线程安全。 - 性能测试:对比LFU与LRU在不同访问模式下的命中率。
- 变种策略:结合LRU的LFU-Aging策略,避免历史高频但近期低频的数据长期占用缓存。
void compareLRUvsLFU() {
std::cout << "\n=== LRU vs LFU 对比测试 ===" << std::endl;
std::cout << "\n场景:周期性访问模式" << std::endl;
// LRU缓存
CacheManager<int, std::string> lruCache(CacheType::LRU, 2);
// LFU缓存
CacheManager<int, std::string> lfuCache(CacheType::LFU, 2);
// 访问序列: 1, 2, 1, 2, 3, 1, 2, 3
int access_sequence[] = { 1, 2, 1, 2, 3, 1, 2, 3 };
for (int i = 0; i < 8; ++i) {
int key = access_sequence[i];
// 检查是否已存在:使用 contains()
if (!lruCache.contains(key)) {
lruCache.put(key, "Value" + std::to_string(key));
}
else {
lruCache.get(key); // 更新 LRU 顺序
}
if (!lfuCache.contains(key)) {
lfuCache.put(key, "Value" + std::to_string(key));
}
else {
lfuCache.get(key); // 更新 LFU 频率
}
}
}
应用案例
-
LRU:适合访问模式相对稳定,有明显热点数据的场景,例如数据库连接池、Web服务器缓存
-
LFU:适合访问频率差异大,需要长期保护高频数据的场景,例如操作系统页面缓存、电商商品缓存。
总结
LFU(Least Frequently Used)缓存基于访问频率淘汰数据,高频访问的条目保留更久。适合长期热点数据统计的场景,如内容推荐系统或广告投放平台,能有效减少高频数据的重复计算。访问模式稳定的系统(如新闻门户的热点排行榜)能最大化LFU的价值。其算法通过哈希表+最小堆或双哈希表实现,时间复杂度可优化至O(1)。
突发流量可能导致低频但新写入的数据被快速淘汰("缓存污染"问题)。例如社交媒体的突发热搜话题可能因初始低频被LFU误删。实现复杂度高于LRU,需维护频率计数器和多层数据结构。
适合场景:视频点播的热门影片缓存、电商长期畅销商品展示、企业级数据库查询缓存。这类场景中热点数据生命周期较长,访问规律性强。
完整代码在https://github.com/huan03189/Cache-System-LRU-and-LFU.git
495

被折叠的 条评论
为什么被折叠?



