
文章目录
本系列
- 本地缓存库分析(一):golang-lru(本文)
- 本地缓存库分析(二):bigcache
- 本地缓存库分析(三):freecache
- 本地缓存库分析(四):fastcache
- 本地缓存库分析(五):groupcache
本地缓存概览
在业务中,一般会将极高频访问的数据缓存到本地。以减少网络IO的开销,下游服务的压力,提高性能
一般来说能放到本地的数据需要满足下面两个限制:
- 数据量不是非常大:数据量大了本地内存撑不住
- 一致性,时效性要求不是非常高:毕竟多个服务的本地缓存很难做到同步更新,及时更新
如果用go自带的map实现本地缓存,大概有两种实现方式:
sync.Mapmap + mutex.RWLock
但有以下缺点:
- 锁竞争严重
- 大量缓存写入,导致gc标记阶段占用cpu多
- 内存占用不可控
- 不支持缓存按时效性淘汰
- 不支持缓存过期
- 缓存数据可以被污染:如果缓存的V是指针,那么业务修改了V的某个值为当前请求用户自己的值,在缓存中的V就被污染了
本系列要介绍的开源缓存库如何解决上述问题?
-
将大map拆分成多个小map,每个小map使用各自的锁
-
零GC:
-
使用堆外内存,不把对象放到堆上,自然不会被gc扫描。但要注意手动管理,需要及时释放内存
-
map的非指针优化:
- 如果kv都没有指针,不会扫描map。注意常用作key的string类型含有指针,会被gc扫描
- 将hash值作为key,value在底层数组中的offset作为value,这样KV都是int,就不会被GC扫描了
-
-
内存占用可控:
- 初始化时制定好底层数组的容量,数据写满时会覆写,这样永远不会超过容量
- 或者指定好最多能放多少KV对,但如果V大小不一,极端情况下内存占用会很大
-
支持缓存按时效性淘汰,例如使用LRU算法
-
支持数据过期
- 某些库在后台启定时任务,定时清理过期的KV
- 某些库会在Get时,惰性检查KV是否过期
-
避免缓存污染:存储Value序列化后的字节数组,而不是指针
- 但cpu开销会增大,每次写入缓存都要经过序列化,每次从缓存读都要经过反序列化。内存开销也变大,每次读都相当于拷贝一份出来
- 也就是用性能换取安全性
golang-lru
本文阅读源码:https://github.com/hashicorp/golang-lru,版本:v2.0.7
该库提供了3种LRU的实现:
-
lru:标准lru -
2q:类似mysql的buffer pool,分为冷数据和热数据两部分。如果某对KV只被添加到缓存中,而没有被查询,那么只会待在冷数据区域直到被淘汰,而不会占用热数据的空间- 为啥要冷热分离?考虑这样一种场景:假设缓存中有10条热点数据,突然有用户往缓存中写10条冷数据,因为容量不够,要淘汰掉所有的热数据。如果之后也不查这些冷数据,而是继续查之前的热数据。产生的后果是所有热数据都会
cacheMiss - 如果应用了冷热分离机制,这些冷数据只会写到冷数据区,然后在冷数据区被淘汰,而不会占用热数据的空间,避免了大量热数据的cacheMiss
- 为啥要冷热分离?考虑这样一种场景:假设缓存中有10条热点数据,突然有用户往缓存中写10条冷数据,因为容量不够,要淘汰掉所有的热数据。如果之后也不查这些冷数据,而是继续查之前的热数据。产生的后果是所有热数据都会
-
expirable_lru:支持过期时间的lru
标准lru
数据结构:包含一个双向链表和hash表

type LRU[K comparable, V any] struct {
// 容量
size int
// entry越靠近头部,越新
evictList *internal.LruList[K, V]
// hash表
items map[K]*internal.Entry[K, V]
onEvict EvictCallback[K, V]
}
LruList就是个带哨兵头节点root的双向链表,每个节点是Entry结构。哈希表items的value也是Entry机构
那么真正的头节点是root.Next
尾节点是root.Prev
type LruList[K comparable, V any] struct {
// 哨兵entry
root Entry[K, V]
// 已经放了多少entry
len int
}
entry结构如下:
type Entry[K comparable, V any] struct {
// 前后指针
next, prev *Entry[K, V]
// 属于哪个list,主要用于遍历链表时,在lru算法中没啥用
list *LruList[K, V]
Key K
Value V
}
初始化时root自己先形成一个环:
func (l *LruList[K, V]) Init() *LruList[K, V] {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
下面介绍一些在LRU算法中会用到的小方法
获取最后一个节点:
func (l *LruList[K, V]) Back() *Entry[K, V] {
if l.len == 0 {
return nil
}
return l.root.prev
}
将entry添加到头部:
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
l.lazyInit()
return l.insertValue(k, v, time.Time{
}, &l.root)
}
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
return l.insert(&Entry[K, V]{
Value: v, Key: k, ExpiresAt: expiresAt}, at)
}
就是普通的双链表操作,将e插到at的后面
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
e.prev = at
e.next = at.next
e.prev.next = e
e.

最低0.47元/天 解锁文章
2158

被折叠的 条评论
为什么被折叠?



