🔥 本文专栏:Redis
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录:
最难的不是做出决定的那一瞬间,而是在此后无数个想要放弃的深夜里,依然能够在心里再一次'选择'坚持。
思维导图
text
Redis 数据存储服务
│
├── C/S 网络服务模型
│ │
│ └── Client ← TCP → Redis Server
│
├── 逻辑数据库
│ │
│ ├── db0 / db1 / db2 ...
│ ├── 每个 DB 拥有独立 Key Space
│ └── SELECT 切换当前连接所使用的 DB
│
├── RESP 应用层协议
│ │
│ ├── Array
│ ├── Bulk String
│ ├── Simple String
│ ├── Error
│ └── Integer
│
└── C++ 客户端编程
│
├── hiredis
│ └── C Redis Client
│
└── redis-plus-plus
│
├── Redis 对象
├── SET / GET / EXISTS / KEYS
├── StringView
├── OptionalString
├── initializer_list
└── Output Iterator
引入
在此前的学习中,我们已经认识了 Redis 中常见的五种数据类型,并且围绕这些数据类型学习了大量 Redis 命令。
但是,当我们真正把 Redis 放到一个后端程序中使用时,仅仅会在 redis-cli 中输入命令显然是不够的。
这是因为,实际业务中的 Redis 读写通常并不是由人手动完成的,而是整个业务程序中的一个子模块。例如:
text
HTTP 请求到达业务服务器
↓
业务代码处理请求
↓
查询 Redis 缓存
↓
命中缓存 / 写入缓存
↓
继续完成后续业务逻辑
也就是说,在真正的工程中:
业务进程本身需要承担 Redis Client 的角色。
而一旦从"手动输入 Redis 命令"过渡到"程序自动访问 Redis",我们就会自然遇到几个问题:
text
Redis Server 内部的数据到底如何划分?
Client 与 Server 在网络上传输什么?
程序是不是需要自己 socket + connect + send + recv?
C++ 程序应该如何方便地操作 Redis?
接下来,本文就沿着这条链路逐步展开。
一、Redis Server 中的逻辑数据库
1. 从 MySQL Database 过渡到 Redis Database
此前学习 MySQL 时,我们已经接触过 database 这一概念。
对于 MySQL 来说:
text
MySQL Server
│
├── database_A
│ ├── table_1
│ └── table_2
│
└── database_B
├── table_3
└── table_4
一个 MySQL Database 可以理解为:
一组逻辑上相关的表所组成的逻辑容器。
而 Redis 同样是一个数据存储服务,因此 Redis 内部也存在"逻辑数据库"的概念。
但是 Redis 与 MySQL 的数据组织方式不同。
Redis 本身采用的是:
text
key → value
这样的 KV 存储模型,因此 Redis 的逻辑数据库内部并不存在 MySQL 中的"表"这一层结构,而是直接维护一个 Key Space:
text
Redis Server
│
├── db0
│ ├── name → "Tom"
│ ├── age → "20"
│ └── rank → ZSet Object
│
├── db1
│ ├── name → "Jerry"
│ └── ...
│
└── db2
这里可以建立一个非常重要的认识:
Redis Database 本质上可以理解为 Redis Server 内部划分出的一个逻辑 Key Space。
2. 每个逻辑数据库拥有自己的键空间
Redis 每个逻辑数据库之间的 Key Space 是相互独立的。
例如:
text
db0:
name → "Tom"
db1:
name → "Jerry"
虽然两个数据库中都存在 name 这个 key,但是二者并不会冲突。
这是因为客户端真正进行一次 Redis 操作时,完整的定位过程应该理解为:
text
当前客户端连接
↓
当前连接选择的逻辑数据库
↓
该数据库对应的 Key Space
↓
根据 key 查找对应 value
所以:
redis
GET name
并不是在整个 Redis Server 中全局查找 name,而是在:
text
当前连接所选择的数据库
中查找这个 key。
需要注意的是,这里的"数据库隔离"主要指的是:
text
不同 DB 拥有独立的 Key Space
它们并不是多个真正完全独立的 Redis Server。
这些逻辑数据库仍然属于同一个 Redis 实例,共享实例级别的资源以及持久化体系。
3. Redis Database 使用编号进行标识
Redis 并不像 MySQL 那样通过:
sql
CREATE DATABASE ...
DROP DATABASE ...
在运行过程中动态创建或者删除逻辑数据库。
对于传统 Redis 单机实例来说,逻辑数据库会按照编号进行标识:
text
db0
db1
db2
...
编号从 0 开始。
传统默认配置通常为:
text
databases 16
也就是:
text
db0 ~ db15
一共 16 个逻辑数据库。
因此,Redis 对一个逻辑数据库最直接的标识方式就是:
text
数据库编号
4. SELECT:切换当前连接使用的数据库
Redis 提供:
redis
SELECT index
来切换当前客户端连接所选择的逻辑数据库。
例如:
redis
SELECT 3
表示:
text
当前连接
↓
切换到 db3
之后该连接继续执行:
redis
SET name Tom
GET name
这些操作就都是针对 db3。
这里有一个非常重要的点:
SELECT 改变的是"当前连接"的数据库状态,而不是整个 Redis Server 当前使用的数据库。
也就是说:
text
Connection A → db0
Connection B → db3
Connection C → db5
可以同时存在。
新建立的 Redis 连接默认使用:
text
db0
如果某个连接执行:
redis
SELECT 3
那么这个状态只属于当前连接。
连接断开之后,再重新创建一个新连接:
text
新的 Connection
↓
重新默认使用 db0
并不会自动记住上一次连接曾经选择过 db3。
因此这里可以形成完整的心智模型:
text
Redis Server
↓
多个逻辑 Database
↓
Database 通过编号标识
↓
每个 Database 拥有独立 Key Space
↓
Client Connection 默认选择 db0
↓
SELECT n 修改当前连接选择的数据库
二、Redis 本质上仍然是一个网络服务程序
认识了 Redis Database 之后,我们继续回到 Redis 最根本的模型。
Redis 本质上是一个:
text
Client / Server
架构的网络服务程序。
在当前的学习模型下,可以先理解为:
text
多个 Client
↓
与 Redis Server 建立 TCP 连接
↓
Client 发送请求
↓
Redis Server 执行命令
↓
生成响应
↓
通过网络返回 Client
对于 Redis 来说,SET、GET、EXISTS 这些命令最终都不是某种"本地函数调用"。
它们真正发生的过程仍然是:
text
Redis Client
↓
构造网络请求报文
↓
TCP
↓
Redis Server
↓
解析命令
↓
访问内存中的 KV 数据
↓
构造响应报文
↓
TCP 返回 Client
既然涉及网络通信,就一定会涉及一个问题:
Client 和 Server 怎么知道网络中这一段字节流究竟表示什么?
这就需要应用层协议。
三、RESP:Redis Client 与 Server 的应用层协议
1. 为什么 Redis 需要自己的应用层协议
对于 TCP/IP 网络模型来说,IP、TCP 等协议已经由操作系统内核实现。
上层程序通常不需要自己重新实现:
text
IP 数据报如何路由
TCP 如何确认
TCP 如何重传
TCP 如何维护连接
这些事情。
但是 TCP 提供给应用程序的本质仍然只是:
text
一段连续的字节流
那么现在 Redis Client 想表达:
redis
SET name wangzhe
Redis Server 收到的并不是一个天然存在的:
text
Command 对象
key 对象
value 对象
而只是一段字节。
所以双方必须事先约定:
text
一条命令有几个参数?
每个参数多长?
某段数据是什么类型?
数据从哪里开始?
到哪里结束?
Redis 使用的这套序列化协议就是:
text
RESP
Redis Serialization Protocol
所以可以把整个过程理解为:
text
Redis Command
↓
RESP 编码
↓
网络字节流
↓
TCP
↓
Redis Server
↓
RESP 解析
↓
恢复出命令以及参数
2. RESP 并不是围绕 KV 设计的
由于 Redis 是一个 KV 存储服务,很容易认为 RESP 协议本身应该围绕:
text
key
value
来组织报文。
其实不是。
RESP 所看到的只是:
text
命令 + 参数
例如:
redis
SET name wangzhe
从 Redis 命令语义来看:
text
SET
key = name
value = wangzhe
但是 RESP 并不知道第二个字段叫 key,也不知道第三个字段叫 value。
从 RESP 角度,它只是:
text
[
"SET",
"name",
"wangzhe"
]
也就是:
一个由多个参数组成的数组。
至于每个参数在某条 Redis 命令中究竟代表什么,是 Redis 命令自身的语义,而不是 RESP 负责的事情。
3. RESP 中常见的数据类型标识
以 RESP2 为例,我们当前最值得认识的几种类型有:
text
* Array
$ Bulk String
+ Simple String
- Error
: Integer
这些符号本质上可以理解为:
类型标记。
Redis Server 在解析一段 RESP 字节流时,会根据第一个字节判断接下来应该按照什么规则解释数据。
4. Redis 请求:Array + Bulk String
对于客户端向 Redis Server 发送命令来说,最常见的结构就是:
text
Array
+
Bulk String
例如:
redis
SET name wangzhe
逻辑上首先表示为:
text
["SET", "name", "wangzhe"]
整个请求有 3 个元素,所以 RESP 首先编码:
text
*3\r\n
其中:
text
*
→ 当前是一个 Array
3
→ Array 中有 3 个元素
\r\n
→ RESP 中的 CRLF 分隔符
然后第一个参数:
text
$3\r\nSET\r\n
可以拆成:
text
$3\r\n
→ 接下来是一个 Bulk String
→ 数据长度为 3 字节
SET
→ 真正的数据内容
第二个参数:
text
$4\r\nname\r\n
第三个参数:
text
$7\r\nwangzhe\r\n
最终完整报文就是:
text
*3\r\n
$3\r\n
SET\r\n
$4\r\n
name\r\n
$7\r\n
wangzhe\r\n
如果不换行展示,真正字节流的逻辑结构就是:
text
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$7\r\nwangzhe\r\n
因此,这里需要建立一个非常重要的认识:
一个字符串参数经过 RESP 编码之后,其网络表示并不仅仅只有字符串的数据内容,还包含用于描述这段数据的协议元信息。
例如:
text
$3\r\nSET\r\n
这里:
text
$3\r\n
→ RESP 描述信息
SET
→ 实际数据内容
但是注意:
text
"$3"
并不是字符串 "SET" 本身的一部分。
它只是 Redis 为了让接收端能够正确解析 "SET" 而加入的协议控制信息。
5. Bulk String 与 Simple String
Bulk String
Bulk String 的基本格式是:
text
$<length>\r\n<data>\r\n
例如:
text
$5\r\nhello\r\n
这里:
text
5
→ 明确告诉接收端后面的实际数据有 5 个字节
这种方式有一个很重要的优势:
数据边界由长度决定,而不是简单依赖某个字符判断结束。
因此 Bulk String 可以承载更加通用的数据,甚至可以包含二进制内容。
Redis Client 发送命令参数时,大量使用的就是这种形式。
Simple String
Simple String 的格式更加简单:
text
+内容\r\n
例如:
text
+OK\r\n
它没有单独的长度字段,而是直接以 CRLF 作为结束。
所以 Simple String 更适合:
text
短小、简单、不包含 CR/LF 的文本响应
例如 SET 成功时常见的:
text
OK
6. Error 与 Integer
Redis RESP 中:
text
-
表示 Error。
例如:
text
-ERR unknown command\r\n
这类内容主要出现在 Redis Server 返回给 Client 的响应中,用来表示:
text
命令不存在
类型错误
语法错误
...
而:
text
:
则表示 Integer。
很多 Redis 命令的响应本身就是一个整数,因此会通过 RESP Integer 表达。
例如:
text
:1\r\n
就表示整数 1。
7. GET 不存在的 key 与 Null
这里还能顺便把后面 Optional 的设计连接起来。
假设执行:
redis
GET not_exist
如果 key 不存在,那么 Redis 并不能简单返回:
text
""
因为:
text
key 存在,value = ""
本身也是一种合法的数据状态。
所以 RESP 必须能够区分:
text
有一个空字符串
和:
text
根本没有值
在 RESP2 中,不存在的 Bulk String 可以表示为:
text
$-1\r\n
也就是 Null Bulk String。
这个"有值 / 没值"的语义,后面就会直接映射到 C++ 客户端库中的 OptionalString。
四、为什么实际开发不自己手写 RESP 与 Socket
如果完全按照底层流程来操作 Redis,那么 C++ 程序理论上完全可以自己实现:
text
socket()
↓
connect()
↓
按照 RESP 规则编码 Redis 命令
↓
send()
↓
recv()
↓
解析 RESP 响应
↓
close()
但是这种做法显然没有必要。
这就和此前使用 MySQL Client Library 是一样的。
真正业务开发中:
text
业务程序
↓
MySQL Client Library
↓
MySQL Server
对于 Redis,同样可以抽象为:
text
业务程序
↓
Redis Client Library
↓
Redis Server
Client Library 已经替我们完成:
text
连接管理
RESP 编码
网络发送
响应接收
RESP 解析
异常处理
所以上层业务代码只需要关心:
text
我要执行什么 Redis 操作?
参数是什么?
返回结果是什么?
五、hiredis 与 redis-plus-plus
1. hiredis:C 语言 Redis Client
hiredis 是一个 C 语言实现的 Redis 客户端库。
可以先把它理解为:
text
C 业务程序
↓
hiredis C API
↓
Redis Server
它负责完成 Redis Client 的基本行为,例如:
text
连接 Redis
发送命令
接收响应
解析 RESP
2. redis-plus-plus:对 hiredis 的 C++ 封装
对于 C++ 开发来说,我们还可以进一步使用:
text
redis-plus-plus
它本身就是一个 C++ Redis Client,并且底层基于 hiredis。
整个关系可以理解成:
text
C++ 业务代码
↓
redis-plus-plus
↓
hiredis
↓
RESP + TCP
↓
Redis Server
这和我们对 Linux Socket API 做 C++ 封装非常类似。
例如 Linux 原生提供:
cpp
socket();
connect();
recv();
send();
close();
如果希望更加符合 C++ 编程习惯,我们可能会自己封装:
cpp
class Socket {
// 封装 socket fd
// 检查系统调用返回值
// 析构时自动释放资源
};
于是:
text
Linux Socket C API
↓
C++ Socket Class
↓
业务代码
Redis 这里也是类似:
text
hiredis C API
↓
redis-plus-plus C++ 封装
↓
C++ 业务代码
redis-plus-plus 还会利用:
text
RAII
STL
异常
模板
连接池
C++ 类型系统
提供更加符合 C++ 编程习惯的接口。
因此,对于 C++ 后端程序来说,通常可以直接把学习重点放在:
text
redis-plus-plus
这一层。
至于 hiredis、redis-plus-plus 如何从源码编译安装,并不是这里最重要的内容,只需要先把开发环境配置好即可。
六、redis-plus-plus 的基本使用流程
首先引入:
cpp
#include <sw/redis++/redis++.h>
这个头文件向上层暴露 redis-plus-plus 的 C++ API。
最简单的代码可以写成:
cpp
#include <sw/redis++/redis++.h>
#include <iostream>
int main() {
sw::redis::Redis redis("tcp://127.0.0.1:6379");
redis.set("name", "wangzhe");
auto value = redis.get("name");
if (value) {
std::cout << *value << std::endl;
}
return 0;
}
这里整体可以先拆成:
text
创建 Redis 对象
↓
提供 Redis Server 连接信息
↓
调用成员函数执行 Redis 命令
↓
redis-plus-plus 内部管理连接
↓
hiredis + RESP + TCP
↓
Redis Server
↓
响应返回
↓
redis-plus-plus 转换为 C++ 类型
1. Redis 对象与连接 URI
首先:
cpp
sw::redis::Redis redis("tcp://127.0.0.1:6379");
这里字符串:
text
tcp://127.0.0.1:6379
包含:
text
tcp
→ 使用 TCP
127.0.0.1
→ Redis Server 地址
6379
→ Redis Server 端口
所以构造 Redis 对象时,相当于告诉客户端:
后续应该连接到哪一个 Redis Server。
不过这里需要注意:
构造
Redis对象,并不代表底层一定立刻创建 TCP 连接。
redis-plus-plus 的连接可以采用 Lazy Connection,也就是延迟建立:
text
构造 Redis 对象
↓
保存连接配置
↓
暂时不一定 connect
↓
真正发送 Redis 命令
↓
按需创建 / 获取底层连接
因此,上层只需要使用 Redis 对象即可,不需要自己关心某一次底层连接究竟在什么时刻创建。
七、SET:从 Redis 命令映射到 C++ 成员函数
此前使用 redis-cli 时,我们可能执行:
redis
SET name wangzhe
到了 redis-plus-plus 中,它就变成:
cpp
redis.set("name", "wangzhe");
也就是说:
Redis 原生命令被进一步封装成了 Redis 对象的成员函数。
set() 一个常见接口可以理解为:
cpp
bool set(const StringView &key,
const StringView &val,
const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0),
UpdateType type = UpdateType::ALWAYS);
其参数与 Redis SET 命令的语义基本对应:
text
key
→ 键
val
→ 值
ttl
→ 过期时间
type
→ 更新条件
1. key 与 value:StringView
这里前两个参数使用:
cpp
StringView
为什么不直接规定必须传递:
cpp
std::string
呢?
首先需要重新认识一下 string_view。
一个 string_view 可以建立这样的心智模型:
text
string_view
├── 字符串数据起始位置
└── 字符串长度
例如:
cpp
std::string str = "hello";
std::string_view sv = str;
可以概念化理解为:
text
sv.data() → str 中 'h' 的位置
sv.size() → 5
这里 sv 并没有重新拷贝:
text
"hello"
它只是获得了原字符串的一段只读视图。
因此:
text
std::string
→ 拥有字符串数据
std::string_view
→ 不拥有字符串数据
→ 只是观察一段字符区间
这可以减少不必要的字符串复制。
2. StringView 与 C++ 标准兼容
std::string_view 是 C++17 标准库正式提供的。
但是 redis-plus-plus 需要兼容不同 C++ 标准。
因此可以统一理解为:
text
C++17+
→ StringView 使用 std::string_view
较早标准
→ redis-plus-plus 提供兼容实现
这样 redis-plus-plus 的上层接口始终可以统一写:
cpp
StringView
而不用让业务代码关心底层究竟是哪一种实现。
3. 可以直接传 std::string
这里还有一个容易误解的点。
接口参数虽然是:
cpp
const StringView &key
但这并不意味着调用者必须先手动创建:
cpp
StringView
例如完全可以:
cpp
std::string key = "name";
std::string value = "wangzhe";
redis.set(key, value);
也可以:
cpp
redis.set("name", "wangzhe");
其本质可以理解为:
text
std::string / C String
↓
转换成 StringView
↓
获得字符串数据位置 + 长度
↓
传递给 set()
因此 StringView 的存在不是为了增加调用复杂度,而是:
让接口可以只读地观察各种字符串来源,并减少不必要的数据拷贝。
4. ttl:设置过期时间
第三个参数:
cpp
ttl
表示 key 的生存时间。
例如:
cpp
redis.set("name",
"wangzhe",
std::chrono::seconds(10));
可以理解为:
text
SET name wangzhe
+
设置 10 秒过期时间
如果 TTL 为 0ms,则表示不额外设置超时时间。
5. UpdateType:映射 NX / XX
第四个参数:
cpp
UpdateType
用于描述 SET 的条件更新行为。
可以对应:
text
UpdateType::ALWAYS
→ 无论 key 是否存在都 SET
UpdateType::NOT_EXIST
→ 对应 NX
→ key 不存在才设置
UpdateType::EXIST
→ 对应 XX
→ key 已经存在才设置
因此:
cpp
redis.set("name",
"wangzhe",
std::chrono::milliseconds(0),
sw::redis::UpdateType::NOT_EXIST);
可以理解为:
redis
SET name wangzhe NX
这里可以进一步总结:
text
NX
→ 更偏向"只插入"
XX
→ 更偏向"只更新"
ALWAYS
→ 存在就更新,不存在就插入
6. SET 的返回值
redis-plus-plus 的这一版 set() 返回:
cpp
bool
它表示:
text
本次 SET 是否真正成功完成
例如使用:
text
NX / XX
时,如果条件不满足,那么 set() 可以返回:
text
false
所以:
cpp
bool ok = redis.set(...);
并不是简单地表示"网络有没有报错",而是和当前 SET 条件是否成功执行有关。
八、GET:为什么返回 OptionalString
Redis 原生命令:
redis
GET key
对应 redis-plus-plus:
cpp
auto value = redis.get(key);
这里参数仍然是一个字符串形式的 key。
但是 GET 的返回值不能简单设计成:
cpp
std::string
原因在于必须区分:
text
情况一:
key 不存在
情况二:
key 存在,但是 value 本身就是空字符串 ""
如果两种情况都返回:
cpp
std::string("")
就无法区分。
因此 redis-plus-plus 使用:
cpp
OptionalString
来表示:
text
这个字符串值可能存在
也可能不存在
可以把它建立成类似:
cpp
std::optional<std::string>
的心智模型。
1. Optional 的核心语义
Optional 最核心解决的问题不是:
text
字符串是不是空
而是:
text
这个值到底存不存在
所以:
text
Optional 中存在 std::string("")
→ 仍然表示"有值"
Optional 本身为空
→ 才表示"没有值"
例如:
cpp
auto value = redis.get("name");
if (value) {
std::cout << *value << std::endl;
}
这里:
text
if (value)
→ 判断 Optional 中有没有值
*value
→ 取出内部保存的 std::string
2. 为什么 if(value) 可以直接判断
Optional 类型会提供类似:
cpp
operator bool()
的能力。
所以:
cpp
if (value)
可以理解为:
text
value
↓
operator bool()
↓
内部存在值?
↓
true / false
需要再次强调:
text
判断的是"有没有对象"
而不是"字符串是不是空串"
3. operator*:取得内部值
如果 Optional 当前有值:
cpp
*value
就可以获得其内部保存的字符串。
可以理解为:
text
OptionalString
↓
operator*()
↓
std::string
因此:
cpp
if (value) {
std::cout << *value;
}
就是最直观的一组操作。
4. Optional 常见成员函数
除了:
cpp
if (value)
*value
之外,还可以认识几个常见成员函数。
has_value()
cpp
value.has_value()
表示:
text
Optional 当前是否存在值
所以:
cpp
if (value.has_value()) {
}
与:
cpp
if (value) {
}
语义接近。
value()
cpp
value.value()
用于取出内部保存的值。
但是如果 Optional 当前没有值,直接调用 value() 会失败并抛出对应异常。
因此通常应该先判断。
value_or()
cpp
value.value_or("default")
表示:
text
有值
→ 返回真实 value
没值
→ 返回给定默认值
例如:
cpp
std::string name = value.value_or("unknown");
九、EXISTS:单 key 判断与批量 key 判断
Redis:
redis
EXISTS key
用于判断 key 是否存在。
redis-plus-plus 中:
cpp
redis.exists("name");
对于单个 key:
text
存在
→ 返回 1
不存在
→ 返回 0
因此,在单 key 场景下可以从布尔语义理解它。
但是它的真实返回值并不是 bool,而是:
存在的 key 数量。
所以 Redis 还支持:
redis
EXISTS key1 key2 key3
如果三个 key 中有两个存在,则返回:
text
2
redis-plus-plus 也支持:
cpp
redis.exists({"key1", "key2", "key3"});
十、initializer_list:为什么可以直接传一组 key
当看到:
cpp
redis.exists({"key1", "key2", "key3"});
这里就会涉及 C++ 的:
cpp
std::initializer_list
可以先建立这样的心智模型:
text
{"key1", "key2", "key3"}
↓
编译器准备一组连续的初始化元素
↓
initializer_list
↓
提供对这一组元素的只读访问
这里 initializer_list 本身并不会再拥有一份完整的数据副本。
可以把它类比成前面学习的 string_view:
text
string_view
→ 对一段字符数据的只读视图
initializer_list
→ 对一组初始化元素的只读视图
从实现思维来看,可以概念化理解为:
text
initializer_list
├── 起始位置
└── 元素数量 / 结束位置
不过标准并不要求其内部必须真的按照"两个指针"实现。
所以最稳妥的理解仍然是:
initializer_list 是对编译器生成的一组连续初始化元素提供的轻量只读视图。
redis-plus-plus 内部可以进一步把:
cpp
redis.exists({"k1", "k2", "k3"});
转成类似:
text
begin()
+
end()
所描述的一段范围,然后依次处理这些 key。
因此:
text
{"k1", "k2", "k3"}
↓
initializer_list
↓
exists() 遍历
↓
构造 EXISTS k1 k2 k3
↓
Redis 返回实际存在数量
十一、KEYS:模式匹配与输出迭代器
此前我们已经学习过 Redis:
redis
KEYS pattern
它会在当前逻辑数据库中查找所有满足 pattern 的 key。
例如:
redis
KEYS user:*
可能返回:
text
user:1
user:2
user:3
KEYS 的时间复杂度是:
text
O(N)
因为需要遍历当前数据库中的整个 Key Space。
因此它并不适合在大规模生产数据上作为频繁扫描手段,这也是后面 SCAN 命令存在的重要原因。
1. redis-plus-plus 中的 keys()
redis-plus-plus 的接口可以理解为:
cpp
template <typename Output>
void keys(const StringView &pattern, Output output);
第一个参数:
cpp
pattern
已经比较熟悉:
text
表示 Redis KEYS 命令中的匹配模式
第二个参数:
cpp
output
则是一个输出迭代器。
例如:
cpp
std::vector<std::string> result;
redis.keys("user:*", std::back_inserter(result));
整体过程可以理解为:
text
KEYS user:*
↓
Redis Server 返回多个 key
↓
redis-plus-plus 解析响应
↓
通过 Output Iterator
↓
依次写入 result
这里 keys() 本身不需要返回:
cpp
std::vector<std::string>
而是由调用者自己决定结果最终放进什么容器。
十二、重新认识 C++ Iterator
学习到这里,就自然遇到了:
cpp
std::back_inserter(result)
也就是所谓的插入迭代器。
在此之前,我们对于 Iterator 可以先保持一个很朴素的模型:
Iterator 是一种模拟指针行为的抽象对象。
很多迭代器都会提供类似:
cpp
*it
++it
it != end
的行为。
因此它们看起来和普通指针非常相似。
但是需要注意:
Iterator 并不一定只是"把一个普通指针封装进类里"。
例如:
text
vector
→ 数据连续
→ iterator 可以非常接近普通指针
list
→ 节点并不连续
→ iterator 必须知道如何从当前节点移动到下一个链表节点
所以更准确地说:
text
Iterator
→ 是满足特定迭代行为要求的一种对象
→ 通过运算符重载模拟指针式访问
1. 不同 Iterator 本质上是在描述不同能力
经典 Iterator 通常会按照能力进行分类,例如:
text
Input Iterator
Output Iterator
Forward Iterator
Bidirectional Iterator
Random Access Iterator
现在没有必要单独死记这些概念。
只需要先理解:
所谓不同迭代器类别,本质上是在描述这个 Iterator 支持哪些操作。
例如:
text
Input Iterator
→ 主要读取
Output Iterator
→ 主要写入
Bidirectional Iterator
→ 可以 ++
→ 可以 --
Random Access Iterator
→ 可以 +n / -n
→ 可以下标访问
十三、插入迭代器:把"赋值"转换为"插入"
std::back_inserter(container) 会生成一种特殊的:
text
Output Iterator
它的任务不是读取某一个已经存在的元素,而是:
text
向容器写入新的元素
例如:
cpp
std::vector<std::string> result;
auto it = std::back_inserter(result);
随后:
cpp
*it = "hello";
表面上看起来是:
text
给 *it 指向的位置赋值
但 back_insert_iterator 会重载相关操作,使这一行为最终转换成类似:
cpp
result.push_back("hello");
所以这里可以建立一条非常重要的链:
text
std::back_inserter(result)
↓
创建 back_insert_iterator
↓
这是一个 Output Iterator
↓
对 Iterator"赋值"
↓
转换成 result.push_back(...)
因此在:
cpp
redis.keys("*", std::back_inserter(result));
中,redis-plus-plus 可以不断把 Redis 返回的 key:
text
key1
key2
key3
...
写给这个输出迭代器。
而这个迭代器再负责:
text
result.push_back(key1)
result.push_back(key2)
result.push_back(key3)
...
最终结果自然就进入:
cpp
std::vector<std::string> result;
中。
十四、将整个 Redis Client 编程链路串起来
学习到这里,可以把此前零散的知识重新组织成一条完整链路。
假设业务程序执行:
cpp
redis.set("name", "wangzhe");
从最上层一路往下,其过程可以理解为:
text
C++ 业务代码
↓
Redis::set()
↓
redis-plus-plus
↓
调用/利用 hiredis
↓
将命令组织为 RESP
↓
["SET", "name", "wangzhe"]
↓
RESP 字节流
↓
TCP
↓
Redis Server
↓
当前 Connection 所选择的 Database
↓
对应 Database 的 Key Space
↓
查找 / 修改 key
↓
生成 RESP Response
↓
TCP 返回
↓
hiredis 解析
↓
redis-plus-plus 转换为 C++ 类型
↓
业务代码拿到返回结果
这条链路非常重要。
因为这样一来,我们就不会把:
cpp
redis.set(...)
看成一个孤立的 C++ API。
它背后依然是此前学习过的:
text
C/S 网络模型
+
TCP
+
应用层协议
+
数据库 Key Space
+
客户端库封装
只不过 redis-plus-plus 将这些底层过程隐藏了起来。
十五、本阶段知识总结
本文并没有继续学习某一种新的 Redis 数据类型,而是进一步认识了:
Redis 在真正的 C++ 后端程序中究竟是如何被使用的。
首先,Redis Server 内部存在多个逻辑数据库:
text
Redis Server
↓
db0 / db1 / db2 ...
↓
每个 DB 拥有自己的 Key Space
数据库通过编号标识,并通过:
redis
SELECT n
切换当前连接所操作的逻辑数据库。
随后,从 Redis 的 C/S 网络模型出发,我们认识到:
text
Client 与 Server
必须通过一套应用层协议
约定网络中字节流的组织方式
Redis 使用的就是:
text
RESP
对于最常见的客户端请求:
text
Redis Command
↓
Array
+
Bulk String
↓
RESP
↓
TCP 字节流
但实际业务开发中,我们并不需要自己:
text
socket
connect
RESP encode
send
recv
RESP decode
而是直接通过 Redis Client Library 完成。
对于 C++:
text
C++ 业务程序
↓
redis-plus-plus
↓
hiredis
↓
RESP + TCP
↓
Redis Server
最后,在学习 redis-plus-plus 的基础 API 时,又自然串联起了一系列 C++ 本身的语言特性:
text
SET
→ StringView
→ 只读字符串视图
GET
→ OptionalString
→ 表达"有值 / 无值"
EXISTS
→ initializer_list
→ 批量传入多个 key
KEYS
→ Output Iterator
→ back_inserter
→ 将 Redis 返回结果写入 STL 容器
所以这部分学习真正重要的并不是单纯记忆:
cpp
redis.set(...)
redis.get(...)
redis.exists(...)
redis.keys(...)
而是理解这些 API 为什么会设计成现在这种形式,以及它们在整个 Redis Client / Server 网络通信链路中分别处于什么位置。
这样后续再学习 Redis 的连接池、Pipeline、事务以及更加复杂的客户端操作时,就不会只是停留在"会调用接口"的层面,而是能够继续沿着:
text
上层 API
→ 客户端库
→ RESP
→ TCP
→ Redis Server
→ 数据结构
这条链路向下理解。
