Post

Redis知识合集

Redis知识合集

title: redis知识合集 date: 2025-07-28 20:51:07 tags: [数据库]

Redis中的key值非常大会造成什么影响?

  1. 对持久化的影响(AOF、RDB): 当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。

AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。
  1. 其他影响:
  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多。

如何解决Redis中key值非常大的问题?

  • 在设计阶段把key值设计成小key值
  • 定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。

Redis 数据类型

redis数据类型指的是value的类型,也就是key存储的类型。redis的数据类型有:string类型,hash类型,list类型,set类型和zset类型,一共五种基本类型,使用起来比较简单。

string

string字符串,是redis中最基本,使用频率最多的数据结构。可以用来存储字符串、整数、浮点数、图片、序列化后的对象;

操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SET key value
GET key
DEL key
EXISTS key
STRLEN key
SETNX key value

INCR key
DECR key

MSET key value key value
MGET key1 key2

EXPIRE key
SETEX key seconds value

list

底层为双向链表,支持双向链表相关的所有操作

1
2
3
4
5
6
7
LPUSH key value
RPUSH key value
LPOP key
RPOP key
LRANGE key start stop
LLEN key
LSET key index value

应用:缓存消息,信息流展示

hash 基于string类型的键值对集合,适合存储对象

1
2
3
4
5
6
7
HSET key field value
HGET key field
HDEL key field
HGETALL key
HKEYS key
HVALS key
HINCRBY key field increment

应用:存储用户信息、商品信息等复杂对象,模拟对象储存

set

1
2
3
4
5
6
7
8
9
10
SADD key member1 merber2
SREM key member
SMEMBERS key // 获取所有元素
SISMEMBER key member
SCARD key

// 集合操作
SINTER key1 key2 // 交集
SUNION key1 key2 // 并集
SDIFF key1 key2 // 差集

sorted set 有序集合,集合内的元素是有序的,每个元素都会关联一个分数(score),根据分数进行排序。

1
2
3
4
5
6
7
8
9
ZADD key score member
ZREM key member
ZRANGE key start stop
ZREVRANGE key start stop
ZRANGEBYSCORE key min max
ZCARD key

ZCOUNT key min max
ZREVRANK key member

Bitmap位图

位图的作用常写算法的同学应该了解,用于标识状态,将各个状态压缩进位图中。

SETBIT key index 0/1
GETBIT key index

index用于标识状态,0/1是状态的布尔值。

应用:记录用户登录状态等

HyperLog

概率性数据结构,用于估算大规模的基数,适合大规模的去重和计数

PFADD key v1 v2 v3...
PFCOUNT key 

应用:网页的CV计数(访问量、点击量等

GEO

Stream

Redis持久化

大部分数据的持久化策略有两个:日志持久化和快照持久化,redis中也是如此。redis的日志持久化通过aof实现,快照持久化通过rdb实现。

AOF

在了解redis aof持久化机制前,先知道redis怎么开启aof。在redis的conf文件中,将appendonly 设置为 yes,并自定义你自己的appendfilename作为aof日志文件保存的地方。

打开后,服务端每对数据进行操作,都会将操作记录在aof文件中,当服务端重启时,会使用aof进行重放操作,将状态恢复到内存中。aof只会记录写操作,日志的生成遵循RESP协议,使用一个数组来记录aof,定义如下:

1
2
3
4
5
6
*<参数数量>\r\n
$<第一个参数的字节数>\r\n
<第一个参数内容>\r\n
$<第二个参数的字节数>\r\n
<第二个参数内容>\r\n
...

接下来讲讲aof日志的写入过程,执行请求时,到来的日志会被写入到aof文件中。所有涉及写文件的机制,都需要缓存,aof文件也一样。通过conf配置中的appendfsync来决定文件写入策略。

  • always:每次请求将日志写入page cache直接调用fsync,这样请求是安全的,但是效率低;
  • everysec:每秒调用一次fsync,会丢失1秒的数据
  • no:只写入page cache,由操作系统负责。

aof写入细节

aof的写入其实并不像上面所说那么简单,aof写入流程比较复杂。

  • 第一步:将请求根据RESP协议序列化成sds格式字符串,然后写入aof缓存中;
  • 第二部:将aof缓存通过调用write刷入系统缓存中等待刷盘。调用write时机有四个:处理完事件后,等待下一次事件到来前;周期函数serverCron,定时触发;服务器退出前;通过配置指令关闭aof功能
  • 第三步:根据appendsync配置,决定刷盘时机。用户态可以决定系统何时将系统缓存刷入硬盘中,方法是调用fsync方法

aof重写

每个指令都写一次aof日志,我们的aof日志文件太大时会怎样?这时会触发aof文件重写,压缩aof文件大小。对于一个key的多次修改可以只保留最后一次修改,整理aof文件的过程就是aof重写。同样我们需要知道aof文件重写的时机,以及如何重写的。

当aof文件超过配置的阈值时,触发重写程序。重写程序会fork一个子进程负责执行,重写期间aof日志会保存在aof缓存和aof重写缓存中。为什么会有两个缓存呢?首先原有的aof缓存保证旧的aof机制正常工作,防止重写期间服务崩溃不会影响数据;然后需要新的aof重写缓存,重写期间的aof日志缓存起来,在aof重写完成后,将aof重写缓存中的日志添加到新的aof文件的后面。

RDB

同样是先从配置文件上看,开启rdb快照缓存的配置信息:

1
save interval limit

每隔interval秒有limit条新数据,就触发rdbsave;

1
dbfilename dump.rdb

这是配置rdb文件名字的地方;

深入理解rdb

首先要知道rdb快照何时生成?一共有三个时机:

  • 主动执行:save 和 bgsave 区别是是否开启新进程避免阻塞
  • 策略被动执行:配置文件中的执行策略满足一条就会触发
  • redis服务关闭

然后再来了解rdb如何执行。rdb bgsace ,会新开子进程,由于fork的copy on write,子进程和父进程会公用一张页表,只有某一方修改内存时,才会真正去复制,而不是一开始就完整复制完整的内存。子进程执行不会修改数据,所以相当于子进程得到了父进程某一帧的内存快照信息,子进程可以在不阻塞父进程的情况下,将快照信息记录下来。

混合持久化

混合持久化是使用了rdb和aof的部分特性形成的新的持久化方式,为了将两种方案扬长取短。混合持久化发生在aof重写时,aof重写不再是对aof日志进行整理,而是执行bgsave,将当前状态写入到新的aof文件。剩余的部分和aof重写保持一致。

如何开启?

1
aof-use-rdb-preamble

MP-AOF

Redis高可用策略

主从复制

为什么要主从复制

主从复制是一种分布式部署模式,利用集群化部署可以提供很多优势,如:

  • 数据备份,写节点和读节点的数据相同,是一种数据上的备份
  • 高可用,将读写压力解耦,分摊到不同的节点上
  • 提供故障恢复的能力,写节点故障,会选举新的写节点,保证高可用

主从复制是指将读写分离,写节点是主节点,只执行写操作,读节点是从节点,主节点要向从节点去同步数据,所以叫主从复制。

复制方式

复制分为两种,全量复制和增量复制,两种复制发生的时机不同。注意主从之间都有tcp连接,用于通信。

全量复制发生在从节点刚上线或者刚从故障中恢复上线,此时从节点需要向主节点申请同步数据,主节点收到从节点的全量复制请求,就是执行bgsave生成数据快照,发送给从节点。从节点接受到快照将快照load进内存,完成数据复制。这里会有两个问题:

一是全量复制期间,主节点收到的写命令如何同步给从节点呢?主节点在执行bgsave时,会开启repl buffer,用于缓存全量复制期间收到的指令,在执行完全量复制后,执行增量复制前,将repl buffer内的数据发送给从节点;

二是为什么选用rdb快照而不是aof文件呢?rdb是redis内存状态的二进制数据,从节点直接将rdb load进内存即可完成数据更新;而aof是指令集合,从节点还需要重放指令才能完成,效率慢;

然后是增量复制,当从节点只是短暂的掉线,不想进行全量同步时,会向主节点发送psync,此时触发增量同步。增量同步需要使用到两个缓冲区:repl buffer 和 repl backlog buffer。具体过程如下:从节点发送psync,携带自己的最新写指令的offset,主节点发送continue响应,主节点根据offset从repl buffer中将从节点需要的指令发送过去。

这里同样有问题:

一是主节点如何知道从节点缺少什么数据呢?需要使用上面所说的两个缓冲区了。repl backlog buffer 用于缓存主从节点之间差异的数据。replication offset表示repl backlog buffer 同步的进度。master repl offset 记录写进度,slave repl offset记录读进度。

先知道这两个缓冲区如何工作。repl backlog buffer何时写入呢?主服务器传播写指令时,会额外将写指令写入repl backlog buffer,同时记录自己的写进度。从服务器接收写命令,更新读进度。当从节点恢复后,将自己的读进度发送给主节点,主节点就知道从repl backlog buffer中获取什么数据发送给从节点了。如果读进度落后太多,repl backlog buffer内已经无法同步了,就会要求从节点进行全量复制。

哨兵模式

主节点存在故障的可能,所以需要具备主从故障转移的能力。哨兵模式就应运而生。哨兵会监听所有节点发送过来的心跳信号。当哨兵节点监听不到主节点的信号时,会判定主节点掉线。这是主观下线,此时是不健壮的决策,可能主节点没有掉线,只是网络波动带来的心跳丢失。所以哨兵通常以集群的模式部署,通常部署奇数个哨兵。当哨兵接收不到主节点的心跳时,会向其他的哨兵节点发起投票,其他哨兵会根据自己的监听情况回复赞同和拒绝。当哨兵接收到半数以上的赞同票,就认为主节点丢失,此时就是客观下线,会向主哨兵申请重新选举个主节点。哨兵leader选举出新的主节点后,会向所有从节点发送新信息。同时向客户端发送新主节点的信息。

哨兵是如何将新的主节点的信息通知给客户端的呢?

客户端订阅哨兵提供的通知主节点信息的渠道,主从切换完成后,哨兵向该渠道广播消息给客户端。

同时哨兵会继续监听主节点的心跳,当老主节点重新连上来时,会通知它被降级为从节点。

这里有个问题,哨兵leader如何选举出新的主节点呢?

从三个维度去选举:

  • slave 优先级,可以在conf中用replica-priority配置
  • 复制进度(slave repl backlog offset),进度越快的越优先选举
  • 节点id,当前两个大小都相同的话,就会选取id较小的

Redis 集群

首先要知道,有了redis主从复制为什么还要有集群分片呢?首先redis主从复制中的写节点只有一个,写压力集中在单机瓶颈上,扩展性较差。然后当写指令太多时,redis内数据量太大时,会带来rdb持久化和全量复制上的开销。这时就提出redis集群。

什么是redis集群呢?redis中的一致性哈希切片分配

redis集群就是假定有16384个slot,集群内的节点平均的分配这些槽,负责哈希到该槽的写命令的执行。这样整个redis数据就被均摊到不同的节点上,解决上面所述的问题。问题来叻,写指令如何知道会分配到那个slot去执行呢?有两个算法:普通哈希和一致性哈希。

首先讲讲普通哈希,redis中使用crc16算法来计算哈希值,写指令的key使用crc16计算得到hash,hash%节点size得到分配的节点。这么做会有个问题,当集群中的节点下线,全部key的分配的槽都会发生改变,需要重新计算哈希关系,导致大量的数据迁移;

然后是一致性哈希,引入哈希槽的概念,固定的哈希槽不会导致key的哈希映射发生变化,节点的上下线只需要改变哈希槽的所属情况即可。redis中默认使用一致性哈希,存在16384个哈希槽,相邻的多个哈希槽组成哈希token,一个节点分配一个token。写指令到来时,key使用crc16计算得到hash,hash%16384+1位于的token是写指令分配到的节点token。

一致性哈希下,节点的上线和下线只需要移动一个token内的key的数据即可。比如token b下线,会顺时针找到下一个token c,将token b内的槽分配给token c,将token b的数据同步给token c。token d节点上线后,会使用hash算法计算hash得到自己的起始槽,然后插入到某个环中,token d会向负责该token的节点申请起始槽到token的结束槽的key的数据,并开始负责这个token。

那么问题又又又来叻,节点的上下线,数据的申请都需要节点之间的通信,节点之间如何通信呢?redis集群主要通过gossip协议进行通信,gossip协议的理念就是:每个节点设置定时任务,周期性的选取一部分节点,把当前节点存储的节点状态信息组播给选取的节点更新。很快集群内的节点的信息就都保持一致了。redis集群具体是如何使用gossip协议进行通信的:

  • meeting消息:新节点加入集群,向所有节点发送meeting消息
  • ping消息:节点会向一部分节点通知自己的状态(节点信息,分配的哈希槽,已知的节点列表)
  • pong消息:meeting和ping的响应
  • fail消息:当节点发现某节点挂了后,会像周围的节点发送fail消息,接收到fail消息后将fail节点标记为下线。和哨兵一样,节点下线后,其他节点会认为其主观下线和客观下线,发现客观下线后才会发送fail消息。

问题又有偶来叻,redis集群中,每个节点都需要进行主从复制部署,并且不需要哨兵模式去复制监控节点状态,内部使用gossip协议即可。节点内的从节点下线时,主节点把日志复制列表中删除;主节点下线时,所有的其他节点内的主节点会选举一个新的从节点,策略和之前说的一样。

Redis脑裂

由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

Redis 过期删除

alt text

alt text

Redis内存满了会怎么处理?

alt text

Redis 缓存

缓存雪崩&&缓存击穿

alt text

缓存穿透

alt text

常见的缓存更新策略

cache aside

alt text

read/write through && write back

alt text

This post is licensed under CC BY 4.0 by the author.