Redis:(4) 缓存穿透、布隆过滤器与多级缓存

1. 缓存穿透的概念

问题 现象 原因 解决方案
缓存穿透 查询不存在的数据,每次请求都打到 DB 恶意攻击/bug 导致查询不存在的 key 布隆过滤器 + 缓存空值
缓存击穿 热点 key过期瞬间,大量请求打到 DB 热点 key 过期 + 高并发 互斥锁/逻辑过期
缓存雪崩 大量 key 同时过期,DB 压力骤增 缓存集中失效/Redis 宕机 随机过期时间 + 多级缓存 + 限流

2. 方案1:布隆过滤器

2.1 原理

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否属于一个集合。

由以下两部分组成:

  1. 位数组 (Bit Array):长度为 m 的二进制数组,初始所有位均为 0。
  2. 哈希函数 (Hash Functions):k 个相互独立的哈希函数,每个函数能将输入元素映射到位数组的一个索引位置 [0, m-1]。

添加元素 :

  1. 输入元素 item。
  2. 使用 k 个哈希函数计算得到 k 个索引值:h1(item), h2(item), ..., hk(item)。
  3. 将位数组中这 k 个索引位置的位设置为 1,若某位已为 1,则保持为 1。

查询元素:

  1. 输入元素 item。
  2. 使用相同的 k 个哈希函数计算得到 k 个索引值。
  3. 检查位数组中这 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})");
}
相关推荐
heimeiyingwang1 小时前
企业非结构化数据的 AI 处理与价值挖掘
大数据·数据库·人工智能·机器学习·架构
山岚的运维笔记1 小时前
SQL Server笔记 -- 第63章:事务隔离级别
数据库·笔记·sql·microsoft·oracle·sqlserver
LZY16191 小时前
MySQL下载安装及配置
数据库·mysql
亓才孓2 小时前
[Mybatis]Mybatis框架
java·数据库·mybatis
tod1132 小时前
Redis 主从复制与高可用架构:从原理到生产实践
数据库·redis·架构
l1t2 小时前
DeepSeek辅助生成的PostgreSQL 表结构设计幻灯片脚本
数据库·postgresql
橘子132 小时前
redis哨兵
数据库·redis·缓存
yzs872 小时前
OLAP数据库HashJoin性能优化揭秘
数据库·算法·性能优化·哈希算法
与衫2 小时前
如何将SQLFlow工具产生的血缘导入到Datahub平台中
java·开发语言·数据库