如何设计分布式缓存-浅谈

最近在看极客兔兔大佬的七天用Go从零实现系列,其中有个分布式缓存geeCache,从设计的角度整理下自己的想法和思路。

如何设计分布式缓存?

设计一个分布式缓存系统,需要考虑资源控制、淘汰策略、并发、分布式节点通信等各个方面的问题。

从上述方面考虑,我们需要实现的功能如下

1、缓存功能以及缓存淘汰策略

2、缓存支持并发读取

3、分布式节点通信 客户端和服务端

4、使用一致性哈希解决分布式负载均衡问题

 

1、缓存功能以及缓存淘汰策略

常见的缓存淘汰策略有三种

FIFO First In First Out 先进先出

淘汰缓存中最早添加的记录。

思想:最早添加的记录,其不被使用的可能性比刚添加的可能性大

实现:通过创建一个队列,新增记录添加到队尾,删除队首记录

缺点:很多场景下,部分记录虽然最早添加但是也常被访问,这些数据命中率会降低。

LFU Least Frequently Used 最少使用

淘汰缓存中访问频率最低的记录

思想:数据过去被访问多次,将来被访问频率会更高

实现:维护一个按照访问次数排序的队列,每次访问,次数加一。删除时找到访问次数最小的删除。

缺点:维护每个记录的访问次数,对内存消耗很高。某个数据历史访问次数奇高,但在之后几乎不被使用,这样就迟迟无法删除。

LRU Least Recently Used 最近最少使用

淘汰缓存中最近最少使用的记录

思想:如果数据最近被访问过,将来被访问的概率会更高

实现:维护一个队列,如果记录被访问,将其移动到队尾。那么队首即是最近最少访问的记录,淘汰该记录即可。

缺点:相对比较平衡

综上所述我们采用LRU算法来淘汰数据

使用双向链表+字典实现,读取和插入以及移动和删除复杂度都是O(1)

具体实现简述

1、创建一个结构体 Cache

包含缓存最大长度、缓存当前长度、双向链表、map、以及回调函数

2、实现结构体方法

2.1、new方法 实例化一个Cache

2.2、查询方法 通过key从Cache中查找记录

2.3、淘汰方法 当Cache空间满了后淘汰最近最少使用的记录

2.4、插入方法 通过key和Value将记录插入

到此我们已经实现了一个带有LRU淘汰算法的缓存系统了。

但是目前的缓存并不支持并发访问,所以我们需要使用互斥锁来封装下这个Cache,使其支持并发访问。

 

2、缓存支持并发读取

使用mutex来实现并发缓存

2.1、创建一个ByteView数据结构

2.2、创建一个cache结构体,包含了一个mutex互斥锁、上述实现的lru以及缓存大小

2.3、自定义数据来源

2.4、创建一个group和group池。负责与用户的交互,并且控制缓存值存储和获取的流程。group如果没有数据,则从自定义的数据来源中获取数据

 

3、分布式节点通信 客户端和服务端

分布式节点通信分为客户端和服务端,这个就不细说了

 

4、使用一致性哈希解决分布式负载均衡问题

什么是一致性哈希

一种用来解决分布式系统中分布和负载均衡问题的算法。

为什么要使用一致性哈希

解决单机服务器容量限制问题,保证某些服务器宕机时数据不丢失

在新增或删除节点时,仅需要修改部分数据,减少数据迁移量。

一致性哈希工作原理

一致性哈希算法将key有映射到2^32的环空间中

计算节点的哈希值,放置在环上。

计算key的哈希值,放置在环上,顺时针找到第一个节点,就是应该选取的节点。

如果服务器节点过少,容易引起数据倾斜,数据分布不均匀。

因此引入了虚拟节点的概念。

一个真实节点对应多个虚拟节点。

只需要新增一个map维护真实节点和虚拟节点的映射关系即可。

实现过程

创建一个结构体,包含hash函数、真实节点对应虚拟节点数量、存放所有虚拟节点的hash环、以及真实节点和虚拟节点的映射表

创建一个New方法,传入真实节点对应的虚拟节点数量和hash函数,返回一个Map实例

实现给map添加真实节点的函数Add

功能如下

对于每个真实节点,创建replicas个虚拟节点,遍历replicas,将索引和真实节点key一起hash得到虚拟节点的hash值,将其加入hash环中和映射表中。给hash环重排序

实现给key查找真实节点的函数Get

功能如下

如果hash环为空,返回空

获取key的hash值,顺时针找到环上的虚拟节点索引,然后通过映射表获取真实节点。

除了上述功能外,大佬的博客还实现了两个功能

1、缓存击穿预防

2、使用protobuf实现通信优化

 

1、缓存击穿预防

什么是缓存雪崩?

缓存在同一个时刻全部失效,导致瞬时DB请求量大。

通常是因为缓存服务器宕机、缓存key设置相同过期时间。

什么是缓存击穿?

一个存在的key,在缓存过期的时刻,同时有大量请求,这些请求会击穿缓存到达DB。

什么是缓存穿透?

查询不存在的数据,但是缓存没有该数据,每次都会请求DB。可能导致DB宕机

创建一个call和group结构体

call表示一个调用请求

group则表示当前所有的调用请求,用一个map存储call

实现do方法,do方法主要实现并发call调用

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error)

先加锁,

如果m为初始化,先初始化m

判断如果目前的key已经被请求了,那么就等待。返回请求后的数据

否则创建一个新的调用,发起请求,请求结束后从map中删除并返回。

 

2、使用protobuf实现通信优化

这个也不细说了。

 

总结

个人认为该系统可能可以改进的地方

一、数据超时机制

二、持久化机制

三、数据冗余,一旦某个节点挂掉,因为没有持久化,也没有数据冗余,所以就没法实现数据恢复。

因此,该系统的使用场景还比较有限。

posted @ 2023-05-15 10:19  菲菲菲菲菲常新的新手  阅读(55)  评论(0编辑  收藏  举报