YYCache源码阅读之内存缓存设计

1,072 阅读3分钟

YYCache的整体结构是分为两部分:内存缓存和磁盘缓存。

内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。

为什么需要缓存?

内存的读写速度远大于磁盘的读写速度,将频繁使用的数据存在内存中,再次用到的时候直接从内存中读取,从而提高性能。

设计内存缓存的几个要点

1. 缓存算法:根据场景选择合适的缓存算法,才能使缓存有较高的命中率。常见的缓存算法有:LRU(最近最少使用)、LRU-2(类似LRU,准入门槛变成2次)、LFU(最不经常使用)、FIFO(先进先出)等等。

2. 读写性能:相同情况下,存储数据和读取数据的速度。

3. 线程安全:考虑到可能会在多个线程中读写缓存。

LRU算法


LRU算法(least recently used)顾名思义,淘汰掉最近最少使用的数据。

1. 访问的数据未命中缓存,如果此时缓存未满的时候。直接将新数据插到最前面(这也决定了基础结构不能用数组,只能用链表,数组插入头部时间复杂度为O(n),链表插入头部时间复杂度为O(1))。

2. 访问的数据命中缓存,将访问的数据插到链表的最前面。

3. 访问的数据未命中数据,但是此时缓存已经满了,此时需要先淘汰掉链表的尾部,然后再见新数据插到最前面。

YYModel内存缓存的设计

YYModel的内存缓存部分是YYMemoryCache这个类实现的。YYMemoryCache使用了LRU缓存算法,由hashMap+双链表实现。因为hashMap的读操作是O(1),用于保证读取速度。双链表的删除和插入操作都是O(1),很好的保证了写操作的速度。

为什么不用单链表?

1. 单链表的删除操作需要读到前一个节点,而读取前一个节点需要从头遍历。

2. 有人会说,也不是吧,可以将下一个节点的数据赋给当前节点,然后删除下一个节点,这样就相当于删除了当前节点(删除了当前节点的数据)。首先这样有一遍拷贝数据的操作,如果删除的是最后一个,这种做法就没有了。而缓存算法的淘汰机制刚好删除的都是最后一个。

YYModel结构


hashmap作用在于快速读取。双链表用于维护顺序,实现淘汰机制。

LRU机制对应的伪代码

1. 访问的数据未命中缓存,如果此时缓存未满的时候。直接将新数据插到最前面

if (!hashmap[@"key"]) {
    Node *node = [[Node alloc] init];
    node.data = data;
    hashmap[@"key"] = node;
    if (self.linkedList.head) {
        node.next = self.linkedList.head;
        node.prev = nil;
        self.linkedList.head.prev = node;
        self.linkedList.head = node;
    } else {
        node.prev = nil;
        node.next = nil;
        self.linkedList.head = node;
        self.linkedList.tail = node;
    }
}

2. 访问的数据命中缓存,将访问的数据插到链表的最前面。

if (hashmap[@"key"]) {
    Node *node = hashmap[@"key"];

    if (node == self.linkedList.head) {
    	return;
    } else if (node == self.linkedList.tail) {
    	node.prev.next = nil;
    	self.linkedList.tail = node.prev;
    	node.next = self.linkedList.head;
    	self.linkedList.head.prev = node;
    	self.linkedList.head = node;
    } else {
    	node.prev.next = node.next;
    	node.next.prev = node.prev;
    	node.next = self.linkedList.head;
    	self.linkedList.head.prev = node;
    	self.linkedList.head = node;
    }
}

3. 访问的数据未命中数据,但是此时缓存已经满了,此时需要先淘汰掉链表的尾部,然后再见新数据插到最前面。

Node *node = self.linkedList.tail;
node.prev.next = nil;
self.linkedList.tail = node.prev;
[hashmap removeObjectForKey:@"key"];
free(node);

线程安全

1. 读写的时候加锁保证线程安全

2. 删除释放节点在异步线程,提高性能。

if (_lru->_totalCost > _costLimit) {
    dispatch_async(_queue, ^{
    [self trimToCost:_costLimit];
    });
}