0%

进程内缓存bigcache

github地址:https://github.com/allegro/bigcache

简介

先扒一波官方文档凑字数

Fast, concurrent, evicting in-memory cache written to keep big number of entries without impact on performance. BigCache keeps entries on heap but omits GC for them. To achieve that, operations on byte slices take place, therefore entries (de)serialization in front of the cache will be needed in most use cases.

机器翻译:快速,并发,驱逐内存中的高速缓存,以保持大量的条目不影响性能.BigCache将条目保存在堆中,但忽略了它们的GC。为此,对字节片进行操作,因此在大多数用例中都需要缓存前面的条目(反序列化)。

虽然redis具有非常不错的性能(官方数据读的速度是110000次/秒,写的速度是81000次/秒),但是这并不等于我们一个进程在一秒可以对redis做好几万次操作。比如,现在基本都是用TCP协议建立与本地redis的连接,那么每次对redis进行操作都要把数据打包发到运输层,运输层再发到网络层,然后才返回发到本地的redis进程上。虽然使用pipeline可以减少一部分I/O时间消耗,但总归还是有性能瓶颈,很难达到每秒几万的操作效率。

因此,在没有使用分布式服务器的情况下,进程内缓存就显得很好用了。

此外,bigcache还拥有以下非常好用的特性:

  • 即使拥有百万记录也非常快(主要用哈希实现)
  • 提供并发访问
  • 在预定的时间后移除记录

简单使用

又来扒拉一波官方文档

1
2
3
4
5
6
7
8
9
10
11
import "github.com/allegro/bigcache"

func main() {
// 声明一个缓存
cache, _ := bigcache.NewBigCache(bigcache.DefaultConfig(10 * time.Minute))

cache.Set("my-unique-key", []byte("value"))

entry, _ := cache.Get("my-unique-key")
fmt.Println(string(entry))
}

使用该库主要有以下几个特点

  • 主要操作只有Set、Get、Delete、Len
  • 只能存取[]byte类型
  • config对象的设置对速度的影响较大
  • 需要用自定义的config时需要实现Hasher接口

config对象简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// Config for BigCache
type Config struct {
// Number of cache shards, value must be a power of two
// 分片类型的数量,用于并发优化
Shards int

// Time after which entry can be evicted
// 第一次删除的时间
LifeWindow time.Duration

// Interval between removing expired entries (clean up).
// If set to <= 0 then no action is performed. Setting to < 1 second is counterproductive — bigcache has a one second resolution.
// 删除的时间间隔
CleanWindow time.Duration

// Max number of entries in life window. Used only to calculate initial size for cache shards.
// When proper value is set then additional memory allocation does not occur.
// 大概就是删除队列的初始大小,在一开始分配,超过了就动态扩容
MaxEntriesInWindow int

// Max size of entry in bytes. Used only to calculate initial size for cache shards.
// 单个value的最大大小,用于计算初始cache shards大小
MaxEntrySize int

// Verbose mode prints information about new memory allocation
// 机器翻译:详细模式打印有关新内存分配的信息
Verbose bool

// Hasher used to map between string keys and unsigned 64bit integers, by default fnv64 hashing is used.
// 你要使用的哈希函数
Hasher Hasher

// HardMaxCacheSize is a limit for cache size in MB. Cache will not allocate more memory than this limit.
// It can protect application from consuming all available memory on machine, therefore from running OOM Killer.
// Default value is 0 which means unlimited size. When the limit is higher than 0 and reached then
// the oldest entries are overridden for the new ones.
// 最大占用内存,不够用时会覆盖就内存,和redis有点像
HardMaxCacheSize int

// OnRemove is a callback fired when the oldest entry is removed because of its expiration time or no space left
// for the new entry, or because delete was called.
// Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
// 由于各种原因删除数据时会触发的回调函数
OnRemove func(key string, entry []byte)

// OnRemoveWithReason is a callback fired when the oldest entry is removed because of its expiration time or no space left
// for the new entry, or because delete was called. A constant representing the reason will be passed through.
// Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
// Ignored if OnRemove is specified.
// 用于传递释放数据的原因,有Expired,NoSpace,Deleted三种
OnRemoveWithReason func(key string, entry []byte, reason RemoveReason)

onRemoveFilter int

// Logger is a logging interface and used in combination with `Verbose`
// Defaults to `DefaultLogger()`
// 日志
Logger Logger
}

优化的一些原理

1、并发优化

由于bigcache支持并发,因此势必会用锁(事实用的是读写锁)。在高并发场景下,如果只用一把读写锁,势必会显著影响读写速度。解决方案是用分块的思想(想必大家在数据结构上都听老师口糊过),把整个内存分成N个部分,每个部分单独分配一把锁保证并发安全。在哈希函数足够优秀的情况下,就可以实现同时允许好几个协程进行读写。

所以这部分的优化主要是N的选择以及哈希函数的选择。

(1)N的选择

首先N肯定不是越大越好,因为太大会带来很多不必要的开销(增加内存使用和寻找所在分块的时间),且性能提升也未必会很明显,所以要在性能和花销上寻找一个平衡。框架作者推荐的值是1024。

另外,N的值最好是2的幂,因为这样底层可以直接用位运算取模,进一步减少时间开销。

(2)Hash算法的选择

太玄学了还是算了

看了半天还是用他内置的FNV64a算法吧。

2、GC优化

Go1.5对GC进行了优化,如果map中的键值对都是基本类型,则GC会忽略他们。

因此框架作者利用这一特性,在每个分块中用map[uint64]uint32和一个预先分配的大字节数组(数组还是切片暂时没有证实)存储数据,把缓存对象序列化的后放到预先分配好的大字节数组中,把它的哈希值作为map[uint64]uint32的key,然后把它在数组中的offset作为map[uint64]uint32的value。

欢迎关注我的其它发布渠道