第三章:Redis

1.Redis VS Memcached

  • 适用于集中式缓存
常见技术选型 基于内存 数据结构 虚拟内存 过期策略 数据持久 灾难恢复 性能
Redis 是的 List,Set等多种 物理内存用完,可以将不用的数据交换到磁盘
Memcached 是的 k/v
  • Memcached只适合做基于内存的缓存,但对数据的持久和容灾所提供的能力不足。
  • 分布式:Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。Redis Cluster 实现了分布式的支持。
  • 内存管理机制:
    在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。
    Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。

2.简述

可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串列表集合散列表有序集合

Redis 支持很多特性

  • 将内存中的数据持久化到硬盘中;
  • 使用复制来扩展读性能
  • 使用分片来扩展写性能。

3.数据类型

这块应该不会考,但是也要了解一下。

数据类型 可以存储的值 操作
STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作、对整数和浮点数执行自增或者自减操作
LIST 列表 从两端压入或者弹出元素、对单个或者多个元素进行修剪,只保留一个范围内的元素
SET 无序集合 添加、获取、移除单个元素、检查一个元素是否存在于集合中、计算交集、并集、差集、从集合里面随机获取元素
字典(HASH) 包含键值对的无序散列表 添加、获取、移除单个键值对、获取所有键值对、检查某个键是否存在
ZSET 有序集合 添加、获取、删除元素、根据分值范围或者成员来获取元素、计算一个键的排名

4.数据结构

4.1 String

字符串对应的数据结构就是字符串。

4.2 列表

对应两种数据类型:

  • 压缩列表(ziplist)
  • 双向循环链表

4.2.1 压缩列表

数据量小的时候,采用压缩列表实现,需要满足以下两个条件:

  • 列表中保存的单个数据小于64字节;
  • 列表中的数据个数少于512个;

压缩列表不支持随机访问。有点类似链表。但是比较省存储空间啊。Redis一般都是通过key获取整个value的值,也就是整个压缩列表的数据,并不需要随机访问。

结构特性

  • 不是基础数据结构,redis自己设计的一种数据结构;
  • 类似数组,连续的内存空间;
  • 允许存储的数据大小不同,从而可以节省内存;
  • 支持不同的数据类型存储;


    第三章:Redis_第1张图片
    image.png

4.3 字典(hash)

字典类型也有两种实现方式。一种是我们刚刚讲到的压缩列表,另一种是散列表。

压缩列表

只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。
具体需要满足两个条件:

  • 字典中保存的键和值的大小都要小于 64 字节;
  • 字典中键值对的个数要小于 512 个

散列表

  • 对于哈希冲突,使用链表法来解决;
  • 支持动态扩容:为了避免散列表性能的下降,当装载因子大于 1 的时候,Redis 会触发扩容,将散列表扩大为原来大小的 2 倍左右;
  • 支持动态缩容:当数据动态减少之后,为了节省内存,当装载因子小于 0.1 的时候,Redis 就会触发缩容,缩小为字典中数据个数的大约 2 倍大小;
  • 渐进式扩容缩容策略,将数据的搬移分批进行,避免了大量数据一次性搬移导致的服务停顿。

4.4 集合

集合这种数据类型用来存储一组不重复的数据。这种数据类型也有两种实现方法,
一种是基于有序数组,另一种是基于散列表。

4.4.1有序数组

当要存储的数据,同时满足下面这样两个条件的时候,Redis 就采用有序数组,来实现集合这种数据类型。

  • 存储的数据都是整数;
  • 存储的数据元素个数不超过 512 个。

4.4.2 散列表

当不能同时满足这两个条件的时候,Redis 就使用散列表来存储集合中的数据。

4.5 有序集合

用来存储一组数据,并且每个数据会附带一个得分。通过得分的大小,我们将数据组织成跳表这样的数据结构,以支持快速地按照得分值、得分区间获取数据。

压缩列表

具体点说就是,使用压缩列表来实现有序集合的前提,有这样两个:

  • 所有数据的大小都要小于 64 字节;
  • 元素个数要小于 128 个。

跳表

1. 跳表定义

引:把链表稍加改造,就可以支持类似"二分"的查找算法,这种数据结构叫做跳表。
跳表==链表+多级索引,链表增加一个down指针。
跳表是一种各方面性能都比较优秀的动态数据结构,可以支持快速的插入、删除、查找操作,写起来也不复杂,甚至可以替代红黑树 。
Redis 中的有序集合(Sorted Set)就是用跳表来实现的。

2.实现

简单来说,就三个字加索引
加一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。当元素个数n比较大的时候,查询效率会非常显著。

第三章:Redis_第2张图片
image.png

第三章:Redis_第3张图片
image.png

3.分析

3.1 查询效率

时间复杂度O(lgn)
谨记空间换时间的思想。

第三章:Redis_第4张图片
image.png

3.2 内存分析

当索引是以2为分割单位时,占用的内存是n-2,整体空间复杂度是O(n)
当索引是以3为分割单位时,占用的内存是n/2,整体空间复杂度还是O(n)


第三章:Redis_第5张图片
image.png

实际上,在软件开发中,我们不必太在意索引占用的额外空间。在讲数据结构和算法时,我们习惯性地把要处理的数据看成整数,但是在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。

3.3 插入和删除分析
  • 插入和删除的时间复杂度均为O(lgn)


    第三章:Redis_第6张图片
    image.png

    删除稍微有点麻烦:除了要删除链表中的元素,还要删除索引中的元素,所以需要记录其前驱结点。

3.4 跳表索引动态更新

问题:当跳表一直在插入,但是却没有更新索引的话,有可能退化为单链表。

第三章:Redis_第7张图片
image.png

作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。
跳表是通过 随机函数来维护前面提到的“平衡性”。

当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
随机函数的选择很有讲究,从概率上来讲,能够保证跳表的索引大小和数据大小平衡性,不至于性能过度退化

第三章:Redis_第8张图片
image.png

4.与红黑树的对比

Redis 中的有序集合支持的核心操作主要有下面这几个:

  • 插入一个数据;
  • 删除一个数据;
  • 查找一个数据;
  • 按照区间查找数据(比如查找值在[100, 356]之间的数据);
  • 迭代输出有序序列。

插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。

5.使用场景

5.1 计数器

对String进行自增和自减操作,从而实现计数器的功能;
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数器。

5.2 缓存

将热点数据放在缓存中,注意两个点:

  • 缓存过期策略来保证缓存的命中率;
  • 内存的最大使用量;

5.3 查找表

例如 DNS 记录就很适合使用 Redis 进行存储。
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因此缓存不作为可靠的数据来源。

5.4 双向链表

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

5.5 会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

5.6 分布式锁

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

5.7 其他

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

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

6.持久化

6.1 RDB

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

  • 将某个时间点的所有数据都存放到硬盘上。
  • 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
  • 如果系统发生故障,将会丢失最后一次创建快照之后的数据。
  • 如果数据量很大,保存快照的时间会很长。

6.2 AOF

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
三种同步选项:

  • always 每个写命令都同步
  • everysec 每秒同步一次
  • no 让操作系统来决定何时同步
    随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。

7.分布式锁

实际上Redis服务本身并不提供分布式锁这样的机制,但是作为全局Key-Value存储系统,客户端可以利用Redis提供的基本功能并通过一定的算法设计来实现分布式锁功能。

7.1 redlock算法

第三章:Redis_第9张图片
image.png

以上就是实现Redis分布式锁官方推荐的RedLock算法逻辑,它是一种多节点Redis的分布式锁算法,可以有效防止单节点故障问题。其执行步骤说明如下:

  • 首先Redis客户端获取当前系统时间,以毫秒为单位;
  • 然后客户端会顺序地尝试向Redis集群中的每个节点获取锁,其具体步骤是使用相同的键Key名和随机值;在向每个Redis节点获取锁的过程中,客户端会以比锁过期时间小得多的时间来设定超时机制,例如锁的整个超时时间为10秒,集群有5个节点,那么每个节点获取锁的超时时间可能会被限制在5~50毫秒之间,这是为了防止在某个节点不可用的情况下,客户端等待时间过长,造成性能阻塞;
  • 之后随着各节点获取锁结果的反馈,Redis客户端会对获取情况进行判断,如果获取各节点锁的总时间小于锁的超时时间设置,并且成功获取锁的节点数目大于N/2+1个(例如5个节点至少要有3个节点成功获取锁),满足上述条件的情况下,Redis客户端才会认为获取锁成功,否则就会认为锁获取失败,并依次释放掉各个节点的锁信息;
  • 获取锁成功后即可以安全地执行操作,完成后再依次释放各节点锁持有的锁信息;

参考文档

  • https://cyc2018.github.io/CS-Notes/#/notes/Redis?id=%e4%ba%8c%e3%80%81%e6%95%b0%e6%8d%ae%e7%b1%bb%e5%9e%8b
  • https://time.geekbang.org/column/article/79159
    ‘】

你可能感兴趣的:(第三章:Redis)