go语言高性能缓存组件ccache分析

1. 背景

在撸代码时,利用局部性原理对数据作缓存是一种经常使用的性能优化手段。node

要作缓存,离不开的就是缓存组件。ccache就是一个很优秀的lru缓存组件,其作了不少很巧妙的优化策略来下降锁冲突,实现高性能。git

下降锁冲突的策略有github

  • 一个元素在累计被访问屡次后才作提权(提权指将元素移动到lru链的头部)
  • 将提权操做放到一个队列中,由一个单独的线程作处理
  • 在同一个线程中作垃圾回收操做

下面看下具体是怎么实现的。缓存

2. lru cache

在分析源代码前,先简单了解下lru cache是作什么的。安全

lru为least recently used的缩写,顾名思义,lru cache在缓存满后,再缓存新内容需先淘汰最久未访问的内容。性能优化

要实现lru策略,通常是用hashtable和list数据结构来实现,hashtable支持经过key快速检索到对应的value,list用来记录元素的访问时间序,支持淘汰最久未访问的内容。以下图服务器

在hashtable中,key对应的内容包含两部分,第一部分为实际要存储的内容,这里定义为value,第二部分是一个指针,指向对应在list中的节点,将其定义为element。数据结构

在list中,每一个节点也包含两个部分,第一个部分是一个指针,指向hashtable中对应的value,这里定义为node,第二部分是next指针,用来串起来整个链表。多线程

若咱们执行get(key2)操做,会先经过key2找到value2和element2,经过element2又能找到node2,而后将node2移动到list队首,因此执行完get(key2)后,上图会变为并发

这时假如又有一个set(key5, value5)操做,而咱们的cache最多只能缓存4条数据,会怎么处理呢。首先会在hashtable中插入key5和value5,而且在list的队首插入node5,而后取出list队尾的元素,这里是node4,将其删除,同时删除node4对应的在hashtable中的数据。执行完上图会变成

经过上述流程,能够很好的实现lru策略。可是因hashtable和list这两种数据结构都不是线程安全的,若要在多线程环境下使用,不管set操做仍是get操做都须要加锁,这样就会很影响性能,特别是如今的服务器cpu核心数量愈来愈多,加锁对性能的损耗是很是大的。

3. ccache优化策略

针对上面的问题,ccache采用了下面几种优化策略,都很是的巧妙。

3.1 对hashtable作分片

这是个很常见的策略。

将一个hashtable根据key拆分红多个hashtable,每一个hashtable对应一个锁,锁粒度更细,冲突的几率也就更低了。

如图所示,一个hashtable根据key拆分红三个hashtable,锁也变成了三个。这样当并发访问hashtable1和hashtable2时,就不会冲突了。

3.2 累计访问屡次才作提权

value中新增一个访问计数,每次get操做时,计数+1。当计数达到阈值时,才将其移动到list的队首,同时将计数重置为0。

如阈值是3,那么对list的写操做就会下降3倍,锁冲突的几率也会减小3倍。

这是一个有损的策略,会使list的顺序不彻底等同于访问时间序。但考虑到lru cache的get操做频率很高,这种策略对命中率的损失应该是能够忽略的。

3.3 单开一个线程更新list

在get和set操做时,都须要更新记录访问时间序的list,但更新操做只须要在下次set操做前完成就能够,并不须要实时更新。基于这一点,能够单独开一个更新线程对list作更新。get和set时,提交更新任务到队列中,更新线程不停从队列中取任务作更新。

这样作有两个好处

  • list不存在多线程访问,不需加锁
  • 操做完hashtable直接返回,异步更新list,函数相应速度更快

这样会带来一个问题,当cpu核心不少,get和set的qps很高时,这个更新线程可能成为瓶颈。不过考虑到list的操做是很是轻量的,再加上服务不可能所有资源都放到读写cache上,这点也是能够忽略的。

3.4 批量淘汰

当缓存满了后,一次淘汰一批元素。优化在缓存满了的时候,每次set新元素都会触发淘汰的问题。

3.5 总体流程

在实现完上述策略后,总体流程大体是这样的

3.5.1 get操做

3.5.2 set操做

相关文章
相关标签/搜索