Cache框架
一. 分布式缓存需要解决的问题
在分布式缓存的场景中,设计和使用缓存时需要解决一些特殊的问题,确保缓存的高效性和一致性。以下是分布式缓存设计中需要重点解决的关键问题:
1. 数据一致性问题
- 最终一致性:由于分布式缓存是跨多个节点的,数据的一致性可能无法保证为强一致性,通常采用最终一致性模型,即一段时间内数据可能不一致,但最终会达到一致。
- 缓存同步:在多节点环境下,数据更新时如何同步缓存变得复杂。常用的解决方案包括缓存更新通知机制(例如使用发布/订阅模式)或一致性哈希算法来保证数据的分布。
2. 缓存失效与过期策略
- 缓存分区的过期:由于数据在不同的缓存节点上分布,失效和过期策略需要统一处理,避免一个节点数据失效,其他节点依然持有过期数据。
- 主动失效:分布式缓存需要支持主动失效,即某个数据被更新后,可以通过广播或点对点通知所有相关节点失效该缓存。
3. 分布式缓存的一致性哈希
- 分布式缓存往往使用一致性哈希(Consistent Hashing)来分布数据,确保缓存请求被分配到正确的节点,特别是当集群中的节点发生变更(如增加或减少节点)时,这种哈希方式能尽量减少缓存的迁移。
4. 缓存击穿、穿透和雪崩
- 缓存击穿:热点数据在缓存过期时,大量请求集中访问数据库。解决办法是对失效的热点数据加锁,或使用缓存预加载机制。
- 缓存穿透:查询不存在的数据会直接打到数据库,造成不必要的负载。可以通过在缓存中存储空值或使用布隆过滤器来防止穿透。
- 缓存雪崩:当大量缓存同时过期,导致后端数据库承受巨大压力。可以通过设置不同的过期时间、限流、以及请求打散等方式来防止雪崩。
5. 分布式锁
- 分布式缓存在高并发下的更新操作可能需要分布式锁来确保数据一致性,防止多个线程或节点同时修改同一条数据。可以使用Redis的分布式锁(如Redlock)来解决该问题。
6. 数据分区与负载均衡
- 数据分区(Sharding):在分布式缓存中,数据需要分片到不同的节点上。要设计合理的分片机制来分配数据,保证均匀的负载分布,避免某些节点负载过高。
- 负载均衡:缓存节点的负载均衡是关键。需要保证请求能被均衡地分配到多个缓存节点上,避免热点节点过载。
7. 高可用与故障恢复
- 缓存节点故障处理:在分布式系统中,节点可能随时宕机或掉线。因此,缓存系统需要具有高可用性和故障恢复机制,比如数据副本、主从架构或一致性协议(如Paxos、Raft)来保障服务的可用性。
- 副本冗余:为避免单节点故障导致数据丢失,可以使用数据冗余或副本机制,在多个节点上存储相同的数据,保证某个节点故障时,数据不会丢失。
8. 一致性模型的选择
- 根据业务需求,选择合适的一致性模型非常重要。在某些场景中,可能需要强一致性,而在其他场景中,最终一致性已经足够。例如,分布式缓存经常使用弱一致性或最终一致性,以保证高性能。
9. 数据的缓存预热和回收策略
- 在分布式环境中,缓存的预热非常重要,特别是在系统刚启动时,合理的缓存预热可以提高缓存的命中率。
- 缓存回收策略需要根据缓存大小和数据访问频率来设定,比如常见的LRU(Least Recently Used)、LFU(Least Frequently Used)等策略。
10. 监控与调优
- 在分布式缓存架构中,性能监控非常重要。需要监控缓存的命中率、失效率、响应时间以及集群状态等指标,及时发现和解决性能瓶颈。
- 对缓存系统的调优包括内存大小的调整、分布式锁的优化、缓存失效策略的调整等。
二. LRU
就是一个简单的LRU,连链表的哑结点、插入、删除都不需要自己实现
仅仅需要根据访问操作实现节点位置更新、以及容量更新,用到的知识点有Go接口特有的类型断言,以此访问list中数据
其次,还有给输入数据类型实现len()操作,方面计算容量并根据容量进行淘汰
三. 单机并发缓存
并发一方面是需要处理互斥,这里使用懒汉模式给group创造唯一对应的lru实例,同时对缓存的所有操作都上锁,使用cache结构实现
而跟用户交互的时候,每个group建立一个实例,同时新建对应的缓存
这里还要实现一个获取数据函数的接口,方便不同业务需求获取数据方式的传入,如果缓存中存在则直接获取,不存在,调用传入的方式,同时更新缓存
type Getter interface {
Get(key string) ([]byte, error)
}
// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)
// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {
return f(key)
}
用到的技术主要是接口函数的实现,以及通过反射动态判断类型,还有闭包回调函数捕获变量
四. HTTP服务端(这里与前面web结合,通过engine管理group)
分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。
对于服务器,使用NewGroup创建对应的缓存管理分组以及相应的本地资源获取方法
同时对外监听端口,提供一个解析地址的方式,响应该用户缓存请求,本质上还是http路由转发
只不过在相应的的处理函数上提供缓存数据返回服务,以及本地实现了缓存处理机制和用户缓存管理机制(包括注册的函数)
五. 一致性哈希(数据分布和负载均衡)
一致性哈希算法(Consistent Hashing) 是一种用于分布式系统中的哈希算法,它能够有效解决分布式缓存或数据库系统中数据分布和动态节点增加/删除的问题。它的核心特点是,当系统中的节点发生变化时(如节点增加或减少),能够最小化需要重新分配的数据量。
1. 一致性哈希算法的核心概念
在传统的哈希算法中,哈希函数将数据(如键或对象)映射到一个固定范围的哈希值,比如 0 到 n-1
。但这种方式有一个问题:当节点发生变化时(增加或删除节点),几乎所有的数据都需要重新分配,导致数据迁移成本非常高。一致性哈希算法通过一种环形的结构设计,能够解决这个问题。
一致性哈希的基本步骤:
- 一致性哈希使用一个哈希环的概念,将哈希空间(通常是 0 到 2^32-1 的整数空间)组织成一个虚拟的环。
- 每个节点(如缓存服务器、数据库节点)在这个环上通过某个哈希函数(如
SHA-1
或MD5
)进行哈希,映射到环上的某个位置。 - 数据(如缓存键或数据项)也通过相同的哈希函数映射到环上的某个位置。
- 数据存储在环上顺时针方向的第一个节点上。当节点发生增加或删除时,只需要重新分配少部分数据,而不是重新分配所有数据。
2. 一致性哈希算法的工作原理
一致性哈希的关键是通过哈希环实现数据和节点之间的映射:
-
哈希环:
- 一致性哈希将所有可能的哈希值组织成一个环状结构。例如,哈希值的范围可以是从 0 到 2^32-1,那么环的起点和终点就是 0 和 2^32-1。
-
节点映射:
- 使用哈希函数将每个节点(服务器)的唯一标识(如 IP 地址、主机名)进行哈希运算,结果映射到哈希环上的某个位置。
-
数据映射:
- 同样地,使用哈希函数将每个数据项(如缓存键、对象)进行哈希运算,结果也映射到哈希环上的某个位置。
-
数据存储:
- 对于某个数据项,其哈希值在环上顺时针方向的第一个节点负责存储该数据。
示例:
假设有 3 个节点 A、B、C 分布在一致性哈希环上。假设节点的哈希值分别是 A -> 10, B -> 30, C -> 70,而数据 D1、D2、D3 的哈希值分别是 15、45、85。根据顺时针方向的规则:
- D1 的哈希值是 15,顺时针第一个节点是 B,因此由 B 负责存储 D1。
- D2 的哈希值是 45,顺时针第一个节点是 C,因此由 C 负责存储 D2。
- D3 的哈希值是 85,环绕回起点,顺时针第一个节点是 A,因此由 A 负责存储 D3。
当节点发生变化时:
- 如果增加节点,如增加节点 D -> 50,那么它只会影响部分数据(如原本由 C 负责存储的 D2),其他数据的存储不变。
- 如果删除节点,如删除节点 C,则其数据(如 D2)将被重新分配到下一个顺时针节点 D。
3. 为什么要使用一致性哈希算法?
一致性哈希算法的最大优点是最小化数据的重新分配。在分布式系统中,节点的增减是常见的操作,如缓存服务器的扩容、缩容,数据库节点的添加或移除等。如果使用传统的哈希算法,这些节点变化会导致大量数据需要重新分配,带来巨大的网络和计算开销。
一致性哈希解决了这些问题,具体好处如下:
a. 节点动态变化时减少数据迁移
在传统哈希算法中,每次节点的增加或删除都会导致大量数据重新分配到不同的节点上,这样会造成缓存失效、性能下降,以及系统不稳定。而一致性哈希则能保证:
- 节点新增:只需要重新分配环上顺时针方向的部分数据给新加入的节点。
- 节点删除:只需要将被删除节点上的数据重新分配给下一个顺时针节点,其他数据不会受到影响。
b. 高可扩展性
一致性哈希允许系统在运行时动态地增加或移除节点,并且数据重新分配的成本很小。对于大规模分布式系统或缓存集群,一致性哈希非常适合,能够随着节点的增加或减少保持良好的扩展性。
c. 避免单点故障
通过一致性哈希,数据分布在多个节点上,减少了单点故障的风险。同时,一致性哈希也可以结合虚拟节点的策略来实现更均衡的负载分布。
d. 负载均衡
通过将数据和节点映射到环上,一致性哈希能够较为均匀地将数据分布到不同的节点上,避免某些节点的过载问题。通过增加虚拟节点的方式,可以进一步优化负载均衡。
4. 虚拟节点(Virtual Nodes)
为了提高负载均衡性,一致性哈希算法引入了虚拟节点的概念。虚拟节点是指为每个物理节点分配多个在哈希环上的虚拟位置,这样可以有效平衡数据的分布。
-
虚拟节点的作用:如果不使用虚拟节点,可能出现数据分布不均的情况(某些节点存储了大量数据,而其他节点存储较少数据)。引入虚拟节点后,每个物理节点在哈希环上对应多个虚拟节点,数据可以更均匀地分布到各个物理节点上。
-
如何实现虚拟节点:每个物理节点可以通过增加唯一标识来生成多个虚拟节点,比如 Node A 可以生成 A#1、A#2、A#3 等虚拟节点,这些虚拟节点分布在哈希环的不同位置。
type Map struct {
hash Hash
replicas int
keys []int // Sorted
hashMap map[int]string
}
// New creates a Map instance
func New(replicas int, fn Hash) *Map {
m := &Map{
replicas: replicas,
hash: fn,
hashMap: make(map[int]string),
}
if m.hash == nil {
m.hash = crc32.ChecksumIEEE
}
return m
}
// Add adds some keys to the hash.
func (m *Map) Add(keys ...string) {
for _, key := range keys {
for i := 0; i < m.replicas; i++ {
hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
m.keys = append(m.keys, hash)
m.hashMap[hash] = key
}
}
sort.Ints(m.keys)
}
// Get gets the closest item in the hash to the provided key.
func (m *Map) Get(key string) string {
if len(m.keys) == 0 {
return ""
}
hash := int(m.hash([]byte(key)))
// Binary search for appropriate replica.
idx := sort.Search(len(m.keys), func(i int) bool {
return m.keys[i] >= hash
})
return m.hashMap[m.keys[idx%len(m.keys)]]
}
六. 分布式节点
这部分也就是实现客户端服务器,判断本地是否存在缓存,如果否,则判断是否远程访问缓存,
如果否则调用回调本地将数据读取到本地缓存里面