您的位置:首页 > 数据库 > Redis

如何实现缓存系统的更新机制

2014-10-20 09:56 543 查看
问题是这样的:假设要你设计一个缓存系统,你如何实现缓存的更新机制!

例如某网站具有高并发访问量,为提高性能,其中的一些网页页面都是存放在缓存中的,当需要更新缓存中的页面时,缓存系统如何完成缓存的更新?

给出解决方案之前,先看看内存数据库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;

1
2
3
4
5
6
7
8
9
10
11

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];

1

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缓存池中的内容了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  缓存系统 redis 内存