- 笔记碎片
Memory Model
内存主要包括:对象内存 + 缓冲内存 + 自身内存 + 内存碎片。如下
对象内存
对象内存是 Redis 内存中占用最大的一块,存储着所有的用户数据。我们知道 Redis 是一个 key-value 的内存数据库,所有的数据都采用 key-value 型数据类型,每次在创建 key-value 键值对对象的时候都要创建两个对象:key 对象和 value 对象。其中 key 对象是字符串,value 对象我们知道有五中数据类型-String、Hash、List、Set、Zset,每种数据类型在使用的时候占用的内存不同。
缓冲内存
主要包括:客户端缓冲、AOF 缓冲区、复制积压缓冲区。
- 客户端缓冲:普通的客户端连接
- AOF 缓冲区:Redis 持久化分为两种:RDB 和 AOF,其中 RDB 是内存快照,AOF 是将 Redis 的命令 append 在文件中,不过在写入文件之前会先写入到缓冲区,然后根据不同的持久化策略向磁盘进行同步。在进行 AOF 重写时也有一个 AOF 重写缓冲区。一般 AOF 缓冲区都会比较小。
- 复制积压缓冲区:主要用于主从同步。在进行主从同步时,Redis 会将最新的命令写入到复制积压缓冲区,在进行复制的时候,会校验复制偏移量是否在复制积压缓冲区中,如果是则进行部分复制,否则进行全量复制。它默认情况下是 1MB,我们需要根据实际请求适当调整他的大小,毕竟设置太小的话,可能会使部分复制退化为全量复制。
自身内存
自身内存主要指 AOF/RDB 的时候 Redis 创建子进程内存的消耗,一般这部分的消耗会比较小。
内存碎片
目前可选的分配器有 jemalloc、glibc、tcmalloc,默认 jemalloc。
出现高内存碎片问题的情况:大量的更新操作,比如 append、setrange;大量的过期键删除,释放的空间无法得到有效利用。
解决办法:数据对齐,安全重启(高可用/主从切换)。
IO Model
-
Pipeline
它允许客户端一次性发送多条命令,减少 RTT 和 IO 的调用次数(IO 调用涉及到用户态到内核态之间的切换)。
它能将一组 Redis 命令进行组装,通过一次 RTT 传输给 Redis,同时再将这组命令的执行结果按照顺序返回给客户端。将原来一组命令多次 RTT 以及 IO 交互变成了一组 RTT 和 IO 交互,大大减少了网络传输时间和 IO 调用的时间。(RTT -> Round Trip Time => request、response)
-
epoll
Linux 内核为处理大批量文件描述符而作了改进的 poll,是 Linux 下多路复用 IO 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了。epoll 除了提供 select/poll 那种 IO 事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存 IO 状态,减少 epoll_wait/epoll_pwait 的调用,提高应用程序效率。
Tread Model
-
before 6.0-single
-
6.0 multi-IO,worker-single
基于非阻塞的 IO 多路复用机制。
基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。模型如下图:
从上图可知,文件事件处理器的结构包含了四个部分:
- 多个 Socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器
多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。
在 Redis 中,Socket 会产生 AE_READABLE 和 AE_WRITABLE 事件:
- 当 socket 变得可读时或者有新的可以应答的 socket 出现时,socket 就会产生一个 AE_READABLE 事件
- 当 socket 变得可写时,socket 就会产生一个 AE_WRITABLE 事件。
文件事件派发器将事件交给事件处理器,事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器对应不同的 socket 事件:
- 如果是客户端要连接 Redis,那么会为 socket 关联连接应答处理器
- 如果是客户端要写数据到 Redis(读、写请求命令),那么会为 socket 关联命令请求处理器
- 如果是客户端要从 Redis 读数据,那么会为 socket 关联命令回复处理器
下图是客户端与 Redis 通信的一次完整的流程:
- Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。
- 如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket,同时将这个 socket 的 AE_READABLE 事件与命令请求处理器关联起来。
- 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联。
- 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。
- 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。
Persistence
RDB
Redis 的内存快照,它是在某一个时间点将 Redis 的内存数据全量写入一个临时文件,当写入完成后,用该临时文件替换上一次持久化生成的文件,这样就完成了一次持久化过程。
- Config
#dbfilename:持久化数据存储在本地的文件
dbfilename dump.rdb
#dir:持久化数据存储在本地的路径,如果是在/redis/src下启动的redis-cli,则数据会存储在当前src目录下
dir ./
##snapshot触发的时机,save <seconds> <changes>
##一般来说我们需要根据系统变更操作密集程度来认真评估这个值
##可以通过 “save “””来关闭snapshot功能
save 900 1
save 300 10
save 60 10000
##当snapshot时出现错误无法继续时,是否阻塞客户端“变更操作”,“错误”可能因为磁盘已满/磁盘故障/OS级别异常等
stop-writes-on-bgsave-error yes
##是否启用rdb文件压缩,默认为“yes”,压缩往往意味着“额外的cpu消耗”,同时也意味这较小的文件尺寸以及较短的网络传输时间
rdbcompression yes
-
Execution process
分为手动和自动两种方式,其中手动方式有两种:save 命令和 bgsave 命令。
-
save 命令:阻塞 Redis 服务,直到整个 RDB 持久化完成。我们知道 RDB 是全量持久化,如果内存的数据量大,则造成长时间的阻塞,这样势必会影响业务。所以一般不推荐采用这种方式。
-
bgsave 命令:该模式下的 RDB 持久化由子进程完成.Redis 进程接收到该命令后,会 fork 操作创建一个子进程,持久化过程有子进程完成。Redis 服务阻塞只会发生在 fork 阶段,而且该阶段时间过程一般都会很短。其流程如下:
- 客户端发送 bgsave 命令,Redis 进程首先判断当前是否存在其他子进程在执行操作,如 RDB 或者 AOF 子进程,如果有,则立刻返回,否则执行 2。
- Redis 父进程执行 fork 操作创建子进程,在 fork 操作过程中父进程会阻塞。
- Redis 父进程 fork 操作完成后,bgsave 命令返回
Background saving started
信息并不再阻塞 Redis 父进程,可以继续响应其他命令了。 - fork 的子进程则根据 Redis 父进程的内存数据生成 RDB 文件,完成后替换原有的 RDB 文件。同时,发送信号给 Redis 父进程表示 RDB 操作已完成,父进程则更新统计信息。
自动触发的方式:save m n
-
Recovery
在服务器启动时自动加载的,如果在 Redis 服务器中没有设置 AOF ,那么 Redis 服务器在启动是就会检测 RDB 文件(redis-check-rdb 命令),并自动载入。在载入期间,Redis 服务器会一直处于阻塞状态,直到完成为止。如果载入的 RDB 文件损坏了,则会载入失败,Redis 服务会启动失败,我们可以通过
redis-check-rdb
来完成对 RDB 文件的检测和修复。- 优点
- 由于 RDB 文件是一个非常紧凑的二进制文件,所以加载的速度回快于 AOF 方式
- fork 子进程方式,不会阻塞
- RDB 文件代表着 Redis 服务器的某一个时刻的全量数据,所以它非常适合做冷备份和全量复制的场景
- 缺点
- 没办法做到实时持久化,会存在丢数据的风险。定时执行持久化过程,如果在这个过程中服务器崩溃了,则会导致这段时间的数据全部丢失
- 优点
AOF
Append only file,它是将每一行对 Redis 数据进行修改的命令以独立日志的方式存储起来。由于 Redis 是将“操作 + 数据” 以格式化的方式保存在日志文件中,他代表了这段时间所有对 Redis 数据的的操作过程,所以在数据恢复时,我们可以直接 replay 该日志文件,即可还原所有操作过程,达到恢复数据的目的。它的主要目的是解决了数据持久化的实时性。
AOF 默认关闭,需要在配置文件 redis.conf 中开启,appendonly yes
。
- Config
## aof功能的开关,默认为“no”,修改为 “yes” 开启
## 只有在“yes”下,aof重写/文件同步等特性才会生效
appendonly no
## 指定aof文件名称
appendfilename appendonly.aof
## 指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec
# appendfsync always
appendfsync everysec
# appendfsync no
##在aof-rewrite期间,appendfsync是否暂缓文件同步,"no"表示“不暂缓”,“yes”表示“暂缓”,默认为“no”
no-appendfsync-on-rewrite no
## aof文件rewrite触发的最小文件尺寸(mb,gb),只有大于此aof文件大于此尺寸是才会触发rewrite,默认“64mb”,建议“512mb”
auto-aof-rewrite-min-size 64mb
## 相对于“上一次”rewrite,本次rewrite触发时aof文件应该增长的百分比。
## 每一次rewrite之后,redis都会记录下此时“新aof”文件的大小(例如A),那么当aof文件增长到A*(1 + p)之后
## 触发下一次rewrite,每一次aof记录的添加,都会检测当前aof文件的尺寸。
auto-aof-rewrite-percentage 100
-
Execution process
总共分为三个流程
- 命令写入
- 文件同步
- 文件重写
-
命令写入
Redis 在命令写入时,将缓冲区(aof_buf)引用进来了。我们知道 Redis 是单线程的,如果每次 append aof 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘的负载,性能肯定会受到一些影响,所以将命令先写入到 aof_buf 中。这样做还有一个目的,那就 Redis 可以提供多种同步策略,让用户在性能和安全方面做出平衡。
-
文件同步
命令写入到缓冲区,然后根据不同的策略刷到硬盘中。Redis 提供提供了三种不同的同步策略:always everysec no,由 appendfsync 控制。
策略 always everysec no 行为 每条命令 fsync 到硬盘 每秒把缓冲区 fsybc 到硬盘 OS 决定什么时候来把缓冲区的命令写到硬盘 特点 不丢失数据,IO 开销大,硬盘压力 可能会丢失某一秒的数据 不用管,不可控 - always :每天命令都会同步至硬盘,是最安全的方式,但是对 IO 开支大,硬盘压力大,无法满足 Redis 高性能的要求,所以我们一般不推荐这种策略。如果对数据安全性要求这么高,其实可以选择关系型数据库。
- everyesc:每秒同步一次,算是一种比较中庸的选择方式,也是 Redis 推荐的方式,但是如果遇到服务器故障,可能会丢失最近一秒的记录 。
- no:Redis 服务并不会直接参与同步,而是将控制权交个操作系统,操作系统会根据系统实际情况来触发同步,不可控。
-
文件重写
随着命令的不断写入,AOF 文件会越来越庞大,直接的影响就是导致“数据恢复”时间延长,而且有些历史的操作是可以废弃的(比如超时、del 等等),为了解决这些问题,Redis 提供了 “文件重写”功能,该功能有以下两种方式触发。
- 手动触发:bgrewriteaof 命令
- 自动触发:由 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数来确定自动触发时机。
- auto-aof-rewrite-min-size:运行 AOF 重写时文件最小体积
- auto-aof-rewrite-percentage:代表当前 AOF 文件空间(aof_current_size)和上一次重写后 AOF 文件空间(aof_base_size)的比值
- 触发时机 = 当前 AOF 文件空间 > AOF 重写时文件最小体积 &&
重写 AOF 文件最直观的表现是导致 AOF 文件减小,重写时,Redis 主要做了如下几件事情让 AOF 文件减小:
- 已过期的数据不在写入文件。
- 保留最终命令。例如
set key1 value1
、set key1 value2
、….set key1 valuen
,类似于这样的命令,只需要保留最后一个即可。 - 删除无用的命令。例如
set key1 valuel;del key1
,这样的命令也是可以不用写入文件中的。 - 多条命令合并成一条命令。例如
lpush list a、lpush list b、lpush list c
,可以转化为lpush list a b c
文件重写流程如下(参考 《Redis 开发与运维》):
- Redis 服务接收到 bgrewriteaof 命令的时候,会做两步检查。
- 如果当前进程正在执行 AOF 重写,则直接返回。
- 如果有进程正在执行 bgsave,那么需要等待 bgsave 执行完毕后再执行 AOF 重写。
- Redis 进程会 fork 一个子进程执行 AOF 重写,成功后,Redis 服务继续响应命令,不会影响 Redis 原有的 AOF 流程(即命令写入 aof_buf 缓冲区和缓冲区的数据刷进硬盘)。
- 在子进程重写过程中,Redis 主进程会将受到的命令也会写入 AOF 重写缓冲区,这个缓冲区和 aof_buf 缓冲区不一样,需要区分,这样做的目的是为了防止重写过程中数据的丢失。
- 由于使用写时复制技术(copy-on-write),子进程只能拿到父进程在 fork 子进程时刻的文件进行重写。子进程根据内存快照,按照命令重写规则将命令重写到新的文件中。这里需要注意的是每次写入硬盘的数据量不能太大,否则容易导致硬盘阻塞,该值由 aof-rewrite-incremental-fsync 控制,默认为 32M。
- 子进程完成 AOF 重写后会给发消息给 Redis 主进程,主进程则会将 AOF 重写缓冲区的数据写进新的文件,然后用新的 AOF 文件 替换老的文件。
- 完成 AOF 重写。
data structure
-
RedisObject
-
String
-
List(Link list-双向 acyclic list )
-
Set
-
ZSet(Sorted Sets)
-
Hash
-
-
Redis Data Structure
- SDS (Simple dynamic String)
- Dict (HashTable)
- ZIPList(连续顺序)
- ZipList(跳跃表)- ZSet
- intSet
others
- Bitmaps
BloomFilter
- HyperLoglog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
-
GEO
3.2 版本的新特性。储存地理位置信息.
-
Pub/Sub
“发布/订阅”在 redis 中,被设计的非常轻量级和简洁,它做到了消息的“发布”和“订阅”的基本能力;但是尚未提供关于消息的持久化等各种企业级的特性。
- Redis Module
detail
rehash
渐进式 rehash (dict[1])
rehash 时采取渐进式的原因:数据量如果过大的话,一次性 rehash 会有庞大的计算量,这很可能导致服务器一段时间内停止服务。
Redis 具体是 rehash 时这么干的:
(1:在字典中维持一个索引计数器变量rehashidx,并将设置为0,表示rehash开始。
(2:在rehash期间每次对字典进行增加、查询、删除和更新操作时,**除了执行指定命令外**;还会将ht[0]中rehashidx索引上的值**rehash到ht[1]**,操作完成后rehashidx+1。
(3:字典操作不断执行,最终在某个时间点,所有的键值对完成rehash,这时**将rehashidx设置为-1,表示rehash完成**
(4:在渐进式rehash过程中,字典会同时使用两个哈希表ht[0]和ht[1],所有的更新、删除、查找操作也会在两个哈希表进行。例如要查找一个键的话,**服务器会优先查找ht[0],如果不存在,再查找ht[1]**,诸如此类。此外当执行**新增操作**时,新的键值对**一律保存到ht[1]**,不再对ht[0]进行任何操作,以保证ht[0]的键值对数量只减不增,直至变为空表。
expire
方式:懒汉式、定期(随机抓取一些 key 做过期检测)
-
惰性删除(对 CPU 极度友好,对内存极度不友好)
-
- 每次从键空间取键的时候,判断一下该键是否过期了,如果过期了就删除。
-
定期删除(折中)
-
- 每隔一段时间去删除过期键,限制删除的执行时长和频率。
Redis Database(default 16)
RedisClient
typedef struct redisClient{
//客户端当前所选数据库
redisDb *db;
}redisClient;
RedisServer
struct redisServer{
//redisDb数组,表示服务器中所有的数据库
redisDb *db;
//服务器中数据库的数量
int dbnum;
};
typedef struct redisDb {
int id; // 数据库ID标识
dict *dict; // 键空间,存放着所有的键值对
dict *expires; // 过期哈希表,保存着键的过期时间
dict *watched_keys; // 被watch命令监控的key和相应client
long long avg_ttl; // 数据库内所有键的平均TTL(生存时间)
} redisDb;
typedef struct dict {
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不进行时,值为-1
int rehashidx;
}dict;
typedef struct dictType{
//计算哈希值的函数
unsigned int (*hashFunction)(const void * key);
//复制键的函数
void *(*keyDup)(void *private, const void *key);
//复制值得函数
void *(*valDup)(void *private, const void *obj);
//对比键的函数
int (*keyCompare)(void *privdata , const void *key1, const void *key2)
//销毁键的函数
void (*keyDestructor)(void *private, void *key);
//销毁值的函数
void (*valDestructor)(void *private, void *obj);
}dictType;
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemark;
//哈希表已有节点数量
unsigned long used;
}dictht;
typedef struct dictEntry {
//键
void *key;
//值
union {
void *value;
uint64_tu64;
int64_ts64;
}v;
//指向下个哈希节点,组成链表
struct dictEntry *next;
}dictEntry;
typedef struct redisObject{
// 对象的类型
unsigned type 4:;
// 对象的编码格式
unsigned encoding:4;
// 指向底层实现数据结构的指针
void * ptr;
//.....
}robj;
内存淘汰策略
设置 maxmemory
如果我们不设置 maxmemory ,Redis 则默认使用无限内存,所以为了 Redis 不系统的内存耗尽,我们在使用 Redis 的时候尽量去配置 maxmemory,给 Redis 设置内存使用上限。maxmemory 配置的是 Redis 的实际使用内存,即 used_memory,但是由于有内存碎片的存在,所以 Redis 实际使用的内存会比 used_memory 要大,在合理情况下一般只会大一点点。
配置内存回收策略
Redis 回收内存大致有两种机制:
-
删除达到过期时间的对象
-
当内存达到 maxmemory 时触发内存溢出控制策略,强制删除选择出来的对象
Redis 删除过期键值对对象一般有两种策略:惰性删除和主动定时任务删除。
惰性删除:这种删除策略,Redis 不会主动去删除已经过期的键值对,而是等待客户端去读取带有超时属性的键时,如果已经超时了则删除该键值对对象,然后返回空。这样有一个好处就是节省了 CPU ,因为 Redis 不需要单独去维护 TTL 链表来处理过期键的删除,但是有一个坏处就是如果过期的键一直都没有被访问,则永远不会被删除了。那么怎么解决呢?Redis 提供了一个定时任务的删除机制来补救。
定时任务删除:Redis 内部维护一个定时任务,默认是每秒运行 10 次,删除逻辑如下图:
内存溢出控制策略
当 Redis 所用内存达到 maxmemory 上限时会触发相应的溢出控制策略。Redis 支持 6 种策略,如下所示:
策略 | 说明 |
---|---|
noeviction | 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOMcommandnotallowedwhenusedmemory,此时 Redis 只响应读操作。 |
volatile-lru | 根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到 noeviction 策略。 |
allkeys-lru | 根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。 |
allkeys-random | 随机删除所有键,直到腾出足够空间为止。 |
volatile-random | 随机删除过期键,直到腾出足够空间为止。 |
volatile-ttl | 根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。 |
内存溢出控制策略可以使用 config set maxmemory-policy {policy}
语句进行动态配置。
当 Redis 因为内存溢出删除键时,可以通过执行 info stats
命令查看 evicted_keys
指标找出当前 Redis 服务器已剔除的键数量。
使用场景
缓存(高可用,Cache-Aside Pattern-边缘缓存模式 )
-
缓存雪崩
由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
1. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
2. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓
存。
3. 为 key 设置不同的缓存失效时间。
-
缓存穿透
是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)
-
bloomFilter(BitMaps)
Redis 4.0 开始提供官方插件,module
-
-
缓存击穿(注意和前两者的区别)
-
缓存预热
系统上线后,将相关的缓存数据直接加载到缓存系统。
-
缓存更新
缓存更新除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
-
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。