redis单线程为什么这么快
我们通常说的Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,也可以理解为执行实际命令的处理是单线程的。但 Redis 的其他功能,比如持久化、AOF重写、异步删除、集群数据同步等都是由额外的线程执行的。所以严格来说Redis并不是单线程的。
1. redis为什么这么快
Redis的高性能概述主要取决于以下几个方面:
- **数据在内存中,全部是内存操作:**完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- **采用高性能IO模型:**Redis 采用了IO多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,并实现高吞吐率。
- **单线程模型降低额外开销:**采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 避免多线程上下文切换的性能损耗
- 避免访问共享资源加锁的性能损耗
- 降低系统复杂度,开发可维护性高
- **高效合理的数据结构:**Redis 中有多种数据类型,每种数据类型的底层都由一种或多种数据结构来支持,比如跳表、HashMap、压缩列表等。正是因为有了这些不同的数据结构,使得数据存储时间复杂度降到最低,Redis 在存储与读取上的速度才不受阻碍。
2. Redis持久化
2.1 RDB
将 Redis 某一时刻的内存数据保存到硬盘的文件当中,默认保存的文件名为 dump.rdb,而在 Redis 服务器启动时,会重新加载 dump.rdb 文件的数据到内存中。
rdb的触发方式
- 使用
save
命令
127.0.0.1:6379> save
OK
save
命令是一个同步操作。当客户端向服务器发送 save 命令请求进行持久化时,服务器会阻塞 save 命令之后的其他客户端的请求,直到数据同步完成。
如果数据量太大,同步数据会执行很久,这期间 Redis 服务器也无法接收其他请求,导致不可用。
- 使用
bgsave
命令
127.0.0.1:6379> bgsave
Background saving started
与 save 命令不同,bgsave
命令是一个异步
操作。当客户端发出 bgsave 命令时,Redis 服务器主进程会 fork 一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求,子进程会在数据保存到 rdb 文件后退出。
这里我们来思考一个问题,既然 Redis 要处理客户端的请求又要同时持久化,那持久化的同时,内存数据结构还在改变,比如一个 hash 字典正在持久化,结果一个请求过来把它给删掉了,可是还没有持久化完呢,这会不会导致持久化的数据不一致啊?
Redis 使用的操作系统的多进程 COW(Copy On Write)机制来实现快照持久化,也就是我们这里的 RDB 持久化方式。
Redis 会使用操作系统的 COW 机制来进行数据段页面的分离,数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改,这时子进程相应的页面是没有变化的,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为啥 RDB 持久化为啥叫快照持久化的原因。
- 配置自动触发
在 redis.conf 中配置: save 多少秒内数据变了多少
# save "" # 不使用RDB存储 不能主从
save 900 1 # 表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 # 表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 # 表示1分钟内至少10000个键被更改则进行快照。
通过配置文件触发持久化的方式与 bgsave 命令类似,达到触发条件时会 fork 一个子进程进行数据保存。
线上的 Redis 环境不建议使用此方式。因为设置触发的时间太短,则容易频繁写入 rdb 文件,影响服务器性能,时间设置太长则会造成数据丢失。
- flushall
flushall
命令用于清空 Redis 数据库,在生产环境下一定慎用,当 Redis 执行了 flushall
命令之后,则会触发自动持久化,把 RDB 文件清空。
- 主从同步触发
在 Redis 主从复制中,当从节点执行全量复制操作时,主节点会执行 bgsave
命令,并将 RDB 文件发送给从节点,该过程会自动触发 Redis 持久化。
- Redis 父进程首先判断:当前是否在执行
save
、bgsave
或bgrewriteaof
(aof 文件重写命令)的子进程,如果在执行则 bgsave 命令直接返回。(不能同时执行的原因是出于性能方面考虑,并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,对性能会产生影响。)- 父进程执行 fork(调用 OS 函数复制主进程)操作创建子进程,这个过程中父进程是非阻塞的,Redis 能执行来自客户端的其它命令。
- 子进程创建 RDB 文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。 (RDB 始终完整)
- 子进程发送信号给父进程表示完成,父进程更新统计信息。
- 父进程 fork 子进程后,继续工作。
bgsave流程图:
2.2 AOF
AOF(Append Only File)持久化方式会记录客户端对服务器的每一次写操作命令,并将这些写操作以 Redis 协议追加保存到以后缀为 aof 文件末尾,在 Redis 服务器重启时,会加载并运行 aof 文件的命令,以达到恢复数据的目的。
Redis 默认不开启 AOF 持久化方式,我们可以在 redis.conf
配置文件中开启并进行更加详细的配置。
appendonly no # 默认不开启,需要开启的话要改成yes。
appendfilename "appendonly.aof" # aof文件名
dir ./ # AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
# appendfsync always # 写入策略,每执行一个命令保存一次
appendfsync everysec # 写入策略,每秒钟保存一次
# appendfsync no # 写入策略,不保存
no-appendfsync-on-rewrite no # 默认不重写aof文件
AOF 执行原理
AOF 文件中存储的是 redis 的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
命令传播
:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。缓存追加
:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。文件写入和保存
:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
AOF 优点
- AOF 持久化保存的数据更加完整,AOF 提供了三种保存策略:每次操作保存、每秒钟保存一次、跟随系统的持久化策略保存,其中每秒保存一次,从数据的安全性和性能两方面考虑是一个不错的选择,也是 AOF 默认的策略,即使发生了意外情况,最多只会丢失 1s 钟的数据;
- AOF 采用的是命令追加的写入方式,所以不会出现文件损坏的问题,即使由于某些意外原因,导致了最后操作的持久化数据写入了一半,也可以通过 redis-check-aof 工具轻松的修复;
- AOF 持久化文件,非常容易理解和解析,它是把所有 Redis 键值操作命令,以文件的方式存入了磁盘。即使不小心使用
flushall
命令删除了所有键值信息,只要使用 AOF 文件,删除最后的flushall
命令,重启 Redis 即可恢复之前误删的数据。
AOF 缺点
- 对于相同的数据集来说,AOF 文件要大于 RDB 文件;
- 在 Redis 负载比较高的情况下,RDB 比 AOF 性能更好;
- RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 更健壮。
2.3 混合持久化
RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时使用 RDB 和 AOF 各种的优点,Redis 4.0 之后新增了混合持久化的方式。
在开启混合持久化的情况下,AOF 重写时会把 Redis 的持久化数据,以 RDB 的格式写入到 AOF 文件的开头,之后的数据再以 AOF 的格式化追加的文件的末尾。
混合持久化的数据存储结构如下图所示:
查询是否开启混合持久化:使用 config get aof-use-rdb-preamble
命令。yes 表示已经开启混合持久化,no 表示关闭,Redis 5.0 默认值为 yes。 如果是其他版本的 Redis 首先需要检查一下,是否已经开启了混合持久化,如果关闭的情况下,可以通过以下两种方式开启:
- 通过命令行开启
- 通过修改 Redis 配置文件开启
混合持久化的加载流程如下:
- 判断是否开启 AOF 持久化,开启继续执行后续流程,未开启执行加载 RDB 文件的流程;
- 判断 appendonly.aof 文件是否存在,文件存在则执行后续流程;
- 判断 AOF 文件开头是 RDB 的格式, 先加载 RDB 内容再加载剩余的 AOF 内容;
- 判断 AOF 文件开头不是 RDB 的格式,直接以 AOF 格式加载整个文件。
AOF 加载流程图如下图所示:
Redis 是通过判断 AOF 文件的开头是否是 REDIS
关键字,来确定此文件是否为混合持久化文件的。
小贴士:AOF 格式的开头是 *,而 RDB 格式的开头是 REDIS。
3. Redis aof文件过大怎样处理
**为了解决 AOF 文件体积膨胀的问题,Redis 提供了文件重写(rewrite)功能。**通过该功能,Redis 服务器可以创建一个新的 AOF 文件来代替旧的 AOF 文件,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 所谓的“重写”其实是一个有歧义的词语,实际上,AOF 重写并不需要对原有旧的 AOF 文件进行任何写入和读取,新旧两个 AOF 文件所保存的数据库状态相同,但新的 AOF 文件不会包含任何浪费空间的冗余命令,所以新的 AOF 文件体积通常比旧的 AOF 文件体积小得多。
如果服务器想要用尽量少的命令来记录 list 键的状态,那么最简单高效的办法不是去读取和分析现有 AOF 文件的内容,而是直接从数据库中读取键 list 的值,然后用 rpush list 3 4 5 6 7
命令来代替保存在 AOF 文件中的六条命令,这样就可以将保存在 list 键所需的命令从六条减到一条了。
除了上面的集合键以外,其它所有类型的键都可以用同样的方法去减少 AOF 文件中的命令数量。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。
触发 AOF 文件重写,要满足两个条件,这两个条件也是配置在 Redis 配置文件中的,它们分别:
- auto-aof-rewrite-min-size:允许 AOF 重写的最小文件容量,默认是 64mb 。
- auto-aof-rewrite-percentage:AOF 文件重写的大小比例,默认值是 100,表示 100%,也就是只有当前 AOF 文件,比最后一次(上次)的 AOF 文件大一倍时,才会启动 AOF 文件重写。
AOF 后台重写
上面的 AOF 重写功能是通过 aof_rewrite
函数来实现的,但这个函数会进行大量的写操作,所以调用这个线程将被长时间阻塞,因为 Redis 服务器使用单线程来处理命令请求,所以如果服务器直接调用 aof_rewrite
函数的话,那么重写 AOF 文件期间,服务器将无法处理客户端发来的命令请求。
Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到后台子进程里执行, 这样处理的最大好处是:
- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
- 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。
不过,使用子进程也有一个问题需要解决:因为子进程在进行 AOF 重写期间,主进程还需要继续处理 命令,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的 AOF 文件中的数 据不一致。
为了解决这个问题,Redis 增加了一个 AOF 重写缓存,这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后,除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,还会追加到这个缓存中。
重写过程分析
Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生 停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到 新 AOF 文件,并开始对新 AOF 文件进行追加操作。
当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
- 处理命令请求
- 将写命令追加到现有的 AOF 文件中
- 将写命令追加到 AOF 重写缓存中
这样一来可以保证:
- 现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。
- 所有对数据库进行修改的命令都会被记录到 AOF 重写缓存中。
- 当子进程完成 AOF 重写之后,它会向父进程发送一个完成信号,父进程在接到完成信号之后,会调用 一个信号处理函数,并完成以下工作:
- 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
- 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。
这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接受命令请求了。在整个 AOF 后台重 写过程中,只有最后的写入缓存和改名操作会造成主进程阻塞,在其他时候,AOF 后台重写都不会对主进程造成阻塞,这将 AOF 重写对性能造成的影响降到了最低。
以上就是 AOF 后台重写,也即是 BGREWRITEAOF
命令的工作原理。
4. Redis存在大key时增加从库会有阻塞吗?
大key有什么影响:
我们都知道,redis的一个典型特征就是:核心工作线程是单线程。
单线程中请求任务的处理是串行的,前面完不成,后面处理不了,同时也导致分布式架构中内存数据和CPU的不平衡。
-
执行大key命令的客户端本身,耗时明显增加,甚至超时
-
执行大key相关读取或者删除操作时,会严重占用带宽和CPU,影响其他客户端
-
大key本身的存储带来分布式系统中分片数据不平衡,CPU使用率也不平衡
-
大key有时候也是热key,读取操作频繁,影响面会很大
-
执行大key删除时,在低版本redis中可能阻塞线程
这样看来大key的影响还是很明显的,最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。
大key的处理:
根据大key的实际用途可以分为两种情况:可删除和不可删除。
如果发现某些大key并非热key就可以在DB中查询使用,则可以在Redis中删掉:
- 当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
Redis UNLINK 命令类似与 DEL 命令,表示删除指定的 key,如果指定 key 不存在,命令则忽略。
UNLINK 命令不同与 DEL 命令在于它是异步执行的,因此它不会阻塞。
UNLINK 命令是非阻塞删除,非阻塞删除简言之,就是将删除操作放到另外一个线程去处理。
- 当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
Redis Scan 命令用于迭代数据库中的数据库键。
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
不可删除的:
- 压缩和拆分key
当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
一般是采用多台 Redis, 分别去存储用户的数据,从而实现内存数据的扩容.
对于用户而言:会将 Redis 分片的多个 Redis 当作一个整体,不在乎数据存在那个中,只在乎能不能存储。参考: https://segmentfault.com/a/1190000024430014
5. 数据恢复
5.1 正常数据恢复
正常情况下,只要开启了 AOF 持久化,并且提供了正常的 appendonly.aof 文件,在 Redis 启动时就会自定加载 AOF 文件并启动,执行如下图所示所示:其中 DB loaded from append only file......
表示 Redis 服务器在启动时,先去加载了 AOF 持久化文件。
小贴士:默认情况下 appendonly.aof 文件保存在 Redis 的根目录下。
持久化文件加载规则
- 如果只开启了 AOF 持久化,Redis 启动时只会加载 AOF 文件(appendonly.aof),进行数据恢复;
- 如果只开启了 RDB 持久化,Redis 启动时只会加载 RDB 文件(dump.rdb),进行数据恢复;
- 如果同时开启了 RDB 和 AOF 持久化,Redis 启动时只会加载 AOF 文件(appendonly.aof),进行数据恢复。
在 AOF 开启的情况下,即使 AOF 文件不存在,只有 RDB 文件,也不会加载 RDB 文件。 AOF 和 RDB 的加载流程如下图所示:
5.2 简单异常数据恢复
在 AOF 写入文件时如果服务器崩溃,或者是 AOF 存储已满的情况下,AOF 的最后一条命令可能被截断,这就是异常的 AOF 文件。
在 AOF 文件异常的情况下,如果为修改 Redis 的配置文件,也就是使用 aof-load-truncated
等于 yes
的配置,Redis 在启动时会忽略最后一条命令,并顺利启动 Redis,执行结果如下:
* Reading RDB preamble from AOF file...
* Reading the remaining AOF tail...
# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 439 !!!
# AOF loaded anyway because aof-load-truncated is enabled
5.3 复杂异常数据恢复
AOF 文件可能出现更糟糕的情况,当 AOF 文件不仅被截断,而且中间的命令也被破坏,这个时候再启动 Redis 会提示错误信息并中止运行,错误信息如下:
* Reading the remaining AOF tail...
# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>
出现此类问题的解决方案如下:
- 首先使用 AOF 修复工具,检测出现的问题,在命令行中输入
redis-check-aof
命令,它会跳转到出现问题的命令行,这个时候可以尝试手动修复此文件; - 如果无法手动修复,我们可以使用
redis-check-aof --fix
自动修复 AOF 异常文件,不过执行此命令,可能会导致异常部分至文件末尾的数据全部被丢弃。
评论区