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 位与操作 (bitsword & (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})");
}
相关推荐
Database_Cool_几秒前
阿里云 RDS MySQL vs 腾讯云 CDB 国内云关系型数据库深度对比
数据库·mysql·阿里云
xiaohaiAIgeo几秒前
【2026年】基于三维建模的实验室智慧管理平台:暖通能源照明的数字孪生方案
大数据·数据库·人工智能·科普知识
深盾科技_Virbox1 小时前
软件授权工具静默安装实践
java·运维·数据库·安全·软件需求
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第172题】【07_Redis篇】第8题:`setnx` 做分布式锁存在的问题
java·redis·分布式锁·死锁·setnx
延凡科技1 小时前
延凡科技综合监控预警处置平台—— 一体化视频AI安防闭环系统设计与功能实现[特殊字符]️
数据库·人工智能·科技·安全·能源
老马聊技术1 小时前
Rocky Linux 9.0 安装 MySQL8.x详细教程
linux·数据库
醇氧2 小时前
主流 Agent 开发框架全解析(2026 最新)
大数据·数据库·人工智能·开源
吴声子夜歌2 小时前
Redis 6.x——整合SpringBoot
数据库·spring boot·redis
ATA88882 小时前
AI辅助生成SQL实战从连接配置到执行计划优化的完整技术流程
数据库·人工智能·智能问数
咏方舟【长江支流】2 小时前
【开源】跨语言·跨平台·跨数据库(4) ——用宝框架 ORM 数据源适配器
数据库·开源·ai编程·orm·咏方舟-长江支流·用宝框架·用宝框架orm