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

缓存组

这个是分布式缓存里面的核心设计,具有本地缓存,远程结点访问,数据加载的能力,具体来说就是当有数据被访问之后,会存在本地,通过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;
}

总结

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

相关推荐
HideF7 小时前
Linux sudo
linux
FlightYe7 小时前
视界原理之3D视频(四):裸眼、眼镜和头显
android·linux·c++·3d·ffmpeg·音视频
利威尔xu8 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
蹊黎8 小时前
Linux 下用 C 语言实现进度条
linux·c语言·单片机
by209999 小时前
C++ 智能指针的所有权与使用
开发语言·c++·经验分享
知已难寻9 小时前
03 · OpenStack的计算管理 Nova
运维·服务器·开发语言·云原生·云计算·openstack·开源软件
Rover Ramble9 小时前
C++ 用互斥锁实现读写锁
开发语言·c++
你怎么知道我是队长9 小时前
C++零基础入门第2周:数据类型、运算符与表达式完全指南(附20段可运行代码+避坑清单)
c++
huainingning10 小时前
Windows syslog日志服务器搭建与模拟器对接
运维·服务器·智能路由器
牛马鸡niumasi10 小时前
【Linux】C语言文件操作
linux