缓存组
这个是分布式缓存里面的核心设计,具有本地缓存,远程结点访问,数据加载的能力,具体来说就是当有数据被访问之后,会存在本地,通过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;
}
总结
本篇文章到这里结束了!!!!希望可以帮助大家理解~~~~