分布式K/V存储方案-Redis
2010-11-15 16:11:47 阿炯

Redis 是一个高性能的key/value数据库。它的出现很大程度补偿了Memcached这类K/V存储的不足,在部分场合可以对关系数据库起到很好的补充作用,它提供了Perl、Python、Ruby、Erlang、PHP客户端,使用很方便。从2015年6月开始,Redis的开发由Redis Labs赞助,而2013年5月至2015年6月期间,其开发由Pivotal赞助。在2013年5月之前,其开发由VMware赞助。它使用ANSI C编写开发,支持网络、基于内存、可选持久性的键值对存储数据库,采用BSD协议授权。

A persistent key-value database with built-in net interface written in ANSI-C for Posix systems.

Redis 本质上也是一种键值数据库的,但它在保持键值数据库简单快捷特点的同时,又吸收了部分关系数据库的优点,从而使它的位置处于关系数据库和键值数据库之间。Redis不仅能保存Strings类型的数据,还能保存Lists类型(有序)和Sets类型(无序)的数据,而且还能完成排序(SORT)等高级功能,在实现INCR,SETNX等功能的时候,保证了其操作的原子性,除此以外,还支持主从复制等功能。

Redis is an advanced key-value store. It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. All this data types can be manipulated with atomic operations to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. Redis supports different kind of sorting abilities.

In order to be very fast but at the same time persistent the whole dataset is taken in memory, and from time to time saved on disc asynchronously (semi persistent mode) or alternatively every change is written into an append only file (fully persistent mode). Redis is able to rebuild the append only file in background when it gets too big.

Redis is written in ANSI C and works in most POSIX systems like Linux, *BSD, Mac OS X, Solaris, and so on. Redis is free software released under the very liberal BSD license. Redis is reported to compile and work under WIN32 if compiled with Cygwin, but there is no official support for Windows currently.

Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(Strings),散列(Hash),列表(List),集合(Set),有序集合(Sorted Set或者是ZSet)与范围查询,Bitmaps,Hyperloglogs 和地理空间(Geospatial)索引半径查询。其中常见的数据结构类型有:String、List、Set、Hash、ZSet这5种。

内置了复制(Replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(Transactions) 和不同级别的磁盘持久化(Persistence),并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。同时也提供了持久化的选项,这些选项可以让用户将自己的数据保存到磁盘上面进行存储。根据实际情况,可以每隔一定时间将数据集导出到磁盘(快照),或者追加到命令日志中(AOF只追加文件),他会在执行写命令时,将被执行的写命令复制到硬盘里面。您也可以关闭持久化功能,将Redis作为一个高效的网络的缓存数据功能使用。

Redis不使用表,其数据库不会预定义或者强制去要求用户对Redis存储的不同数据进行关联。数据库的工作模式按存储方式可分为:硬盘数据库和内存数据库。Redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快。

(1)硬盘数据库的工作模式:


(2)内存数据库的工作模式:


在系统中相关的二进制命令简介
redis-server:Redis服务器的daemon启动程序
redis-cli:Redis命令行操作工具,也可以用telnet根据其纯文本协议来操作
redis-sentinel:软链到了redis-server
redis-benchmark:Redis性能测试工具,测试Redis在当前系统下的读写性能
redis-check-aof:rdb数据检查修复工具
redis-check-rdb:to check or repair redis-server rdb files.
redis-check-dump:检查导出工具,4.0后无此指令

使用jemalloc来进行内存的分配:
make MALLOC=jemalloc
make PREFIX=/usr MALLOC=jemalloc

数据模型
Redis的外围由一个键、值映射的字典构成。与其他非关系型数据库主要不同在于:Redis中值的类型不仅限于字符串,还支持如下抽象数据类型:
字符串列表
无序不重复的字符串集合
有序不重复的字符串集合
键、值都为字符串的哈希表

值的类型决定了值本身支持的操作。Redis支持不同无序、有序的列表,无序、有序的集合间的交集、并集等高级服务器端原子操作。

持久化
Redis通常将全部的数据存储在内存中。2.4版本后可配置为使用虚拟内存,一部分数据集存储在硬盘上,但这个特性废弃了。目前通过两种方式实现持久化:
使用快照,一种半持久耐用模式。不时的将数据集以异步方式从内存以RDB格式写入硬盘。
1.1版本开始使用更安全的AOF格式替代,一种只能追加的日志类型,将数据集修改操作记录起来。Redis能够在后台对只可追加的记录作修改来避免無限增长的日志。

同步
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是其它从服务器的主服务器,这使得Redis可执行单层树复制。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。后文会详细对其中的一些方面做了深入讲解。

1.对Sorted Sets的内存优化
实际上在2.2版本中,Redis就对小数据量Value的情况做了性能优化,主要优化方式是将小数据量的Value值不再按具体的数据结构存储,而是存在一块二进制的整块数据。而这一改进一直没能应用于Sorted Sets数据结构上来,而在2.4版本中,作者终于想到合适的办法把Sorted Sets在小数据量下也进行了此种优化。

2.RDB文件持久化提速
这块很大程度上依赖于上面第1点,由于小量数据被存为一个大的二进制数据块,所以在持久化的时候,就不需要再遍历数据了,只需要一个key进行一次持久化写入。

3.提供批量写入功能
下面是所有提供批量写入功能的命令
* SADD set val1 val2 val3 … — 返回添加的元素个数
* HDEL hash field2 field3 field3 … — 返回删除的元素个数
* SREM set val1 val2 val3 … – 返回删除的元素个数
* ZREM zset val1 val2 val3 … – 返回删除的元素个数
* ZADD zset score1 val1 score2 val2 … – 返回添加的元素个数
* LPUSH/RLPUSH list val1 val2 val3 … – 返回操作后的LIST的长度

提供批量命令的效果是显而易见的,网络往返的时间被大大节约了,在最理想的网络情况下,作者的测试结果是一次性写入200w个元素,仅仅花费了1.28秒,每秒超过100w元素的写入!为何不为所有写入命令都加上批量功能呢?作者解释说,由于很多命令在返回值上需要携带信息,如果改成批量的,无法批量返回信息内容。不过相信上面的改进已经可以让很多应用场景得到大大改进了。

4.改用jemalloc的内存分配模式
Redis长期以来的思想就是尽量不产生外部依赖,比如网络事件库没有用传统的libevent库,而是自己单独抽离出几个文件组成的更简单且性能更高的网络事件启动库,这一库目前在很多开源项目中也被采用。而此次引入jemalloc实在是由于作者认为Linux下的glibc的内存分配器的表现实在是太差了,无法有效地防止碎片的产生。虽然jemalloc是外部引入,你也不需要在安装Redis时先安装一堆东西,因为它已经包含在Redis源码里了,还是像往常一样直接Make编译即可。

5.减少 copy-on-write 使用
Redis的RDB文件持久化和AOF日志写入,都是通过调用fork()方法产生子进程来做的。由于主进程还是继续处理请求,当有数据写操作导致数据内容发生变化时,原来的内存段会被复制一份,这就是我们熟知的copy-on-write机制。而采用这一机制的问题就是,在最坏的情况下,进行一次RDB文件写入,可能导致使用内存加倍。所以在2.4版本中,作者对这一机制的使用进行了优化,大大减少了对copy-on-write的使用。

作者还坦言,自己在2.2版本中在这方面的一些修改是有问题的,这导致了2.2版本中的许多Bug。

6.INFO输出内容增强
2.4版本的INFO内容会有较大改变,其中比较重要的有下面两个
used_memory_peak:185680824
used_memory_peak_human:177.08M

你的实际物理内存使用(RSS)和内存碎片情况通常都与最高峰内存使用相关,而这参数就是用来描述这些情况。一个是以byte为单位(185680824),一个是自动智能单位(177.08M)。

7.测试框架的优化和提速

Redis为什么这么快

1.完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2.数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4.使用多路I/O复用模型,非阻塞IO;

5.使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

以上几点都比较好理解,下边针对多路 I/O 复用模型进行简单的探讨:

多路 I/O 复用模型

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

为什么Redis是单线程的

首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦)。

1.Redis是用”单线程-多路复用IO模型”来实现高性能的内存数据服务的,这种机制避免了使用锁,但是同时这种机制在进行sunion之类的比较耗时的命令时会使redis的并发下降。因为是单一线程,所以同一时刻只有一个操作在进行,所以,耗时的命令会导致并发的下降,不只是读并发,写并发也会下降。而单一线程也只能用到一个CPU核心,所以可以在同一个多核的服务器中,可以启动多个实例,组成master-master或者master-slave的形式,耗时的读命令可以完全在slave进行。

2.“不能任由操作系统负载均衡,因为我们更了解自己的程序,所以可以手动地为其分配CPU核,而不会过多地占用CPU,或是让关键进程和一堆别的进程挤在一起”。
 
CPU 是一个重要的影响因素,由于是单线程模型,Redis 更喜欢大缓存快速 CPU,而不是多核。在多核 CPU 服务器上面,Redis 的性能还依赖NUMA 配置和处理器绑定位置。最明显的影响是 redis-benchmark 会随机使用CPU内核。为了获得精准的结果,需要使用固定处理器工具(在 Linux 上可以使用 taskset)。最有效的办法是将客户端和服务端分离到两个不同的 CPU 来高效使用三级缓存。


Redis 体系架构速览

Redis(Remote Dictionary Server)是一个开源的内存数据存储系统,其在缓存、分布式会话存储、消息队列、实时计算、地理位置应用和分布式锁等方面都展现出了强大的适用性,可谓是开发人员再熟悉不过的中间件了,也是面试中的大户。本节在上文的基础上,从数据类型、线程模型、持久化、过期策略、雪崩&穿透、高可用等多个维度做了整理。



数据类型

Redis是一款高性能的键值存储数据库,支持多种数据类型。以下是 Redis 10大数据类型:

String(字符串)

String是 Redis 最基本的数据类型,可以存储字符串、整数或浮点数。其支持的操作包括设置值、获取值、增减操作等。

Hash(哈希)

Hash类型是一种键值对集合,其中每个键都对应一个值。Hash可以存储多个域和域值,支持的操作包括设置值、获取值、删除域、获取所有域等。

List(列表)

List类型是一种有序的字符串列表,可以存储多个字符串,支持的操作包括从列表左侧或右侧插入和删除元素、获取列表长度等。

Set(集合)

Set类型是一种无序的字符串集合,其中每个元素都是唯一的。支持的操作包括添加元素、获取集合中的所有元素、计算集合的交集、并集、差集等。

Sorted Set(有序集合)

Sorted Set类型是一种有序的字符串集合,其中每个元素都有一个分数(score)值。支持的操作包括添加元素、获取有序集合中的元素、计算有序集合的交集、并集、差集等。

Bitmaps(位图)

Bitmaps类型是一种位数组,其中每个二进制位代表一个布尔值。支持的操作包括设置位、获取位、进行位运算等。

HyperLogLog(基数)

HyperLogLog类型是一种基数算法,用于估计一个集合中不同元素的数量。支持的操作包括添加元素、获取基数值等。

Geospatial(地理位置)

Geospatial类型是一种地理位置数据类型,用于存储地理位置信息和坐标。支持的操作包括添加位置信息、获取位置信息、计算位置之间的距离等。

Streams(流)

Streams类型是一种持久化的消息队列,用于存储和处理消息。支持的操作包括添加和获取消息、消费消息、获取消息的长度等。

Modules(模块)

Redis支持动态加载模块,可以通过加载模块扩展 Redis 的功能,如添加新的数据类型、命令等。常见的 Redis 模块包括 RedisBloom、RedisTimeSeries、RedisJSON等。


底层数据结构


线程模型

(1)Redis 是单线程吗?

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。但Redis程序并不是单线程的,它在启动的时候是会启动后台线程(BIO)的:
1.在 2.6 版本,会启动两个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;

2.在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。

(2)Redis 单线程模式是怎样的?

Redis 6.0 版本之前的单线模式如下图:


蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。Redis 初始化的时候会做下面这几件事情:
1.调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket;

2.调用 bind() 绑定端口和调用 listen() 监听该 Socket;

3.将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。

初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:
1.先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

2.接着调用 epoll_wait 函数等待事件的到来:如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

(3)Redis 采用单线程为什么还这么快?

单线程的 Redis 吞吐量可以达到 10W/每秒,其采用单线程(网络 I/O 和执行命令),为什么有这么快呢?有如下几个原因:
1.Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;

2.Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

3.Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

(4)Redis 6.0 之后为什么引入了多线程?

虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。所以为了提高网络 I/O 的并行度,6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。

Redis 6.0 版本之后,其在启动的时候,默认情况下会额外创建 6 个线程(不包括主线程):
Redis-server:Redis的主线程,主要负责执行命令;
bio_close_File、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。


持久化

其持久化机制有两种,第一种是RDB快照,第二种是AOF日志。

(1)RDB机制

RDB ( Redis Data Base) 指的是在指定的时间间隔内将内存中的数据集快照写入磁盘,RDB 是内存快照(内存数据的二进制序列化形式)的方式持久化,每次都是从 Redis 中生成一个快照进行数据的全量备份。


优点:
存储紧凑,节省内存空间;恢复速度非常快。
适合全量备份、全量复制的场景,经常用于灾难恢复(对数据的完整性和一致性要求相对较低的场合)。

缺点:
容易丢失数据,容易丢失两次快照之间 Redis 服务器中变化的数据。
RDB 通过 fork 子进程对内存快照进行全量备份,是一个重量级操作,频繁执行成本高。

RDB备份过程:
RDB 持久化方案进行备份时,Redis 会单独 fork 一个子进程来进行持久化,会将数据写入一个临时文件中,持久化完成后替换旧的 RDB 文件。在整个持久化过程中,主进程(为客户端提供服务的进程)不参与 IO 操作,这样能确保 Redis 服务的高性能,RDB 持久化机制适合对数据完整性要求不高但追求高效恢复的使用场景。下面展示 RDB 持久化流程:


Redis 父进程首先判断:当前是否在执行 save,或 bgsave/bgrewriteaof 的子进程,如果在执行则 bgsave 命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
父进程执行 fork 操作创建子进程,这个过程中父进程是阻塞的,Redis 不能执行来自客户端的任何命令。父进程 fork 后,bgsave 命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令。
子进程进程对内存数据生成快照文件。
父进程在此期间接收的新的写操作,使用 COW 机制写入。
子进程完成快照写入,替换旧 RDB 文件,随后子进程退出。

(2)AOF机制

AOF (Append Only File) 是把所有对内存进行修改的指令(写操作)以独立日志文件的方式进行记录,重启时通过执行 AOF 文件中的 Redis 命令来恢复数据。类似MySql bin-log 原理。AOF 能够解决数据持久化实时性问题,是现在 Redis 持久化机制中主流的持久化方案。

优点:
数据的备份更加完整,丢失数据的概率更低,适合对数据完整性要求高的场景
日志文件可读,AOF 可操作性更强,可通过操作日志文件进行修复

缺点:
AOF 日志记录在长期运行中逐渐庞大,恢复起来非常耗时,需要定期对 AOF 日志进行瘦身处理
恢复备份速度比较慢;同步写操作频繁会带来性能压力

AOF 持久化方案进行备份时,客户端所有请求的写命令都会被追加到 AOF 缓冲区中,缓冲区中的数据会根据 Redis 配置文件中配置的同步策略来同步到磁盘上的 AOF 文件中,追加保存每次写的操作到文件末尾。同时当 AOF 的文件达到重写策略配置的阈值时,Redis 会对 AOF 日志文件进行重写,给 AOF 日志文件瘦身。Redis 服务重启的时候,通过加载 AOF 日志文件来恢复数据。



(3)Redis4.0的混合持久化

仅使用RDB快照方式恢复数据,由于快照时间粒度较大时,会丢失大量数据。

仅使用AOF重放方式恢复数据,日志性能相对 rdb 来说要慢。在 Redis 实例很大的情况下,启动需要花费很长的时间。

为了解决这个问题,Redis4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。RDB 文件的内容和增量的 AOF 日志文件存在一起,这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小:
1.大量数据使用粗粒度(时间上)的RDB快照方式,性能高,恢复时间快。
2.增量数据使用细粒度(时间上)的AOF日志方式,尽量保证数据的不丢失。

更多详细信息可参考《redis持久化方案rdb和aof》、《理解Redis持久化与事务》。


高可用

要想设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式、切片集群。

(1)主从复制

主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。



也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。

主从服务器之间的命令复制是异步进行的。具体来说,在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。

(2)哨兵模式

在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。


为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。

(3)切片集群模式

当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis cluster)方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步:

根据键值对的 key,按照 CRC16 算法 (opens new window)计算一个 16 bit 的值;再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。

接下来的问题就是,这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:
1.平均分配:在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个;

2.手动分配:可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。

为了方便理解,通过一张图来解释数据、哈希槽,以及节点三者的映射分布关系。


上图中的切片集群一共有 2 个节点,假设有 4 个哈希槽(Slot 0~Slot 3)时,我们就可以通过命令手动分配哈希槽,比如节点 1 保存哈希槽 0 和 1,节点 2 保存哈希槽 2 和 3。

redis-cli -h 192.168.1.1 –p 6379 cluster addslots 0,1 redis-cli -h 192.168.1.2 –p 6379 cluster addslots 2,3

然后在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 4 进行取模,再根据各自的模数结果,就可以被映射到哈希槽 1(对应节点1) 和 哈希槽 2(对应节点2)。

需要注意的是,在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。


过期删除与内存淘汰

(1)Redis的过期策略

Redis是key-value数据库,在程序中可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了以后,Redis是如何处理的。过期策略通常有以下三种:
1.定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

2.惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

3.定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis同时使用了惰性过期和定期过期两种过期策略。但是Redis定期删除是随机抽取机制,不可能扫描删除掉所有的过期Key。因此需要内存淘汰机制。

(2)Redis的内存淘汰策略

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

(3)过期键删除策略和内存淘汰机制之间的关系

过期健删除策略强调的是对过期健的操作,如果有健过期了,而内存还足够,不会使用内存淘汰机制,这时也会使用过期健删除策略删除过期健。
内存淘汰机制强调的是对内存的操作,如果内存不够了,即使有的健没有过期,也要删除一部分,同时也针对没有设置过期时间的健。


Redis版本发行计划


官方主页:http://redis.io/
该文章最后由 阿炯 于 2023-11-10 15:50:53 更新,目前是第 4 版。