1. 缓存穿透的概念
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据,每次请求都打到 DB | 恶意攻击/bug 导致查询不存在的 key | 布隆过滤器 + 缓存空值 |
| 缓存击穿 | 热点 key过期瞬间,大量请求打到 DB | 热点 key 过期 + 高并发 | 互斥锁/逻辑过期 |
| 缓存雪崩 | 大量 key 同时过期,DB 压力骤增 | 缓存集中失效/Redis 宕机 | 随机过期时间 + 多级缓存 + 限流 |
2. 方案1:布隆过滤器
2.1 原理
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否属于一个集合。
由以下两部分组成:
- 位数组 (Bit Array):长度为 m 的二进制数组,初始所有位均为 0。
- 哈希函数 (Hash Functions):k 个相互独立的哈希函数,每个函数能将输入元素映射到位数组的一个索引位置 [0, m-1]。
添加元素 :
- 输入元素 item。
- 使用 k 个哈希函数计算得到 k 个索引值:h1(item), h2(item), ..., hk(item)。
- 将位数组中这 k 个索引位置的位设置为 1,若某位已为 1,则保持为 1。
查询元素:
- 输入元素 item。
- 使用相同的 k 个哈希函数计算得到 k 个索引值。
- 检查位数组中这 k 个索引位置的位状态:
- 若任意一个位置为
0:判定元素一定不存在。 - 若所有位置均为
1:判定元素可能存在。
- 若任意一个位置为
2.2 bloom_filter.h
cpp
/**
* @file bloom_filter.h
* @brief 线程安全的纯内存布隆过滤器头文件
* @version 1.0.0
*
* @details
* 核心功能:
* 1. 快速判断元素"一定不存在"或"可能存在"
* 2. 拦截无效请求,防止缓存穿透
* 3. 纯内存存储,线程安全,无外部依赖
*
* 技术要求:
* - C++17 或更高版本 (需要 std::shared_mutex)
* - 64 位系统架构推荐
*/
#ifndef DFS_REDIS_BLOOM_FILTER_H
#define DFS_REDIS_BLOOM_FILTER_H
#include <string>
#include <vector>
#include <memory>
#include <cmath>
#include <cstdint>
#include <mutex>
#include <shared_mutex>
#include <atomic>
namespace dfs {
namespace redis {
/**
* @struct BloomFilterConfig
* @brief 布隆过滤器配置结构体
*
* 用于初始化时设定预期的容量和允许的误判率。
* 这两个参数直接决定了内存占用和哈希计算次数。
*/
struct BloomFilterConfig {
size_t expectedItems = 1000000; // 预期存入的元素数量 (n)
double falsePositiveRate = 0.01; // 允许的误判率 (p), 0.01 代表 1%
};
/**
* @class BloomFilter
* @brief 线程安全的布隆过滤器实现类
*
* 使用位数组和多个哈希函数来高效判断元素是否存在。
* 线程安全通过 shared_mutex 实现,支持高并发读。
*/
class BloomFilter {
public:
using Ptr = std::shared_ptr<BloomFilter>; // 智能指针类型别名
// 构造函数 (基于配置结构体)
explicit BloomFilter(const BloomFilterConfig& config = BloomFilterConfig());
// 构造函数 (基于直接参数)
explicit BloomFilter(size_t expectedItems, double falsePositiveRate = 0.01);
/// 默认析构函数
~BloomFilter() = default;
// 禁用拷贝构造和拷贝赋值,防止位数组被意外复制导致资源浪费或状态不一致
BloomFilter(const BloomFilter&) = delete;
BloomFilter& operator=(const BloomFilter&) = delete;
// 允许移动语义,支持高效的所有权转移
BloomFilter(BloomFilter&&) = default;
BloomFilter& operator=(BloomFilter&&) = default;
/**
* @brief 1.添加元素到过滤器
* @param item 要添加的字符串元素
* @return true 始终返回 true (添加操作在布隆过滤器中不会失败)
* @note 线程安全 (写锁)
*/
bool add(const std::string& item);
/**
* @brief 2.检查元素是否可能存在
* @param item 要检查的字符串元素
* @return true 可能存在 (有误判率), false 一定不存在
* @note 线程安全 (读锁)
*/
bool mightContain(const std::string& item) const;
/**
* @brief 3.批量添加元素
* @param items 元素字符串向量
* @return true 成功
* @note 线程安全 (写锁)。相比循环调用 add,此方法只加锁一次,性能更高。
*/
bool addBatch(const std::vector<std::string>& items);
/**
* @brief 4.清空过滤器所有数据
* @note 线程安全 (写锁)。清空后误判率归零,计数归零。
*/
void clear();
// --- 监控与状态获取接口 ---
size_t getBitSize() const { return bitSize_; } // 获取位数组总位数 (m)
size_t getHashCount() const { return hashCount_; } // 获取哈希函数数量 (k)
size_t getItemCount() const { return itemCount_.load(); } // 获取当前添加次数 (注意:非唯一元素个数,因为无法去重计数)
/**
* @brief 估算当前内存占用 (字节)
* @return 位数组占用的字节数
* @note 不包含对象本身开销,仅计算 bits_ 向量 payload
*/
size_t getMemoryUsage() const { return bits_.size() * sizeof(uint64_t); }
private:
void calculateOptimalParameters(); // 根据配置计算最优的位数组大小和哈希次数
/**
* @brief 计算元素在位数组中的所有哈希位置
* @param item 输入元素
* @return 包含 k 个位置索引的向量
* @details 使用双哈希技巧 (h1 + i * h2) 模拟 k 个哈希函数
*/
std::vector<size_t> getHashPositions(const std::string& item) const;
/**
* @brief 基础哈希函数
* @param item 输入数据
* @param seed 种子值,用于生成不同的哈希流
* @return 64 位哈希值
*/
uint64_t hash(const std::string& item, uint32_t seed) const;
void setBit(size_t pos); // 设置指定位为 1
bool testBit(size_t pos) const; // 测试指定位是否为 1
// --- 成员变量 ---
BloomFilterConfig config_; // 配置信息
size_t bitSize_; // 位数组总大小 (m)
size_t hashCount_; // 哈希函数个数 (k)
std::vector<uint64_t> bits_; // 位数组,每 uint64_t 存 64 位
std::atomic<size_t> itemCount_{0}; // 添加操作计数 (原子操作,无锁读)
mutable std::shared_mutex mutex_; // 读写锁 (mutable 允许 const 函数加锁)
};
} // namespace redis
} // namespace dfs
#endif // DFS_REDIS_BLOOM_FILTER_H
2.3 bloom_filter.cpp
cpp
/**
* @file bloom_filter.cpp
* @brief 线程安全的纯内存布隆过滤器实现源文件
*/
#include "bloom_filter.h"
#include <algorithm> // for std::fill
namespace dfs {
namespace redis {
// -----------------------------------------------------------------------------
// 构造函数实现
// -----------------------------------------------------------------------------
BloomFilter::BloomFilter(const BloomFilterConfig& config)
: config_(config) {
// 初始化时立即计算所需的内存和哈希参数
calculateOptimalParameters();
}
BloomFilter::BloomFilter(size_t expectedItems, double falsePositiveRate) {
config_.expectedItems = expectedItems;
config_.falsePositiveRate = falsePositiveRate;
calculateOptimalParameters();
}
// -----------------------------------------------------------------------------
// 核心参数计算
// -----------------------------------------------------------------------------
void BloomFilter::calculateOptimalParameters() {
// ln(2) 的常数,用于布隆过滤器公式计算
constexpr double ln2 = 0.6931471805599453;
// 1. 计算最优位数组大小 m (bitSize_)
// 公式:m = - (n * ln(p)) / (ln(2) * ln(2))
// 确保结果至少为 1,防止除零或过小
bitSize_ = static_cast<size_t>(
-static_cast<double>(config_.expectedItems) *
std::log(config_.falsePositiveRate) / (ln2 * ln2)
);
// 2. 计算最优哈希函数个数 k (hashCount_)
// 公式:k = (m / n) * ln(2)
hashCount_ = static_cast<size_t>(
static_cast<double>(bitSize_) / static_cast<double>(config_.expectedItems) * ln2
);
// 3. 边界保护
// 哈希次数至少为 1,最多限制为 32 (过多哈希会降低性能且收益递减)
if (hashCount_ == 0) hashCount_ = 1;
if (hashCount_ > 32) hashCount_ = 32;
// 位大小至少为 64KB (1024 * 64 bits),防止配置过小导致瞬间饱和
if (bitSize_ == 0) bitSize_ = 1024 * 64;
// 4. 初始化位数组
// 使用 uint64_t 作为存储单元,每个元素存储 64 个位
// 计算需要的 uint64_t 个数:向上取整 (bitSize_ + 63) / 64
size_t numWords = (bitSize_ + 63) / 64;
bits_.resize(numWords, 0);
}
// -----------------------------------------------------------------------------
// 哈希算法实现
// -----------------------------------------------------------------------------
/**
* @brief 自定义混合哈希函数
* @details
* 类似 MurmurHash3 的混合逻辑,但按字节处理以支持任意长度字符串。
* 使用两个不同的种子和混合常数来确保 h1 和 h2 的独立性。
*/
uint64_t BloomFilter::hash(const std::string& item, uint32_t seed) const {
// 初始化两个哈希状态
uint64_t h1 = seed;
// 使用黄金比例常数作为 h2 的初始扰动,增加随机性
uint64_t h2 = seed ^ 0x9e3779b97f4a7c15ULL;
const uint8_t* data = reinterpret_cast<const uint8_t*>(item.data());
size_t len = item.size();
// 逐字节混合 (Byte-by-byte mixing)
// 比按 4/8 字节处理慢,但实现简单且兼容性好
for (size_t i = 0; i < len; ++i) {
// h1 混合逻辑
h1 ^= data[i];
h1 *= 0x87c37b91114253d5ULL; // MurmurHash3 常数
h1 = (h1 << 31) | (h1 >> 33); // 循环左移 31 位
// h2 混合逻辑 (使用不同常数)
h2 ^= data[i];
h2 *= 0x4cf5ad432745937fULL;
h2 = (h2 << 31) | (h2 >> 33);
}
// 最终混合 (Finalizer)
// 将长度混入哈希值,防止前缀冲突
h1 ^= len;
h2 ^= len;
h1 += h2;
h2 += h1;
// Avalanch 效应:确保输入位的微小变化导致输出位的巨大变化
h1 ^= h1 >> 33;
h1 *= 0xff51afd7ed558ccdULL;
h1 ^= h1 >> 33;
h1 *= 0xc4ceb9fe1a85ec53ULL;
h1 ^= h1 >> 33;
// 返回 h1 作为基础哈希值
// 注意:h2 在 getHashPositions 中通过 seed=1 单独计算
return h1;
}
/**
* @brief 获取元素对应的所有位位置
* @details 使用双哈希技巧 (Double Hashing)
* 传统方法需要计算 k 次 hash(item, seed_i)。
* 优化方法:hash_i(item) = hash1(item) + i * hash2(item)
* 这减少了 k-2 次哈希计算,显著提升性能。
*/
std::vector<size_t> BloomFilter::getHashPositions(const std::string& item) const {
std::vector<size_t> positions;
positions.reserve(hashCount_);
// 计算两个独立的基础哈希值
uint64_t h1 = hash(item, 0);
uint64_t h2 = hash(item, 1);
// 线性组合生成 k 个位置
for (size_t i = 0; i < hashCount_; ++i) {
// 组合哈希值。uint64_t 溢出是预期的,相当于模 2^64
uint64_t combined = h1 + i * h2;
// 映射到位数组范围内 [0, bitSize_)
positions.push_back(static_cast<size_t>(combined % bitSize_));
}
return positions;
}
// -----------------------------------------------------------------------------
// 位操作辅助函数
// -----------------------------------------------------------------------------
/**
* @brief 将指定位设置为 1
* @param pos 全局位索引
* @details
* wordIndex = pos / 64 (找到第几个 uint64_t)
* bitIndex = pos % 64 (找到该 uint64_t 中的第几位)
*/
void BloomFilter::setBit(size_t pos) {
size_t wordIndex = pos / 64;
size_t bitIndex = pos % 64;
// 位或操作设置位
bits_[wordIndex] |= (1ULL << bitIndex);
}
/**
* @brief 测试指定位是否为 1
* @param pos 全局位索引
* @return true 如果位为 1, false 如果位为 0
*/
bool BloomFilter::testBit(size_t pos) const {
size_t wordIndex = pos / 64;
size_t bitIndex = pos % 64;
// 位与操作测试位
return (bits_[wordIndex] & (1ULL << bitIndex)) != 0;
}
// -----------------------------------------------------------------------------
// 公共接口实现
// -----------------------------------------------------------------------------
bool BloomFilter::add(const std::string& item) {
// 1. 计算哈希位置
auto positions = getHashPositions(item);
// 2. 获取写锁 (独占锁)
// 确保在修改位数组期间没有其他线程读取或写入
std::unique_lock<std::shared_mutex> lock(mutex_);
// 3. 设置所有对应位为 1
for (size_t pos : positions) {
setBit(pos);
}
// 4. 更新计数 (原子操作)
// 注意:这里统计的是 add 调用次数,若重复添加相同元素,计数也会增加
itemCount_.fetch_add(1, std::memory_order_relaxed);
return true;
}
bool BloomFilter::mightContain(const std::string& item) const {
// 1. 计算哈希位置
auto positions = getHashPositions(item);
// 2. 获取读锁 (共享锁)
// 允许多个线程同时执行 mightContain,提高并发读性能
std::shared_lock<std::shared_mutex> lock(mutex_);
// 3. 检查所有对应位
// 布隆过滤器逻辑:只要有一个位是 0,则元素一定不存在
for (size_t pos : positions) {
if (!testBit(pos)) {
return false; // 确定不存在
}
}
// 如果所有位都是 1,则可能存在 (有概率误判)
return true;
}
bool BloomFilter::addBatch(const std::vector<std::string>& items) {
if (items.empty()) return true;
// 批量操作优化:只加一次写锁
// 如果循环调用 add,会加锁 items.size() 次,开销大
std::unique_lock<std::shared_mutex> lock(mutex_);
for (const auto& item : items) {
auto positions = getHashPositions(item);
for (size_t pos : positions) {
setBit(pos);
}
}
// 批量更新计数
itemCount_.fetch_add(items.size(), std::memory_order_relaxed);
return true;
}
void BloomFilter::clear() {
// 获取写锁
std::unique_lock<std::shared_mutex> lock(mutex_);
// 重置所有位为 0
std::fill(bits_.begin(), bits_.end(), 0ULL);
// 重置计数
itemCount_.store(0, std::memory_order_relaxed);
}
} // namespace redis
} // namespace dfs
2.4 类方法
公共接口:
| 函数名 | 功能描述 | 参数说明 | 返回值 |
|---|---|---|---|
| 构造函数 BloomFilter(const BloomFilterConfig& config) | 基于配置结构体初始化布隆过滤器 | config: 包含预期元素数量和误判率的配置结构体 |
无 |
| 构造函数 BloomFilter(size_t expectedItems, double falsePositiveRate) | 基于直接参数初始化布隆过滤器 | expectedItems: 预期存入元素数量 falsePositiveRate: 允许误判率(默认0.01) |
无 |
| 析构函数 ~BloomFilter() | 释放资源 | 无 | 无 |
| addbool add(const std::string& item) | 添加元素到过滤器 | item: 要添加的字符串元素 |
true (始终成功) |
| mightContainbool mightContain(const std::string& item) const | 检查元素是否可能存在 | item: 要检查的字符串元素 |
true: 可能存在(有误判) false: 一定不存在 |
| addBatchbool addBatch(const std::vector<std::string>& items) | 批量添加元素 | items: 元素字符串向量 |
true (始终成功) |
| clearvoid clear() | 清空过滤器所有数据 | 无 | 无 |
监控与状态获取接口**:**
| 函数名 | 功能描述 | 返回值 |
|---|---|---|
| getBitSize size_t getBitSize() const | 获取位数组总位数 (m) | 位数组大小 |
| getHashCount size_t getHashCount() const | 获取哈希函数数量 (k) | 哈希函数个数 |
| getItemCount size_t getItemCount() const | 获取添加操作调用次数 | 原子计数值 |
| getMemoryUsage size_t getMemoryUsage() const | 估算当前内存占用(字节) | bits_ 向量占用字节数 |
私有方法:
| 函数名 | 功能描述 | 关键逻辑 | 设计要点 |
|---|---|---|---|
| calculateOptimalParameters | 根据配置计算最优位数组大小和哈希次数 | m = -n·ln(p)/(ln2)²k = (m/n)·ln2 | 边界保护:k∈[1,32], m≥64KB |
| getHashPositions | 计算元素在位数组中的所有哈希位置 | 双哈希技巧:hash_i = h1 + i×h2 | 用2次哈希模拟k次,性能提升显著 |
| hash | 基础哈希函数(自定义混合哈希) | 类似MurmurHash3的字节混合+最终混合 | 支持任意长度字符串,确保 Avalanche 效应 |
| setBit | 设置指定位为1 | wordIndex = pos/64, bitIndex = pos%64 | 位运算高效操作,避免分支 |
| testBit | 测试指定位是否为1 | 位与操作 (bits[word] & (1ULL<<bit)) != 0 | const 函数,支持读锁并发 |
3. 方案2:多级缓存中空值缓存
3.1 概念
┌─────────────────────────────────────────┐
│ 用户请求 │
└────────────────┬────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ L1: 本地缓存 (进程内内存) │
│ - 速度: ~100ns │
│ - 容量: 几 MB ~ 几百 MB │
│ - 缺点: 每个服务实例独立,数据不一致 │
└────────────────┬────────────────────────┘
│ 未命中
▼
┌─────────────────────────────────────────┐
│ L2: Redis 缓存 (独立服务) │
│ - 速度: ~1ms │
│ - 容量: 几 GB ~ 几十 GB │
│ - 优点: 多实例共享,数据一致 │
└────────────────┬────────────────────────┘
│ 未命中
▼
┌─────────────────────────────────────────┐
│ DB: 数据库 (MySQL/PostgreSQL 等) │
│ - 速度: ~10-100ms │
│ - 容量: TB 级 │
│ - 缺点: 慢,扛不住高并发 │
└─────────────────────────────────────────┘
3.2 LRU 缓存
3.2.1 算法中的 LRU
LRU 缓存在 LeetCode 中非常知名,其在算法题中的实现如下:
cpp
class LRUCache {
public:
// 关键:每次查看或插入都要把关键字提到最前面,插入如果长度超出则删除最后面的,需要虚拟头尾节点和map,结构体中要有一个key作为map的键
struct Node{
int key;
int val;
Node* pre;
Node* next;
Node(int k, int v):key(k), val(v){}
};
int m_capacity;
Node* dummyHead, *dummyTail;
unordered_map<int, Node*> key2node;
LRUCache(int capacity) {
dummyHead = new Node(-1, -1);
dummyTail = new Node(-1, -1);
dummyHead -> next = dummyTail;
dummyHead -> pre = nullptr;
dummyTail -> next = nullptr;
dummyTail -> pre = dummyHead;
m_capacity = capacity;
}
int get(int key) {
if(key2node.count(key)){
Node* cur = key2node[key];
Node* pre = cur -> pre;
Node* nxt = cur -> next;
pre -> next = nxt;
nxt -> pre = pre;
Node* oldHead = dummyHead -> next;
dummyHead -> next = cur;
cur -> pre = dummyHead;
cur -> next = oldHead;
oldHead -> pre = cur;
return cur -> val;
}else{
return -1;
}
}
void put(int key, int value){
if(key2node.count(key)){
Node* cur = key2node[key];
Node* pre = cur -> pre;
Node* nxt = cur -> next;
pre -> next = nxt;
nxt -> pre = pre;
Node* oldHead = dummyHead -> next;
dummyHead -> next = cur;
cur -> pre = dummyHead;
cur -> next = oldHead;
oldHead -> pre = cur;
cur -> val = value;
key2node[key] = cur;
}else{
Node* cur = new Node(key, value);
if(key2node.size() < m_capacity){
Node* oldHead = dummyHead -> next;
dummyHead -> next = cur;
cur -> pre = dummyHead;
cur -> next = oldHead;
oldHead -> pre = cur;
}else{
Node* oldHead = dummyHead -> next;
dummyHead -> next = cur;
cur -> pre = dummyHead;
cur -> next = oldHead;
oldHead -> pre = cur;
Node* oldTail = dummyTail -> pre;
Node* newTail = oldTail -> pre;
newTail -> next = dummyTail;
dummyTail -> pre = newTail;
key2node.erase(oldTail -> key);
}
key2node[key] = cur;
}
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
3.2.2 LRU 应用
local_cache.h:
cpp
/**
* @file local_cache.h
* @brief 本地缓存模块 - LRU淘汰策略的进程内缓存
* @author DFS Team
* @date 2025-02-18
*
* 核心功能:
* 1. O(1)时间复杂度的LRU淘汰算法
* 2. 线程安全的缓存操作
* 3. 支持TTL过期和逻辑过期
* 4. 作为多级缓存的L1层
*/
#ifndef DFS_REDIS_LOCAL_CACHE_H
#define DFS_REDIS_LOCAL_CACHE_H
#include <string>
#include <memory>
#include <list>
#include <unordered_map>
#include <mutex>
#include <chrono>
#include <optional>
namespace dfs {
namespace redis {
/**
* @brief 缓存数据结构
*
* 存储缓存值及其元数据,支持TTL过期和空值标记
*/
struct CacheData {
std::string data; ///< 缓存值
std::chrono::steady_clock::time_point expireTime; ///< 过期时间点
bool isNull = false; ///< 空值标记(用于防止缓存穿透)
};
/**
* @brief 基于进程内存的 LRU L1 缓存 (O(1) 时间复杂度实现)
*
* 【核心数据结构】:
* 哈希表 (unordered_map) + 双向链表 (std::list)
*
* 【LRU 原理】:
* ┌─────────────────────────────────────────────────────────────┐
* │ 双向链表 (访问顺序) │
* │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
* │ │ HEAD │◄──►│ Key3 │◄──►│ Key1 │◄──►│ Key2 │◄──► TAIL │
* │ └──────┘ └──────┘ └──────┘ └──────┘ │
* │ 最近使用 ←───────────────────────────────→ 最久未使用 │
* └─────────────────────────────────────────────────────────────┘
*
* 哈希表: { "key1" → iterator, "key2" → iterator, "key3" → iterator }
*
* 【时间复杂度】:
* • get: O(1) - 哈希表查找 + 链表移动到头部
* • set: O(1) - 哈希表插入 + 链表插入头部
* • del: O(1) - 哈希表删除 + 链表删除
* • evictLRU: O(1) - 直接删除链表尾部
*
* 【线程安全】:所有公共方法内部加锁,可多线程安全调用
*/
class LocalCache {
public:
/**
* @brief 1.构造函数,初始化缓存实例
* @param maxSize 缓存最大条目数,默认 10000
* @param defaultTtl 默认过期时间(秒),默认 300 秒(5分钟)
* @note 当 set/setNull 未指定 ttl 时,将使用此默认值
*/
LocalCache(size_t maxSize = 10000, int defaultTtl = 300);
/**
* @brief 2.设置键值对到缓存中
* @param key 缓存键
* @param value 缓存值
* @param ttlSeconds 过期时间(秒),-1 表示使用默认值,>0 表示自定义时长
* @note 时间复杂度: O(1)
* @note 如果 key 已存在,则更新值并刷新过期时间,同时移动到 LRU 头部
* @note 如果缓存已满,会先触发 LRU 淘汰再插入新元素
*/
void set(const std::string& key, const std::string& value, int ttlSeconds = -1);
std::optional<std::string> get(const std::string& key); // 3.从缓存中获取值
void del(const std::string& key); // 4.从缓存中删除指定键
bool exists(const std::string& key); // 5.检查键是否存在且未过期
void clear(); // 6.清空缓存中所有条目
size_t size() const; // 7.获取当前缓存中的有效条目数量
void setNull(const std::string& key, int ttlSeconds = 60); // 8.缓存空值(用于防止缓存穿透)
bool isNull(const std::string& key); // 9.检查键是否缓存了空值且未过期
private:
// LRU 链表节点,存储键和缓存数据
struct ListNode {
std::string key; // 缓存键
CacheData data; // 缓存数据(包含值、过期时间、空值标记等)
ListNode() = default;
ListNode(const std::string& k, const CacheData& d) : key(k), data(d) {}
};
using ListIterator = std::list<ListNode>::iterator; // 链表迭代器类型别名,便于代码可读性
/**
* @brief 10.淘汰所有已过期的缓存条目
* @note 时间复杂度: O(n),n 为缓存大小
* @note 逆向遍历链表,因为尾部更可能是旧数据(过期概率更高)
* @note 此函数目前未被自动调用,需外部定时触发或手动调用
*/
void evictExpired();
/**
* @brief 11.执行 LRU 淘汰:移除最久未使用的条目(链表尾部)
* @note 时间复杂度: O(1),仅在缓存满且需要插入新元素时调用
*/
void evictLRU();
/**
* @brief 12.将指定节点移动到链表头部(标记为最近使用)
* @param it 指向要移动节点的迭代器
* @note 时间复杂度: O(1),std::list::splice 是常数时间操作,用于 get/set 成功后更新访问顺序
*/
void moveToHead(ListIterator it);
/**
* @brief 13.从缓存中彻底移除指定 key(哈希表 + 链表)
* @param key 要删除的缓存键
* @note 时间复杂度: O(1)
*/
void removeFromCache(const std::string& key);
// ==================== 成员变量 ====================
size_t maxSize_; // 缓存最大容量(条目数上限)
int defaultTtl_; // 默认过期时间(秒)
mutable std::mutex mutex_; // 互斥锁,保护所有共享数据(mutable 允许 const 函数加锁)
std::list<ListNode> lruList_; // LRU 双向链表:头部=最近使用,尾部=最久未使用
std::unordered_map<std::string, ListIterator> cacheMap_; // 哈希表:key -> 链表节点迭代器,实现 O(1) 查找
};
}
}
#endif
local_cache.cpp:
cpp
#include "local_cache.h"
#include "../common/logger.h"
namespace dfs {
namespace redis {
LocalCache::LocalCache(size_t maxSize, int defaultTtl) : maxSize_(maxSize), defaultTtl_(defaultTtl) {}
void LocalCache::set(const std::string& key, const std::string& value, int ttlSeconds) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cacheMap_.find(key);
if (it != cacheMap_.end()) {
it->second->data.data = value;
it->second->data.isNull = false;
int ttl = ttlSeconds > 0 ? ttlSeconds : defaultTtl_;
it->second->data.expireTime = std::chrono::steady_clock::now() + std::chrono::seconds(ttl);
moveToHead(it->second);
return;
}
if (cacheMap_.size() >= maxSize_) {
evictLRU();
}
CacheData data;
data.data = value;
data.isNull = false;
int ttl = ttlSeconds > 0 ? ttlSeconds : defaultTtl_;
data.expireTime = std::chrono::steady_clock::now() + std::chrono::seconds(ttl);
lruList_.push_front(ListNode(key, data));
cacheMap_[key] = lruList_.begin();
}
std::optional<std::string> LocalCache::get(const std::string& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cacheMap_.find(key);
if (it == cacheMap_.end()) {
return std::nullopt;
}
if (std::chrono::steady_clock::now() > it->second->data.expireTime) {
removeFromCache(key);
return std::nullopt;
}
moveToHead(it->second);
return it->second->data.data;
}
void LocalCache::del(const std::string& key) {
std::lock_guard<std::mutex> lock(mutex_);
removeFromCache(key);
}
bool LocalCache::exists(const std::string& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cacheMap_.find(key);
if (it == cacheMap_.end()) {
return false;
}
return std::chrono::steady_clock::now() <= it->second->data.expireTime;
}
void LocalCache::clear() {
std::lock_guard<std::mutex> lock(mutex_);
lruList_.clear();
cacheMap_.clear();
}
size_t LocalCache::size() const {
std::lock_guard<std::mutex> lock(mutex_);
return cacheMap_.size();
}
void LocalCache::setNull(const std::string& key, int ttlSeconds) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cacheMap_.find(key);
if (it != cacheMap_.end()) {
it->second->data.isNull = true;
int ttl = ttlSeconds > 0 ? ttlSeconds : 60;
it->second->data.expireTime = std::chrono::steady_clock::now() + std::chrono::seconds(ttl);
moveToHead(it->second);
return;
}
if (cacheMap_.size() >= maxSize_) {
evictLRU();
}
CacheData data;
data.isNull = true;
int ttl = ttlSeconds > 0 ? ttlSeconds : 60;
data.expireTime = std::chrono::steady_clock::now() + std::chrono::seconds(ttl);
lruList_.push_front(ListNode(key, data));
cacheMap_[key] = lruList_.begin();
}
bool LocalCache::isNull(const std::string& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cacheMap_.find(key);
if (it == cacheMap_.end()) {
return false;
}
return it->second->data.isNull && std::chrono::steady_clock::now() <= it->second->data.expireTime;
}
void LocalCache::moveToHead(ListIterator it) {
lruList_.splice(lruList_.begin(), lruList_, it);
}
void LocalCache::removeFromCache(const std::string& key) {
auto it = cacheMap_.find(key);
if (it != cacheMap_.end()) {
lruList_.erase(it->second);
cacheMap_.erase(it);
}
}
void LocalCache::evictLRU() {
if (lruList_.empty()) return;
auto& tail = lruList_.back();
cacheMap_.erase(tail.key);
lruList_.pop_back();
LOG_DEBUG("LRU evicted key: %s", tail.key.c_str());
}
void LocalCache::evictExpired() {
auto now = std::chrono::steady_clock::now();
auto it = lruList_.rbegin();
while (it != lruList_.rend()) {
if (now > it->data.expireTime) {
std::string keyToRemove = it->key;
++it;
removeFromCache(keyToRemove);
} else {
++it;
}
}
}
}
}
在项目实际应用中,使用库提供的 std::list 进行简化操作,并增加对过期时间的支持。
其中缓存空值实现了对查询空值的防护。
3. 使用示例
cpp
/**
* @brief 多级缓存服务类示例
* 架构:LocalCache(L1) → BloomFilter → RedisCache(L2) → Database
*/
class MultiLevelCacheService {
public:
MultiLevelCacheService(
std::shared_ptr<dfs::redis::LocalCache> l1Cache,
std::shared_ptr<dfs::redis::BloomFilter> bloom,
std::shared_ptr<dfs::redis::RedisCache> redis)
: l1Cache_(l1Cache), bloom_(bloom), redis_(redis) {}
/**
* @brief 查询用户信息(带缓存策略)
*/
std::optional<std::string> getUser(const std::string& userId) {
const std::string key = "user:" + userId;
// L1: 本地缓存(最快)
auto l1Val = l1Cache_->get(key);
if (l1Val.has_value()) {
return l1Val; // 命中L1,直接返回
}
// 检查是否为缓存的空值(防穿透)
if (l1Cache_->isNull(key)) {
return std::nullopt; // 已知不存在
}
// BloomFilter: 快速拦截无效ID
if (!bloom_->mightContain(key)) {
// 确定不存在,L1缓存空值60秒
l1Cache_->setNull(key, 60);
return std::nullopt;
}
// L2: Redis缓存
auto l2Val = redis_->get(key);
if (l2Val.has_value()) {
// 回填L1缓存(TTL 300秒)
l1Cache_->set(key, l2Val.value(), 300);
return l2Val;
}
return std::nullopt;
}
/**
* @brief 更新用户信息(缓存一致性策略)
*/
bool updateUser(const std::string& userId, const std::string& newData) {
const std::string key = "user:" + userId;
// 1. 更新数据库(伪代码)
// bool dbSuccess = database.updateUser(userId, newData);
// if (!dbSuccess) return false;
// 2. 删除/更新缓存(Cache-Aside策略)
redis_->del(key); // 删除L2,下次查询时回填
l1Cache_->del(key); // 删除L1
// 3. 或:直接更新缓存(Write-Through策略)
// redis_->set(key, newData, 3600);
// l1Cache_->set(key, newData, 300);
// 4. 确保布隆过滤器中有该key
bloom_->add(key);
return true;
}
private:
std::shared_ptr<dfs::redis::LocalCache> l1Cache_;
std::shared_ptr<dfs::redis::BloomFilter> bloom_;
std::shared_ptr<dfs::redis::RedisCache> redis_;
};
// 使用示例
void integratedExample() {
// 初始化各组件
auto l1Cache = std::make_shared<dfs::redis::LocalCache>(5000, 300);
dfs::redis::BloomFilterConfig bfConfig;
bfConfig.expectedItems = 500000;
bfConfig.falsePositiveRate = 0.01;
auto bloom = std::make_shared<dfs::redis::BloomFilter>(bfConfig);
// Redis连接池和缓存
dfs::redis::PoolConfig poolConfig;
poolConfig.host = "127.0.0.1";
poolConfig.port = 6379;
auto pool = std::make_shared<dfs::redis::RedisConnectionPool>(poolConfig);
auto redis = std::make_shared<dfs::redis::RedisCache>(pool);
// 创建服务
MultiLevelCacheService service(l1Cache, bloom, redis);
// 业务调用
auto user = service.getUser("1001");
if (user) {
std::cout << "User data: " << user.value() << std::endl;
}
// 更新用户
service.updateUser("1001", R"({"name":"Alice","updated":true})");
}