数据结构(字典,跳跃表)、使用场景(计数器、缓存、查找表、消息队列、会话缓存、分布式锁)、Redis 与 Memcached、 键的过期时间、数据淘汰策略、持久化(RDB、AOF)

1. 数据结构

1.1 字典

dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry

/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table.*/

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。

  • 在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面
  • 完成之后释放空间并交换两个 dictht 的角色。
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx ==-1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的rehash 操作给服务器带来过大的负担。

渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次rehash 都会递增。

  • 例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],
  • 这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,
  • dict[0] 的table[rehashidx] 指向 null,并令 rehashidx++。

在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。

采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的操作也需要到对应的 dictht 去执行。

/* Performs N steps of incremental rehashing. Returns 1 if thereare still
* keys to move from the old to the new hash table, otherwise 0is returned.
* *Note that a rehashing step consists in moving a bucket (thatmay have more
* than one key as we use chaining) from the old to the new hashtable, however
* since part of the hash table may be composed of empty spaces,it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise theamount of
* work it does would be unbound and the function may block fora long time. */

int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; /* Max number of empty buckets tovisit. */
    if (!dictIsRehashing(d)) return 0;
        while (n-- && d->ht[0].used != 0) {
            dictEntry *de, *nextde;
            /* Note that rehashidx can't overflow as we are sure the re are more
            * elements because ht[0].used != 0 */
            assert(d->ht[0].size > (unsigned long) d->rehashidx);
            while (d->ht[0].table[d->rehashidx] == NULL) {
                d->rehashidx++;
                if (--empty_visits == 0) 
                    return 1;
            } 
            de = d->ht[0].table[d->rehashidx];
            /* Move all the keys in this bucket from the old to the new hash HT */
            while (de) {
                uint64_t h;
                nextde = de->next;
                /* Get the index in the new hash table */
                h = dictHashKey(d, de->key) & d->ht[1].sizemask;
                de->next = d->ht[1].table[h];
                d->ht[1].table[h] = de;
                d->ht[0].used--;
                d->ht[1].used++;
                de = nextde;
            } 
            d->ht[0].table[d->rehashidx] = NULL;
            d->rehashidx++;
        } 
        /* Check if we already rehashed the whole table... */
        if (d->ht[0].used == 0) {
            zfree(d->ht[0].table);
            d->ht[0] = d->ht[1];
            _dictReset(&d->ht[1]);
            d->rehashidx = -1;
            return 0;
        } 
        /* More to rehash... */
        return 1;
    }

1.2 跳跃表

是有序集合的底层实现之一。

跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。

在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。

与红黑树等平衡树相比,跳跃表具有以下优点:

  • 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
  • 更容易实现;
  • 支持无锁操作。

2. 使用场景

2.1 计数器

可以对 String 进行自增自减运算,从而实现计数器功能。

Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

2.2 缓存

热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

2.3 查找表

例如 DNS 记录就很适合使用 Redis 进行存储。

查找表和缓存类似,也是利用了 Redis 快速的查找特性

但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

2.4 消息队列

List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息。

不过最好使用 Kafka、RabbitMQ 等消息中间件。

2.5 会话缓存

在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。

当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。

2.7 分布式锁实现

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。

可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

2.8 其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。

ZSet 可以实现有序性操作,从而实现排行榜等功能。

3. Redis 与 Memcached

两者都是非关系型内存键值数据库,主要有以下不同:

  Redis Memcached
数据类型 支持五种不同的数据类型,可以更灵活地解决问题。 仅支持字符串类型
数据持久化 两种持久化策略:RDB 快照和 AOF 日志 不支持
分布式 Redis Cluster 实现了分布式的支持 不支持(只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点)
内存管理机制 并不是所有数据都一直存储在内存中,可以将一些很久没用的value 交换到磁盘

数据会一直在内存中;

将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高(例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了)

4. 键的过期时间

Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。

对于散列表这种容器,只能为整个键设置过期时间(整个散列表) ,而不能为键里面的单个元素设置过期时间。

5. 数据淘汰策略

可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略

Reids 具体有 6 种淘汰策略:

策略 描述
volatile-lru 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru 从所有数据集中挑选最近最少使用的数据淘汰
allkeys-random 从所有数据集中任意选择数据进行淘汰
noeviction 禁止驱逐数据


作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。

使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据

可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。

Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。

6. 持久化

Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。

6.1 RDB 持久化

将某个时间点的所有数据都存放到硬盘上。

可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。

如果系统发生故障,将会丢失最后一次创建快照之后的数据。

如果数据量很大,保存快照的时间会很长。

6.2 AOF 持久化

写命令添加到 AOF 文件(Append Only File) 的末尾。

使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上

这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:

选项 同步频率
always 每个写命令都同步
everysec 每秒同步一次
no 让操作系统来决定何时同步
  • always 选项会严重减低服务器的性能;
  • everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且
  • Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
  • no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。

随着服务器写请求的增多,AOF 文件会越来越大。

Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令

猜你喜欢

转载自blog.csdn.net/Zhxin606a/article/details/89577493