如何实现缓存系统的更新机制
问题是这样的:假设要你设计一个缓存系统,你如何实现缓存的更新机制!
例如某网站具有高并发访问量,为提高性能,其中的一些网页页面都是存放在缓存中的,当需要更新缓存中的页面时,缓存系统如何完成缓存的更新?
在 给出解决方案之前,先看看内存数据库Redis中字典的设计。
Redis 中的字典结构表示如下:
typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ } dict;
我们暂时不管其dict结构体中其他的成员,只关注其中的哈希表成员:
dictht ht[2];
Redis的字典底层是用hash表来实现的, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对,但是,但是有趣的是这里使用了两个hash表。
为什么使用两个hash表呢?
因为在真实使用场景下,我们需要扩展或者收缩hash表的规模。出于节约内存的考虑,我们不可能一次性分配一个巨大巨大的hash表,因此,只在初始时分配一个合适大小的hash表,在使用过程中,当发现碰撞率过高时,我们就需要扩大hash表的规模。同样是基于节约内存的考虑,当hash表的填充率很低时,我们可以适当的收缩hash表的规模。
使用两个hash表是为了方便进行hash表的扩展和收缩。扩展或收缩哈希表时我们需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面。而且这个将ht[0]里面的键值对rehash到ht[1]不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
这样做的原因在于, 如果 ht[0] 里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1] ; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。
因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次、渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1] 。
以下是哈希表渐进式 rehash 的详细步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
【PS:如果上面的描述不够清楚,可以移步这里: 渐进式rehash】
====================================================
OK,看完的Redis的字典设计及其rehash机制,相信其能给我们带来灵感。现在我们再来说说 缓存系统的更新机制设计问题。
如下是一个可行的解决方案(假设我们的更新机制是每5分钟更新一次缓存):
1)设计两个缓存池,记为A、B,而A和B的内容都是从后端服务器数据库中获取到的数据。正常情况下,客户端的请求都是从缓存池A中获取缓存内容,同时,设置一个全局的变量ref用于记录当前正在访问缓存A的客户端数量,来一个客户端请求将ref值加1,响应完一个客户端请求后ref减一。
2)当缓存更新时间到时,如果ref不为0,则我们不能直接更新缓存,因为这时有客户端正在从缓存池A取数据。这里,我们可以借鉴Redis的rehash思想,更新时间到,我们将客户端的访问都引导到B缓存池,此时的缓存池A不再接受新的客户端数据请求,A的ref变量只减不增,当ref变量减少到0时,我们便可以更新A缓存池中的内容了。
【PS:初始nosql等相关知识,如有错误,欢迎指正!】
参考链接:
http://redisbook.com/en/latest/preview/dict/datastruct.html