Redis基础知识点快速复习手册(上)

2019-08-07 10:25:02 浏览数 (1)

前言

本文快速回顾了Redis书籍、博客以及本人面试中遇到的基础知识点,方便大家快速回顾知识。

用作面试复习,事半功倍。

分为上下篇,上篇主要内容为:

基础概述数据类型数据结构字典跳跃表使用场景会话缓存缓存计数器查找表消息队列分布式 Session分布式锁其它Redis 与 Memcached 对比数据类型数据持久化单线程分布式内存管理机制键的过期时间数据淘汰策略

面试知识点复习手册

全复习手册文章导航:

点击公众号下方技术推文——面试冲刺

已发布知识点复习手册

Java基础知识点面试手册(上)

Java基础知识点面试手册(下)

Java容器(List、Set、Map)知识点快速复习手册(上)

Java容器(List、Set、Map)知识点快速复习手册(中)

Java容器(List、Set、Map)知识点快速复习手册(下)

基础

概述

Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。

  • 键的类型只能为字符串
  • 值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。

Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。

数据类型

数据类型

可以存储的值

操作

STRING

字符串、整数或者浮点数

对整个字符串或者字符串的其中一部分执行操作对整数和浮点数执行自增或者自减操作

LIST

列表

从两端压入或者弹出元素读取单个或者多个元素进行修剪,只保留一个范围内的元素

SET

无序集合

添加、获取、移除单个元素检查一个元素是否存在于集合中计算交集、并集、差集从集合里面随机获取元素

HASH

包含键值对的无序散列表

添加、获取、移除单个键值对获取所有键值对检查某个键是否存在

ZSET

有序集合

添加、获取、删除元素根据分值范围或者成员来获取元素计算一个键的排名

STRING

image.png

代码语言:javascript复制
> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)

LIST

image.png

代码语言:javascript复制
> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3

> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"

> lindex list-key 1
"item2"

> lpop list-key
"item"

> lrange list-key 0 -1
1) "item2"
2) "item"

SET

image.png

代码语言:javascript复制
> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0

> smembers set-key
1) "item"
2) "item2"
3) "item3"

> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1

> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0

> smembers set-key
1) "item"
2) "item3"

HASH

image.png

代码语言:javascript复制
> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0

> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"

> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0

> hget hash-key sub-key1
"value1"

> hgetall hash-key
1) "sub-key1"
2) "value1"

ZSET(SORTEDSET)

image.png

代码语言:javascript复制
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0

> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"

> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"

> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0

> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"

zset是set的一个升级版本,他在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset会自动重新按新的值调整顺序。 可以对指定键的值进行排序权重的设定,它应用排名模块比较多。

跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一

另外还可以用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score的倒序来获取工作任务,让重要的任务优先执行。

数据结构

字典

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

代码语言:javascript复制
/* 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;
代码语言:javascript复制
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 的角色。

代码语言:javascript复制
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 去执行。

代码语言:javascript复制
/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, 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 the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; /* Max number of empty buckets to visit. */
    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 there 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;
}

跳跃表

什么是跳跃表?(程序员小灰)http://blog.jobbole.com/111731/

https://blog.csdn.net/qq910894904/article/details/37883953

来看看跳跃表的复杂度分析:

  • 空间复杂度: O(n) (期望)
  • 跳跃表高度: O(logn) (期望)

相关操作的时间复杂度:

  • 查找: O(logn) (期望)
  • 插入: O(logn) (期望)
  • 删除: O(logn) (期望)

其效率可比拟于二叉查找树(对于大于数操作需要O(log n)平均时间),并且不需要像二叉树一样过段时间重新平衡。

它是按层建造的。底层是一个普通的有序链表。每个更高层都充当下面列表的“快速跑道”,这里在层i中的元素按概率l/p出现在层i 1中。

平均起来,每个元素都在p/(p-1)个列表中出现,而最高层的元素(通常是在跳跃列表前段的一个特殊的头元素)在O(logp n)个列表中出现。

调节p的大小可以在内存消耗和时间消耗上进行折中。

image.png

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

image.png

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

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

使用场景

会话缓存

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

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

缓存

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

计数器

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

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

查找表

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

查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

消息队列

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

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

分布式 Session

多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。

分布式锁

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

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

其它

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

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

Redis 与 Memcached 对比

image.png

数据类型

Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。

数据持久化

Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。

单线程

https://www.cnblogs.com/syyong/p/6231326.html

Redis快的主要原因是:

  • 完全基于内存
  • 数据结构简单,对数据操作也简单
  • 使用多路 I/O 复用模型
  • 单进程单线程好处
    • 代码更清晰,处理逻辑更简单
    • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
    • 不存在多进程或者多线程导致的切换而消耗CPU
  • 单进程单线程弊端
    • 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
  • 其他一些优秀的开源软件采用的模型
    • 多进程单线程模型:Nginx
    • 单进程多线程模型:Memcached

image.png

分布式

Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点

Redis Cluster 实现了分布式的支持。采用虚拟槽。(为何不需要计算了?不懂)

内存管理机制

在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而Memcached 的数据则会一直在内存中。

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

键的过期时间

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

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

数据淘汰策略

可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 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 策略通过统计访问频率,将访问频率最少的键值对淘汰。

参考与拓展阅读

  • https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Redis.md
  • 【3y】从零单排学Redis【青铜】

关注我

我是蛮三刀把刀,目前为后台开发工程师。主要关注后台开发,网络安全,Python爬虫等技术。

来微信和我聊聊:yangzd1102

Github:https://github.com/qqxx6661

原创博客主要内容

  • 笔试面试复习知识点手册
  • Leetcode算法题解析(前150题)
  • 剑指offer算法题解析
  • Python爬虫相关实战
  • 后台开发相关实战

同步更新以下博客

1. Csdn

http://blog.csdn.net/qqxx6661

拥有专栏:Leetcode题解(Java/Python)、Python爬虫开发

2. 知乎

https://www.zhihu.com/people/yang-zhen-dong-1/

拥有专栏:码农面试助攻手册

3. 掘金

https://juejin.im/user/5b48015ce51d45191462ba55

4. 简书

https://www.jianshu.com/u/b5f225ca2376

个人公众号:Rude3Knife

如果文章对你有帮助,不妨收藏起来并转发给您的朋友们~

0 人点赞