Design a high performance cache for multi-threaded environment

如何设计一个支持高并发的高性能缓存库html

不 考虑并发状况下的缓存的设计你们应该都比较清楚,基本上就是用map/hashmap存储键值,而后用双向链表记录一个LRU来用于缓存的清理。这篇文章 应该是讲得很清楚http://timday.bitbucket.org/lru.html。可是考虑到高并发的状况,如何才能让缓存保持高性能呢?redis

高并发缓存须要解决2个问题:1. 高效率的内存分配;2. 高效率的读取,插入和清理数据。关于第一个问题涉及到高效率的内存分配器,使用成熟的jemalloc/tcmalloc足够知足需求。这里探讨下如何解决第二个问题。数组

由 于缓存系统的特色, 每次读取缓存都须要更新一些访问信息(最后读取时间,访问频率),在清理时会根据这些信息使用不一样的策略来进行数据清理,这样看来彷佛每次的读操做都变成 了写操做。看过几篇文章都比较集中在如何优化这个读操做修改LRU的行为。例如: http://www.ebaytechblog.com/2011 /08/30/high-throughput-thread-safe-lru-caching/#.UzvEb3V53No 以及 http://openmymind.net/High-Concurrency-LRU-Caching/, 可是这种状况下不论怎么优化,使用链表的LRU始终是个瓶颈, 由于每次读操做只能一个线程来修改这个LRU链表,而且修改都是集中在链表的两端。有些文章甚至使用lock-free的doubled linked list来减小锁竞争。 一些成熟的缓存系统如memcached,使用的是全局的LRU链表锁,而redis是单线程的因此不须要考虑并发的问题。因为这些都是远程的缓存服务 器,所以它们的瓶颈每每是网卡,因此并发上面并不须要什么高要求。缓存

仔 细思考后,发如今并发的状况下使用LRU链表来记录访问信息其实并不合适,会致使严重的锁竞争,这点没法避免。所以,须要完全放弃使用LRU链表。鉴于缓 存系统的特性,能够作以下假设: 缓存若是达到阀值,可使插入当即返回失败。这样,咱们可使用一个预分配的数组来记录全部的cache item的访问信息。整个结构以下:安全

concurrent cache system arch网络

可 以看到锁粒度是hashmap里面的bucket级别,每次读操做只需对相应的bucket加锁,而后更新bucket对应的访问信息数组元素便可,因为 每一个bucket对应的访问信息是独立占据数组的一个元素的,所以bucket锁就保证了访问信息的线程安全。这样就解决了读取操做的并发竞争问题。多线程

接 下来看看如何解决插入问题。从图能够看到,每一个bucket的须要分配一个独立的access info位置索引,多线程插入的时候会发生竞争,为了减小竞争,能够预先生成一个目前空闲的位置链表,这样插入的时候,每一个线程能够根据当前的 bucket索引选择从不一样的free链表里面分配一个位置。这样锁竞争能够分散到多个free list上面,每次插入时把分配过的位置索引从free list 移除。并发

最 后,清理过程能够放在一个独立线程里面,为了不插入由于缓存满了而返回失败,每次在缓存快满的时候(free list的size不够用了),进行一次access info array扫描。根据不一样的缓存清除策略和访问信息(时间和频率)来决定哪些位置索引是能够从新释放到free list列表。因为扫描过程当中无需加锁,扫描对读取和插入操做是没有性能影响的。只有最后进行释放时才会对须要释放的bucket和free list进行加锁,锁竞争大大减小。dom

如上设计,大大减小了缓存的读取,插入和清理过程当中的锁竞争问题,而且读取和插入都是O(1)的,并不会由于缓存系统的增大影响性能(清理后台线程可能会跑的久点,能够选择性清理来优化)。这样一个支持高并发的缓存系统就完成了。socket

简 单的实现后,实测在8核CPU上面8线程读,8线程写,能够跑到 读写TPS均在1M/S以上,参考官方单线程的redis的benchmark数据 Using a unix domain socket 排除网络瓶颈,SET/GET的TPS大概在200k/s 左右,能够看到这样一个高并发的cache基本上是scalable的。

相关文章
相关标签/搜索