Redis1️⃣NoSQL 数据库 & Redis 简介
1、NoSQL 数据库
non-relational SQL,也称 Not Only SQL
泛指非关系型数据库,作为关系型数据库的补充。
作用:应对基于海量用户和海量数据前提下的数据处理问题。
- 降低 CPU 及内存压力。
- 降低 I/O 压力。
1.1、对比 SQL
SQL | NoSQL | |
---|---|---|
结构化 | ✔ | ❌ |
存储方式 | 基于业务逻辑(表结构,约束) | 基于 K-V 模式 |
存储位置 | 磁盘 | 内存 |
事务 | ACID 强一致性 | BASE 弱一致性 |
扩展性 | 垂直(主从集群) | 垂直(主从集群),水平(分片集群) |
适用场景 | 要求强事务支持,即席查询(按需求添加查询条件) | 高并发读写,海量读写,高可扩展性要求 |
说明:
- NoSQL 数据存储在内存中,查询效率远高于 SQL 数据库。
- SQL 本身不支持水平扩展,但可借助第三方工具进行分库分表。
1.2、常见 NoSQL
Memcache | Redis | MongoDB | |
---|---|---|---|
存储形式 | K-V 型 | K-V 型 | 文档型 |
说明 | 早期 NoSQL 数据库,Redis 的雏形 | 涵盖了 Memcache 的大部分功能 | 模式自由的文档型数据库 |
数据存储 | 内存 | 内存 | 内存 |
持久化 | 不支持 | 支持 (用于备份恢复) |
支持 (内存不足时,将不常用数据保存到硬盘) |
数据类型 | 简单 K-V 模式 (字符串) |
丰富的 K-V 模式 (Redis 数据类型) |
对 Value 提供丰富的查询功能(尤其是 JSON),支持二进制数据和大型对象 |
其它常见 NoSQL:
HBase(列类型),Neo4j(图类型)
2、Redis
2.1、简介
Redis(Remote Dictionary Server)
远程词典服务器
基于内存的开源 K-V 型存储系统,具备高性能和高并发的特点。
- 基于内存
- 存储形式:全局链式哈希表,K-V 型。
- Key:通常是字符串类型,且支持设置过期时间。
- Value:支持多种数据结构。
- 数据的操作指令具有原子性。
- 线程模型:
- 单线程:保证线程安全,不会因线程切换而影响性能(Redis 6 引入网络多线程)
- I/O 多路复用:epoll,解决并发需求。
- 存储:数据存储在内存中,支持持久化。
- 集群:支持搭建主从集群(垂直),分片集群(水平)。
- 渐进式扩容:Redis 默认两张全局哈希表。
- 数据存储在第一张,需要扩容时使用第二张,并将第一张的元素映射到第二张。
- 请求分摊:每处理一次请求,就将一个哈希桶的元素映射到另一张表,每个请求分摊了哈希映射的时间。
- 定时器:周期性地进行数据的重新映射。
- 支持多语言客户端。
2.2、安装
3、线程模型
在 Redis 6 之前,单线程 + IO 多路复用
- 多路:多个 socket 连接
- 复用:一个线程
文件事件处理器
File Event Handler(FEH)
基于 Reactor 模式开发的、单线程的网络事件处理器
组成
-
多个 Socket(不同客户端请求会建立不同的 Socket)
- 连接应答(accept)
- 读取(read)
- 写入(write)
- 关闭(close)
-
IO 多路复用程序
-
文件事件分派器
-
事件处理器
-
连接应答处理器:客户端连接 redis
-
命令请求处理器:客户端要写数据到 redis
-
命令回复处理器:客户端要从 redis 读数据
-
处理流程
-
监听多个 Socket,根据 Socket 需要执行的操作产生事件。
-
将产生事件的 Socket 依次放入队列,有序、同步地发送给文件事件分配器。
(处理完成后才发送下一个 Socket)
-
根据事件类型为 Socket 绑定事件处理器。
-
调用绑定好的事件处理器来处理事件。
一次通信过程
-
Redis 启动初始化时,将连接应答处理器跟
AE_READABLE
事件关联。 -
若一个客户端(redis-cli)发起连接,会产生一个
AE_READABLE
事件。- 由连接应答处理器负责和客户端建立连接,创建客户端对应的 Socket
- 同时将这个 Socket 的
AE_READABLE
事件和命令请求处理器关联。 - 使客户端可以向主服务器发送命令请求。
-
当客户端向 Redis 发请求时(读/写)
- 客户端对于的 socket 会产生一个
AE_READABLE
事件,触发命令请求处理器。 - 处理器从 Socket 读取客户端请求, 传给相关程序执行。
- 客户端对于的 socket 会产生一个
-
当 Redis 服务器准备好给客户端的响应数据后,将 socket 的
AE_WRITABLE
事件和命令回复处理器关联。 -
当客户端准备好读取响应数据时
-
在 socket 产生一个
AE_WRITABLE
事件 -
由对应命令回复处理器处理,将准备好的响应数据写入 socket,供客户端读取。
-
全部数据写完后,删除该 socket 的
AE_WRITABLE
事件和命令回复处理器的关联。
-
实现技术
select
过程:同时监控多个 fd(file descriptor, 文件描述符)
- 调用时会阻塞,OS 将用户进程加入到所有资源的等待队列中。
- 当其中有 fd 就绪(可读/可写/except)、超时(timeout),函数返回。
- 就绪:返回可读、可写的文件描述符个数
- 超时:返回 null。
- 函数返回后
- 中断程序唤起用户线程。
- 用户可以遍历所有 fd,通过 FD_ISSET 判断具体哪个 fd 收到数据,做出相应处理。
特点:实现简单,但开销大。
- 遍历:
- 每次调用 select 都需要将进程加入到所有监视 fd 的等待队列(遍历)
- 每当函数返回,将就绪的 fd 写入 fd_set 中,将整个 fd_set 传给内核,拷贝到用户空间。(拷贝)
- 每次唤醒都需要从每个队列中移除(遍历)。
- 为了知道哪个 fd 收到数据,需要遍历所有监视 fd(遍历)。
- 大小限制:fd_set 的大小有限
- 32 位系统最多监听 1024 个
- 64 位系统最多监听 2048 个。
poll
对比 select
- 相同:拷贝全部监听的 fd,即 fd_set。
- 不同:
- fd_set 是链表结构,不受数量限制。
- 函数返回后,通过 pollfd 结构处理就绪的 fd。
缺点:同 select
- 函数返回后,需要遍历 pollfd 来获取就绪的 fd。
- 而实际上,连接的客户端在同一时刻可能只有很少处于就绪状态,随着 fd 数量增加,性能线性下降。
epoll(最新)
(Linux 特有)使用一个 fd 管理多个 fd
将用户进程监控的 fd 的事件存放到内核的一个事件表中,
这样在用户空间和内核空间只需拷贝一次。
- epoll_create:创建一个 epoll 的句柄。
- 参数 size 并非限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
- epoll 句柄占用一个 fd 值,使用完必须关闭,避免 fd 耗尽。
- epoll_ctl:事件注册函数,将需要监听的事件和需要监听的 fd 交给 epoll 对象。
- 事件:添加(EPOLL_CTL_ADD),删除(EPOLL_CTL_DEL),修改(EPOLL_CTL_MOD)
- 结构:红黑树。避免重复添加,性能高 O(lognN)
- 事件说明:
- 事件一旦添加,触发回调关系的建立。
- 事件一旦发生,调用回调函数 ep_poll_callback,将事件对应 fd 的 epitem 添加到 rdlist 双向链表。
- epoll_wait:
- 等待 io 事件,当回调函数被调用时会唤醒该进程。
- 检查 rdlist 双向链表是否有注册事件,拷贝到 txlist,清空 rdlist。
- ep_send_events 函数:
- 遍历 txlist,调用每个 epitem 关联的 fd 的 poll
- 目的是取得 fd 上较新的 events,发送到用户空间(封装在struct epoll_event)
- 返回 events
工作模式
- LT(level triggered):水平触发,默认
- 当 epoll_wait 检测到 fd 事件发生,将事件通知应用程序
- 应用程序可以不立即处理该事件。
- 下次调用 epoll_wait 时会再次通知。
- ET(edge-triggered):边缘触发(减少 epoll 事件重复触发)
- 应用程序必须立即处理该事件。
- 如果不处理,下次调用 epoll_wait 时不会再次通知。
对比
- select 和 poll 基本一致,
- select 采用数组存储,监听数量有限。
- poll 采用链表存储,不受限制。
- select、poll、epoll 都会返回就绪 fd 数量。
- select 和 poll 不会明确指出就绪 fd,需要遍历所有监听 fd。
- epoll 会,可直接处理即可。
- 检查 fd 就绪
- select、poll:轮询,随着 fd 增加 性能降低,
- epoll:通知 + 回调,不会 fd 数量影响,除非活跃 socket 很多。
- 工作模式:epoll 支持 ET,效率高。
- 拷贝:
- select、poll 需要将有关 fd 的数据结构拷贝进内核,最后再拷贝出来。
- epoll:有关数据结构本身就存于内核态中,利用 mmap() 文件映射,减少开销。
Redis 6 多线程
网络 IO 多线程
只处理 Socket 读写,执行命令仍是单线程。