Redis为什么比较快 - java后端面试必考 - 基于C老师

目录

Redis为什么比较快

RDB 文件的二进制格式是什么?

AOF(Append-Only File)日志的全流程


Redis为什么比较快

Redis 之所以快,核心原因是它采用了单线程处理命令,并结合了多线程优化,最大程度地减少了线程切换、锁竞争和 CPU 资源浪费。从 线程管理 的角度来看,Redis 的高性能主要来自以下几个方面:


1️⃣ 单线程模型(避免线程上下文切换)

Redis 大部分操作(读取、写入、计算)都由一个主线程处理,因此避免了多线程带来的上下文切换开销

线程上下文切换的成本

• 传统数据库(如 MySQL)采用多线程处理请求,多个线程可能会同时访问同一个数据,需要 加锁(Mutex) 来保证数据一致性。

Redis 采用单线程处理请求,一个时间点只执行一个操作,避免了线程切换和锁竞争

Redis 采用的事件驱动模型

Redis 使用 epoll + 事件驱动(event loop) 处理多个客户端请求,单线程但仍然能高效并发处理多个请求:

// Redis 事件循环(伪代码)
while (1) {
    events = epoll_wait(epoll_fd);  // 监听事件(如客户端连接、读写请求)
    for (event in events) {
        process(event);  // 依次处理事件
    }
}

所有请求都进入事件队列按顺序执行,无锁,高效。

避免了线程同步问题,让 CPU 资源集中处理请求。

结论:Redis 采用单线程,避免了线程切换的 CPU 开销,同时借助 epoll + 事件驱动 提高并发能力,使其性能更快。


2️⃣ 多线程 I/O 处理(减少主线程阻塞)

为了提高网络I/O的并行度,对于网络I/O采用多线程处理

Redis 6.0 以前,所有 网络 I/O 处理(accept()、read()、write())都是单线程,但 Redis 6.0+ 引入了 I/O 线程 来优化网络请求处理:

// Redis 6.0 之前:主线程处理 I/O + 命令执行(慢)
while (1) {
    read(client);   // 读取请求数据
    process(client); // 处理命令
    write(client);  // 发送响应
}

// Redis 6.0 之后:I/O 线程并行处理(快)
parallel_for_each(client, read());  // 多线程并行读取
for (client in clients) process(client);  // 主线程执行命令
parallel_for_each(client, write()); // 多线程并行写入

Redis 6.0 多线程 I/O

网络 I/O 采用多线程,并行读取/写入客户端数据,减少主线程的阻塞时间。

主线程仍然负责命令执行,保证数据一致性,避免锁竞争。

结论:Redis 6.0+ 通过 多线程 I/O 处理,降低了 网络 I/O 开销,让主线程能更专注于数据操作,提高吞吐量。


3️⃣ BIO 后台线程(处理慢操作)

除了主线程,Redis 还使用了 后台线程(BIO 线程) 处理慢 I/O 操作,避免主线程阻塞:

BIO 线程

作用

触发的 Redis 命令

BIO_CLOSE_FILE

异步关闭文件

Redis 关闭客户端连接、RDB 文件关闭

BIO_AOF_FSYNC

AOF 持久化

fsync()、AOF 持久化操作

BIO_LAZY_FREE

异步删除大 Key

UNLINK key、FLUSHALL ASYNC

典型案例:异步删除大 Key

DEL key(阻塞主线程)

• 如果 key 很大(比如 100MB 的 hash),DEL 会卡住 Redis,影响请求处理。

UNLINK key(使用 BIO 线程异步删除)

• UNLINK 将删除任务丢给 BIO_LAZY_FREE 线程,主线程继续处理请求,不被阻塞

结论:Redis 通过 BIO 线程异步执行耗时任务(AOF 刷盘、删除大 Key),降低了主线程的阻塞时间。


4️⃣ 内存操作优化(纯内存计算,避免磁盘 IO)

Redis 运行在内存中,所有数据直接从内存读取,避免了磁盘 I/O(传统数据库如 MySQL 需要从磁盘读取)。

使用高效的数据结构(如 skiplist、hash table、intset),优化存取速度。

使用 mmap() 进行 AOF 写入,减少磁盘 I/O 开销。

结论:Redis 所有数据都存放在内存,避免了磁盘 I/O,速度远超基于磁盘的数据库。


5️⃣ 采用无锁优化(减少同步等待)

单线程处理数据无锁,避免锁竞争导致的性能下降。

管道(Pipeline)优化:客户端可以一次发送多个命令,减少 RTT(网络往返)。

内存分配优化:Redis 使用 jemalloc 进行高效的内存管理,避免碎片化。

结论:Redis 通过 单线程无锁设计 + 内存优化,减少锁竞争,进一步提高性能。


6️⃣ 总结:Redis 为什么比传统数据库快?

优化点

原理

带来的性能提升

单线程模型

避免线程切换、锁竞争

减少 CPU 上下文切换,提升请求处理速度

多线程 I/O

并行处理 accept() / read() / write()

降低主线程的 I/O 开销,提高吞吐量

BIO 线程

异步处理 AOF 刷盘、关闭文件、大 Key 删除

避免主线程阻塞,提高响应速度

纯内存存储

直接从内存读取数据

避免磁盘 I/O,提升读写性能

无锁优化

采用单线程 + 事件驱动

减少锁竞争,提高并发处理能力

结论

Redis 快的核心原因是:

1. 单线程 处理请求,避免锁竞争和线程切换。

2. 多线程 I/O 提高网络吞吐,减少主线程的 I/O 开销。

3. BIO 线程 处理耗时任务(AOF、关闭文件、删除大 Key),防止主线程阻塞。

4. 纯内存计算,数据读写直接操作内存,避免磁盘 I/O。

5. 高效的数据结构,比如 hash table、skiplist、ziplist,优化数据存储。

这些优化让 Redis 每秒可以处理 10 万级别的 QPS(查询/秒),远超传统数据库!

RDB 文件的二进制格式是什么?

是的,RDB(Redis Database)文件的二进制格式就是数据在计算机中的真正存储格式

RDB 以二进制格式存储,相比文本格式(如 AOF),读取更快,占用空间更小


为什么 RDB 采用二进制存储?

1. 节省空间:二进制比文本格式紧凑,例如 int 1000 在文本中占 4 字节,但二进制仅占 2 字节

2. 加载速度快:Redis 直接按二进制读取,无需解析,比 AOF 恢复快 10 倍

3. 数据结构优化:RDB 存储 压缩后的数据结构,比原始存储更高效。


示例:RDB 文件的二进制内容

假设 Redis 中存储以下键值对

SET name "Alice"
SET age 25
SET city "Shanghai"

RDB 文件(示例二进制)

52 45 44 49 53 30 30 30 39 FA 02 6E 61 6D 65 05 41 6C 69 63 65
FA 02 61 67 65 19 FA 04 63 69 74 79 08 53 68 61 6E 67 68 61 69

解析(部分)

• 52 45 44 49 53 → “REDIS”(文件头标识)

• 30 30 30 39 → Redis 版本

• 6E 61 6D 65 05 41 6C 69 63 65 → 键 “name”,值 “Alice”

• 61 67 65 19 → 键 “age”,值 25

• 63 69 74 79 08 53 68 61 6E 67 68 61 69 → 键 “city”,值 “Shanghai”

AOF(Append-Only File)日志的全流程

AOF(Append-Only File) 是 Redis 提供的一种持久化机制,通过将所有写操作(如 SET、DEL 等) 追加写入日志文件(.aof),确保即使服务器崩溃也能恢复数据

核心目标记录所有写入操作,保证 Redis 数据不丢失!


1️⃣ AOF 日志的完整工作流程

AOF 主要涉及 3 个阶段

1. 命令写入 AOF 缓冲区(Append)

2. AOF 日志落盘(Flush)

3. AOF 文件重写(Rewrite)


2️⃣ AOF 详细流程

1. 命令写入 AOF 缓冲区(Append)

当客户端向 Redis 发送写操作命令(如 SET key value),Redis 不仅在内存中修改数据,还会把该命令转换成 Redis 协议格式并追加到 AOF 缓冲区中。

示例:

SET name "Alice"
INCR counter
DEL age

AOF 缓冲区内容(Redis 协议格式):

*3
$3
SET
$4
name
$5
Alice
*2
$4
INCR
$7
counter
*2
$3
DEL
$3
age

AOF 只记录 Redis 的”写”操作GET 等只读操作不会写入 AOF,避免日志膨胀。


2. AOF 日志落盘(Flush)

AOF 采用异步写入磁盘的方式,将 AOF 缓冲区中的数据写入 AOF 文件

Redis 提供了 3 种 AOF 写入策略(appendfsync 参数控制):

策略

原理

性能

安全性

always

每次写入命令都 fsync() 落盘

最慢

最安全(不会丢数据)

everysec(默认)

每秒 fsync() 一次,可能丢失 1 秒数据

折中

较安全

no

由操作系统决定何时 fsync()

最快

最不安全

Redis 默认 appendfsync everysec,保证性能和数据安全的平衡!

appendfsync everysec 工作方式

1. 客户端发送 SET name "Alice",Redis 先写入 AOF 缓冲区

2. 每秒执行一次 fsync(),把数据从内核缓冲区写入磁盘,降低 fsync 频率,提高性能。

3. 如果 Redis 崩溃,可能丢失 1 秒内的数据


3. AOF 文件重写(Rewrite)

问题:AOF 日志不断增长,文件会越来越大,怎么办?

解决方案:AOF 重写(Rewrite)—— 压缩日志,减少体积!

AOF 重写原理

• AOF 重写不会修改原始日志,而是创建一个新的 AOF 文件,只保留最终数据库状态的最少命令

• 通过 fork() 让子进程完成重写,避免阻塞 Redis 主线程。

AOF 重写触发条件

手动执行:BGREWRITEAOF

自动触发(auto-aof-rewrite-percentage 配置)

• AOF 文件增长超过 auto-aof-rewrite-min-size

• AOF 文件大小比上次重写后增长了一定比例(默认 100%)

示例

SET counter 1
INCR counter
INCR counter
INCR counter

原始 AOF(包含所有历史操作)

SET counter 1
INCR counter
INCR counter
INCR counter

AOF 重写后(仅保留最终状态)

SET counter 4

AOF 重写可以大幅减少 AOF 文件大小,提高恢复速度!


3️⃣ AOF 恢复数据流程

Redis 重启时,如何恢复数据?

恢复步骤

1. 加载 AOF 文件,逐行解析命令。

2. 按顺序执行 AOF 记录的所有命令,恢复数据。

3. 启动 Redis 服务,提供正常访问。

示例

如果 AOF 文件内容如下:

SET name "Alice"
INCR counter
SET city "Shanghai"

Redis 启动时,会按顺序执行这些命令,最终恢复 Redis 数据:

127.0.0.1:6379> GET name
"Alice"
127.0.0.1:6379> GET counter
"1"
127.0.0.1:6379> GET city
"Shanghai"

AOF 方式能保证 Redis 重启后数据不丢失,特别适合高可靠性需求的场景!


4️⃣ AOF vs RDB 对比

持久化方式

数据安全性

性能

文件大小

恢复速度

适用场景

AOF(默认)

几乎不丢数据(everysec 模式)

较慢(频繁写入)

较大(保存所有操作)

较慢(逐行执行恢复)

数据安全要求高

RDB

可能丢失最近一次快照后的数据

最快(定期保存)

最小(仅存数据快照)

最快(直接加载数据)

适用于大规模数据、冷备

Redis 默认同时开启 AOF + RDB,保证数据安全!

Redis主从复制的核心

来自

字节一面——redis主从复制的核心原理_哔哩哔哩_bilibili

Redis为什么比较快 - java后端面试必考 - 基于C老师_第1张图片

Runid(运行 ID)

在 Redis 里指的是 “运行标识符”(Run ID),它的英文全称是 “Run Identifier”


1. runid 是什么?

• runid 是 Redis 每个节点启动时自动生成的唯一标识符

• 每次 Redis 服务器重启,都会生成一个新的 runid。

• runid 用于主从复制(Replication),让从节点(Replica)知道自己上次同步的是哪个主节点(Master)。


2. runid 的作用

从节点在断线后重新连接主节点时,会使用 runid 来判断:

1. 如果 runid 相同

• 说明主从节点之前已经同步过,并且主节点没有重启过。

• 这时,Redis 尝试进行部分复制(psync 增量同步),只同步缺失的数据,提高效率。

2. 如果 runid 不同

• 说明主节点 重启过 或者 从节点之前连接的不是当前这个主节点

• 这时,Redis 只能执行全量同步(full resync),把主节点的所有数据重新发送给从节点。


3. 如何查看 runid?

你可以用 INFO 命令查看当前 Redis 实例的 runid:

127.0.0.1:6379> INFO SERVER
# 返回的信息中会有:
run_id:3b1f8c473f9a4c6d98e7b3a1d7c49f3a6

这个 run_id 就是当前 Redis 实例的唯一标识符。

什么叫fork一个子进程?

什么是 fork 一个子进程?

fork 是操作系统(主要是 Linux/Unix)中的一个系统调用,它的作用是创建一个新的进程,这个新进程被称为 子进程,它几乎是父进程的一个复制品


fork 的工作原理

当一个进程调用 fork() 时:

1. 操作系统会复制当前进程的所有资源(代码、数据、文件描述符等)。

2. 创建一个新的进程(子进程),它与原来的进程(父进程)几乎一样,但有自己独立的 进程 ID

3. 父进程和子进程同时运行,但子进程从 fork() 之后的代码开始执行。

在 Redis 里,为什么要 fork?

• 在 Redis 进行 RDB 持久化(bgsave)或者 AOF 重写(BGREWRITEAOF) 时,Redis 不会在主进程中直接进行快照操作,否则 Redis 在写入磁盘时,主进程会被阻塞,无法处理客户端请求

• 于是 Redis 使用 fork() 创建一个子进程,让子进程去执行持久化,而父进程继续处理请求,这样可以避免影响主线程的性能。

如果fork了一个子进程的话,redis还是单线程的吗?还是单线程只是指用户发起命令之后的反应是单线程的?

你的问题非常棒! Redis 在 fork 了子进程之后,它还是单线程的吗?


Redis 的“单线程”是什么意思?

1. Redis 主线程(主进程)是单线程的,它只用一个线程来处理客户端请求(SET、GET、DEL 等)。

2. 但 Redis 并不完全是“全局单线程”,它会在某些任务上使用额外的线程,比如:

持久化(BGSAVE、BGREWRITEAOF):通过 fork() 创建子进程来执行。

I/O 处理(Redis 6.0+):使用多个后台线程来处理磁盘 I/O 相关操作。


重点回答:fork 了子进程后,Redis 还是单线程吗?

Redis 处理客户端请求的部分仍然是单线程的!

fork 出来的子进程是独立的,它不会影响主线程的“单线程”特性。

主进程仍然是单线程,只是额外开了一个进程来执行 RDB 或 AOF 操作。

Redis 的“单线程”指的是

只有一个主线程处理客户端的请求,但 Redis 可能会有多个子进程或后台线程执行其他任务,比如持久化、I/O 操作


具体流程:Redis fork 之后的行为

场景 1:Redis BGSAVE(持久化快照)

当你执行:

BGSAVE

Redis 主进程仍然是单线程的,但它会fork 一个子进程来保存 RDB 快照:

主进程(单线程): 继续处理客户端请求
子进程(RDB 持久化): 写入 RDB 文件

主进程 继续响应 GET / SET 请求,不受影响。

子进程 独立运行,把当前数据写入 RDB 文件。

场景 2:Redis BGREWRITEAOF(AOF 重写)

当你执行:

BGREWRITEAOF

Redis 也会 fork 一个子进程来执行 AOF 重写:

主进程(单线程): 继续处理请求
子进程(AOF 重写): 优化 AOF 日志文件

主进程 依然是单线程处理请求。

子进程 读取旧 AOF 文件,生成更小的 AOF 文件。

场景 3:Redis 6.0+ 以后,后台 I/O 线程

Redis 6.0 之后,引入了多线程 I/O

• Redis 现在可以使用多个线程来处理网络 I/O 相关操作,如 write() 和 read()。

但执行命令的逻辑仍然是单线程的,多个 I/O 线程只是帮助 Redis 读取客户端数据、发送响应,提高吞吐量。

io-threads 4  # Redis 6.0+ 支持多线程 I/O

这个配置让 Redis 能用 4 个 I/O 线程来处理网络请求,但 命令执行依然在主线程里进行


结论

1. Redis 的核心执行逻辑(处理 GET / SET 命令)仍然是单线程的!

2. fork 出来的子进程不会影响 Redis 的“单线程”特性,子进程只负责 RDB / AOF 任务。

3. Redis 6.0+ 引入了多线程 I/O,提高了网络吞吐量,但命令执行仍然是单线程的。

Redis 之所以说自己是“单线程”,只是指“处理命令的主线程是单线程的”,但它可以使用子进程或后台线程来优化其他任务!

你可能感兴趣的:(redis,数据库,缓存)