分布式存储----缓存组

缓存组

这个是分布式缓存里面的核心设计,具有本地缓存,远程结点访问,数据加载的能力,具体来说就是当有数据被访问之后,会存在本地,通过lru算法,如果在本地没有访问找到数据,就会向其他结点发出请求,查询是否有请求的数据,如果还是没有找到,就会在数据库里面加载数据

所以这个设计允许结点之间相互访问数据,同时保持本地的高效访问

一个group里面的的核心是PeerPicker,我们客户要查询数据的时候会传递组的名字,所以我们会直接锁定我们要查询的组,然后会在本地缓存里面查询数据,如果没有查询到数据,就会通过一致哈希性,尝试找到我们需要的虚拟结点,但是负责的结点不一定有这个数据的缓存,如果没有找到,那么需要加载数据,从数据库或者指定的文件路径,把这个数据放到该结点里面。

这里面一定要注意,一个结点会有多个缓存组

这也说明多个结点会存同一个数据,当本地结点没有数据的时候,会通过一致性哈希找到确定的结点,而多个结点存同一个数据的目的就是为了当客户端连接到结点时候,可以不用访问其他结点,而是直接从本地缓存取出数据即可

每一个CacheGroup都代表了一个独立的缓存空间,使用LruCache作为本地缓存,通过singleFlight防止缓存击穿,每一个结点即使客户端也是服务器端,InvalidateFromPeer这个函数很好的验证了这一点,我们客户端连接的时候是随意连接这个组里面的某一个结点的,所以当通过一致性哈希发现这个结点在其他地方的时候,我们会通过gRPC向远程结点请求数据,如果就是当前这个结点,那么就直接调用getter。所以当一个结点收到其他结点的请求的时候,会把这个数据发送给目标节点,那么目标结点也会存同样一个数据。这就会导致多个结点有着同一份数据,当用户需要修改数据的时候,会通知其中的一个结点,为了防止其他有着相同数据的结点存放着失效的数据,我们会用广播的形式,通知那些存放数据的结点,删除原来失效的数据、

然后我们每一个结点里面的缓存组存储数据也是LRU

cpp 复制代码
class CacheGroup
{
public:
    CacheGroup() = default;

    CacheGroup(std::string_view name, int64_t bytes, DataGetter getter)
        : cache_(std::make_unique<LRUCache>(bytes)), name_(name), getter_(getter)
    {}

    CacheGroup(const CacheGroup&) = delete;
    auto operator=(const CacheGroup& other) -> CacheGroup = delete;

    CacheGroup(CacheGroup&& other)
    {
        cache_ = std::move(other.cache_);
        name_ = std::move(other.name_);
        getter_ = std::move(other.getter_);
    }

    auto operator=(CacheGroup&& other) -> CacheGroup&
    {
        cache_ = std::move(other.cache_);
        name_ = std::move(other.name_);
        getter_ = std::move(other.getter_);
        return *this;
    }

    auto Get(const std::string_view& key) -> ByteViewOptional;
    bool Set(const std::string_view& key, ByteView data);
    bool Delete(const std::string_view& key);

    // 处理来自其他结点失效的请求
    bool InvalidateFromPeer(const std::string_view& key);

private:
    auto Load(const std::string_view& key) -> ByteViewOptional;
    auto LoadData(const std::string_view& key) -> ByteViewOptional;

    std::unique_ptr<LRUCache> cache_; // 本地缓存
    std::string_view name_;
    std::atomic<bool> is_close_{false};
    DataGetter getter_;
    SingleFlight loader_;
    GroupStatus status_;
};

对于这一个类,我们对外提供了两个接口,一个是创建缓存组,一个是得到缓存组,我们一般不会调用其构造函数。我们请求结点的时候通过gRPC,gRPC Server就会接收到请求之后找到结点得到或者创建缓存组

cpp 复制代码
auto MakeCacheGroup (const std::string_view& name, int64_t bytes, DataGetter getter) -> CacheGroup&;
auto GetCacheGroup (const std::string_view& name) -> CacheGroup*;
cpp 复制代码
auto MakeCacheGroup(const std::string_view& name, int64_t bytes, DataGetter getter) -> CacheGroup&
{
    if (getter == nullptr)
    {
        std::exit(1);
    }
    std::lock_guard<std::mutex> lock(mutex_);
    cache_groups[name] = std::move(CacheGroup{name, bytes, getter});
    return cache_groups[name];
}

auto GetCacheGroup(const std::string_view& name) -> CacheGroup*
{
    std::lock_guard<std::mutex> lock(mutex_);
    if (cache_groups.find(name) == cache_groups.end())
    {
        return nullptr;
    }
    return &cache_groups[name];
}

这一个是加载数据,加载数据从数据库里面加载,用到了SingleFlight机制,用lambda表达式把我们需要的函数传递进去,得到数据之后我们还需要更新缓存,这个getter_是一个function,传递进去的是std::string_view,返回ByteViewOptional,我们初始化getter_的初始化是在CacheGroup的构造函数里面传递进去

cpp 复制代码
auto CacheGroup::Load(const std::string_view& key) -> ByteViewOptional
{
    auto ret = loader_.Do(key.data(), [&] {return LoadData(key);});
    if (!ret)
    {
        return std::nullopt;
    }
    // 更新本地缓存
    cache_->Set(key, ret.value());
    return ret;
}

auto CacheGroup::LoadData(const std::string_view& key) -> ByteViewOptional
{
    auto val = getter_(key);
    if (!val)
    {
        return std::nullopt;
    }
    status_.local_hits++; // 本地缓存次数加1
    return val;
}

得到数据就是从cache里面通过LRU算法取数据,如果没有,就调用load从本地缓存加载数据

我们这里set数据之后会同步数据,但是这个环节还不在这一块完成,在后面会一一实现

cpp 复制代码
auto CacheGroup::Get(const std::string_view& key) -> ByteViewOptional
{
    if (is_close_)
    {
        return std::nullopt;
    }
    if (key == "")
    {
        return std::nullopt;
    }
    // 先从本地缓存中获取
    auto ret = cache_->Get(key);
    if (ret)
    {
        status_.local_hits++; // 本地缓存命中+1
        return ret;
    }

    status_.local_misses++; // 本地缓存未命中+1
    return Load(key);
}

bool CacheGroup::Set(const std::string_view& key, ByteView data)
{
    if (is_close_)
    {
        return false;
    }
    if (key.empty())
    {
        return false;
    }
    cache_->Set(key, data);
    return true;
}

bool CacheGroup::Delete(const std::string_view& key)
{
    if (is_close_)
    {
        return false;
    }
    if (key.empty())
    {
        return false;
    }
    cache_->Delete(key);
    return true;
}

bool CacheGroup::InvalidateFromPeer(const std::string_view& key)
{
    if (is_close_)
    {
        return false;
    }
    if (key.empty())
    {
        return false;
    }
    // 其他节点通知当前节点:这个 key 的缓存可能已经过期,不能继续用了,所以要删除本地这份缓存,避免后续返回旧数据
    /*
    A 的缓存:用户名字 = "张三"
    B 修改了数据:用户名字 = "李四"
          ↓ 
    B 向 A 发送失效请求
          ↓
    A 删除本地缓存中的 "张三"
          ↓
    下次查询时缓存未命中,再走正常加载流程获取新数据
    */
    cache_->Delete(key);
    return true;
}

总结

本篇文章到这里结束了!!!!希望可以帮助大家理解~~~~

相关推荐
znnnk1 小时前
【网络优化】减少B站直播卡顿的上行占用:AdGuard nowebrtc规则 / WebRTC Control 实测
服务器·网络·webrtc·直播·bilibili·adguard
艾iYYY1 小时前
思考: C++ 万能引用与完美转发
c++·windows·microsoft
余额瞒着我当琳1 小时前
C++算法学习篇5--二分查找法
c++·学习·算法
2401_868534781 小时前
论“网络去NAT”的规划与设计
服务器·网络
华允物联-HUAIOT1 小时前
串口服务器支持 WiFi 吗?有线与无线串口联网科普
运维·服务器·单片机
水饺编程1 小时前
第2章:Linux 基础,whoami 命令
linux·c语言·汇编
蒸蒸yyyyzwd2 小时前
秋招学习笔记 Day 64 之准备系统软件开发工程师岗位笔记
c++·八股
鱼很腾apoc2 小时前
【MySQL】 第2期 查询与函数系统详解:从基础 CRUD 到复合多表查询
linux·数据库·学习·mysql·dml
奇树谦2 小时前
LRU 缓存原理与 C++ 实践示例
开发语言·c++·缓存