如何设计缓存

Posted by CoderLeonidas on June 1, 2017

如何设计缓存

从几个开源的iOS缓存相关框架找一些思路,然后再做一下总。

YYCache

作者ibireme在开发YYCache的时候也是调研了大量的缓存实现案例,比如系统的NSCache、NSDictionary和其他开源框架比如TMMemoryCache 、PINMemoryCache。YYCache也是实现了2级缓存,即内存缓存和磁盘缓存,并提供了简洁清晰的API接口,内部使用了LUR缓存淘汰算法来做缓存清理的逻辑。

值得参考的亮点:

线程安全

线程安全对于缓存设计十分关键。YYCache使用了2种锁来保证线程安全。

  • YYMemoryCache 使用了 pthread_mutex 线程锁(互斥锁)来确保线程安全

  • YYDiskCache 则选择了更适合它的 dispatch_semaphore。

关于这2种锁的选择也有讲究。

一开始作者使用的是OSSpinLock自旋锁和dispatch_semaphore信号量。

  • OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。

  • dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。

但由于后来苹果爆出OSSpinLock不再安全,所以就用pthread_mutex互斥锁来替代OSSpinLock(苹果官方也是这么做的)。

这里可以看出,不同锁的使用场景、性能等都是需要考虑的因素。

内存缓存设计的数据结构

在YYMemoryCache中,作者使用了双向链表来保存缓存节点,同时节点中包含了key和value,配合CFDictionary存储节点。

  • 双向链表

可以通过前驱和后继节点指针方便的进行节点的获取,移动,添加,删除等操作,并且避免了像数组中移除一个元素需要进行大量copy的耗性能的操作。

  • CFDictionary

可以通过key对节点达到时间复杂度为O(1)级别的的高速查找。

算法

使用了LRU淘汰算法。数据使用的时间越近,越不会被淘汰。当缓存空间不足,删除使用时间最远的那条数据。

选择底层的类

同样是字典实现,但是作者使用了更底层且快速的CFDictionary而没有用NSDictionary来实现。

异步释放

无论缓存的自动清理和释放,作者默认把这些任务放到子线程去做。

参考文章

YYCache 源码解析

YYCache 设计思路