在撸代码时,利用局部性原理对数据作缓存是一种经常使用的性能优化手段。node
要作缓存,离不开的就是缓存组件。ccache就是一个很优秀的lru缓存组件,其作了不少很巧妙的优化策略来下降锁冲突,实现高性能。git
下降锁冲突的策略有github
下面看下具体是怎么实现的。缓存
在分析源代码前,先简单了解下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核心数量愈来愈多,加锁对性能的损耗是很是大的。
针对上面的问题,ccache采用了下面几种优化策略,都很是的巧妙。
这是个很常见的策略。
将一个hashtable根据key拆分红多个hashtable,每一个hashtable对应一个锁,锁粒度更细,冲突的几率也就更低了。
如图所示,一个hashtable根据key拆分红三个hashtable,锁也变成了三个。这样当并发访问hashtable1和hashtable2时,就不会冲突了。
value中新增一个访问计数,每次get操做时,计数+1。当计数达到阈值时,才将其移动到list的队首,同时将计数重置为0。
如阈值是3,那么对list的写操做就会下降3倍,锁冲突的几率也会减小3倍。
这是一个有损的策略,会使list的顺序不彻底等同于访问时间序。但考虑到lru cache的get操做频率很高,这种策略对命中率的损失应该是能够忽略的。
在get和set操做时,都须要更新记录访问时间序的list,但更新操做只须要在下次set操做前完成就能够,并不须要实时更新。基于这一点,能够单独开一个更新线程对list作更新。get和set时,提交更新任务到队列中,更新线程不停从队列中取任务作更新。
这样作有两个好处
这样会带来一个问题,当cpu核心不少,get和set的qps很高时,这个更新线程可能成为瓶颈。不过考虑到list的操做是很是轻量的,再加上服务不可能所有资源都放到读写cache上,这点也是能够忽略的。
当缓存满了后,一次淘汰一批元素。优化在缓存满了的时候,每次set新元素都会触发淘汰的问题。
在实现完上述策略后,总体流程大体是这样的