【Redis】C++ 自定义客户端

目录

[1 认识 Redis 的应用层协议 - RESP 协议](#1 认识 Redis 的应用层协议 - RESP 协议)

[2 安装 Redis-plus-plus 库](#2 安装 Redis-plus-plus 库)

[3 使用 Redis-plus-plus 库](#3 使用 Redis-plus-plus 库)

[3.1 基础使用](#3.1 基础使用)

[3.2 全局指令对应的函数](#3.2 全局指令对应的函数)

[set 与 get 函数](#set 与 get 函数)

[mset 与 mget 函数](#mset 与 mget 函数)

[exists 函数](#exists 函数)

[del 函数](#del 函数)

[keys 函数](#keys 函数)

[expire 与 ttl 函数](#expire 与 ttl 函数)

[type 函数](#type 函数)

[3. 3 list 类型相关函数](#3. 3 list 类型相关函数)

[blpop 函数](#blpop 函数)

[3.4 set 类型相关函数](#3.4 set 类型相关函数)

[smembers 函数](#smembers 函数)

[3.5 zset 类型相关函数](#3.5 zset 类型相关函数)

[zrange 函数](#zrange 函数)


1 认识 Redis 的应用层协议 - RESP 协议

Redis 的 RESP 协议(RE dis S erialization Protocol)是客户端与 Redis 服务器之间通信的应用层协议。它设计简单、易解析、效率高,并且是二进制安全的。从 Redis 2.0 开始,RESP 成为标准通信协议;Redis 6.0 引入了 RESP3,增加了更多数据类型和特性。

RESP 的设计目标

  • 简单:人类可读,便于调试(可用 telnet 直接测试)。

  • 高效:解析器实现简单,减少客户端/服务端 CPU 开销。

  • 二进制安全:可以传输任意字节,不依赖特定编码。

  • 可扩展:RESP3 支持更多类型,如 Map、Set、Push 等。

RESP 的数据类型(RESP2)

RESP2 中,所有消息由以下 5 种基本类型组成,通过第一个字节区分类型:

类型 前缀 说明 示例
Simple String + 简单字符串,不含 CRLF +OK\r\n
Error - 错误信息 -ERR unknown command\r\n
Integer : 整数 :1000\r\n
Bulk String $ 二进制安全字符串 $6\r\nfoobar\r\n
Array * 数组,可嵌套 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

2 安装 Redis-plus-plus 库

虽然 RESP 有各种数据类型,但实际编程时不需要我们手动的去构建,已经有成熟的第三方库对此进行了封装,比如 Redis-plus-plus 库,要安装 Redis-plus-plus 库,得先安装 hiredis 库,因为 Redis-plus-plus 库依赖 hiredis 库

Ubuntu:apt install libhiredis-dev

Centos:yum install hiredis-devel.x86_64

下载redis-plus-plus 源码:git clone https://github.com/sewenew/redis-plus-plus.git

Ubuntu 直接使用 cmake 构建,Centos 自带的 cmake 版本较低,需要先安装 cmake3

bash 复制代码
cd redis-plus-plus
mkdir build
cd build
cmake3 ..
make
make install

构建成功后,会在 系列头⽂件. 会在 /usr/local/include/ 中多出 sw ⽬录,并且内部包含redis-plus-plus的⼀ /usr/local/lib/ 中多出⼀系列 libredis 库⽂件

3 使用 Redis-plus-plus 库

下面只介绍部分指令对应的函数,其余的指令对应的函数可以通过学习介绍的函数举一反三

  • 当一个函数,参数需要传递多个值的时候,往往都是支持 初始化列表 或者是 一对迭代器 的方式,来进行实现的
  • 当一个函数,返回值需要表示多个数据的时候,也往往会借助 插入迭代器,来实现往一个容器中添加元素的效果
  • 当某些场景涉及到无效值的时候,往往会搭配 std :: optional来进行使用.

3.1 基础使用

在你的 C++ 代码中,包含头文件并创建 Redis 对象即可开始操作:

cpp 复制代码
#include <sw/redis++/redis++.h>

// 创建 Redis 对象并连接本地 Redis
auto redis = sw::redis::Redis("tcp://127.0.0.1:6379");

// 测试连接
std::string result = redis.ping();  // 返回 "PONG"

Redis 对象内部维护连接池,可以安全地在多线程环境中共享使用。连接是延迟创建的,只有首次发送命令时才会真正建立 TCP 连接。

现在要使用 makefile 编译上面的代码,在编译之前要先寻找 libredis++.a 和 libhiredis.a 这两个静态库在哪里:

bash 复制代码
sudo find /usr/ -name libhiredis*
sudo find /usr/ -name libredis*

然后在 makefile 里显示的指明第三方库的具体路径(/usr/local/lib64/不在默认搜索路径,不能使用 -l 选项指明):

复制代码
hello:hello.cc
	g++ $^ -o $@  /usr/local/lib64/libredis++.a /usr/local/lib/libhiredis.a -lpthread -std=c++17

.PHONY:clean
clean: 
	rm -rf hello

并注意使用 C++17 标准编译

3.2 全局指令对应的函数

set 与 get 函数

set 的函数原型:

cpp 复制代码
ool Redis::set(const StringView &key,
                    const StringView &val,
                    const std::chrono::milliseconds &ttl,
                    UpdateType type)

StringView 类型是什么:

StringView 是 redis-plus-plus 中用于表示只读字符串视图的类型。它的设计目的是为了避免不必要的字符串拷贝,提升性能。

本质与目的

StringView 本身不拥有字符串数据,它只是一个"视图",内部包含一个指向字符数据的指针和一个长度。这和你把 std::string 传给函数时可能发生的拷贝不同,StringView 只是引用了已有的内存,因此传递效率很高。

它的角色类似于 C++17 标准库中的 std::string_view。

为什么需要它?

这涉及 redis-plus-plus 对 C++ 标准版本的兼容策略。

  • 当 redis-plus-plus 使用 C++17 或更高标准编译时 ,它内部的 StringView 实际上就是 std::string_view 的别名(或直接使用后者)。

  • 当 redis-plus-plus 使用 C++11 或 C++14 编译时 ,由于标准库还没有 std::string_view,redis-plus-plus 就提供了自己实现的 StringView 作为替代品,保证 API 在不同标准下保持一致。

get 的函数原型:

cpp 复制代码
OptionalString Redis::get(const StringView &key)

OptionalString 类型是什么:

OptionalString 是 redis-plus-plus 库定义的一个类型别名,它本质上是一个"可能包含值,也可能为空"的字符串包装器

它解决什么问题?

在 Redis 中,GET 一个不存在的键时,服务器会返回 nil(空值)。如果 get 函数直接返回 std::string,就无法区分"值是空字符串"和"键根本不存在"这两种情况。而 C++ 的 std::optional 正好能表达"有值/无值"的语义。

它的真实身份是什么?

OptionalString 的具体类型取决于你编译 redis-plus-plus 时使用的 C++ 标准:

  • C++17 模式下 :它就是 std::optional<std::string> 的别名。

  • C++11/14 模式下 :因为标准库还没有 std::optional,所以 redis-plus-plus 提供了一个自定义的 Optional 模板类来实现同样功能。

如何正确使用它?

拿到 OptionalString 后,必须先判断它是否包含有效值 ,再取值。直接取值一个空的 optional 会抛出 std::bad_optional_access 异常。

cpp 复制代码
auto val = redis.get("key");

// 正确做法:先判断
if (val) {  // OptionalString 可以隐式转换为 bool
    std::cout << *val << std::endl;  // 用 * 或 val.value() 解引用
} else {
    // 键不存在
}

get 与 set 的使用示例:

cpp 复制代码
#include <sw/redis++/redis++.h>
#include <iostream>

int main()
{
    auto redis = sw::redis::Redis("tcp://127.0.0.1:6379");
    redis.flushall();

    redis.set("key1","111");
    redis.set("key2","222");
    redis.set("key3","333");

    auto value1 = redis.get("key1");
    if(value1) std::cout << "value1=" << value1.value() << std::endl;

    auto value2 = redis.get("key2");
    if(value2) std::cout << "value2=" << value2.value() << std::endl;

    auto value3 = redis.get("key3");
    if(value3) std::cout << "value3=" << value3.value() << std::endl;

    return 0;
}

使用 set 设置过期时间

第三个参数 ttl 接收一个 std::chrono::milliseconds 对象,默认值为 0 毫秒。当 ttl 大于 0 时,键将在指定时间后自动过期。

使用示例:

cpp 复制代码
#include <sw/redis++/redis++.h>
#include <chrono>

using namespace std::chrono_literals; // 启用 10s, 5min 等字面量

auto redis = sw::redis::Redis("tcp://127.0.0.1:6379");

// 设置 "session:key" 的值为 "data",并在 10 秒后过期
redis.set("session:key", "data", std::chrono::seconds(10)); 
// 或者使用字面量
redis.set("session:key", "data", 10s);

// 也可以使用其他时间单位,例如 500 毫秒
redis.set("temp:key", "value", 500ms);

NX 和 XX 选项设置

set 的第四个参数 UpdateType 是一个枚举类型,定义在 sw::redis 命名空间下。

  • sw::redis::UpdateType::NOT_EXIST :对应 Redis 的 NX (if Not eXists)语义,仅当键不存在时才执行设置操作。

  • sw::redis::UpdateType::EXIST :对应 Redis 的 XX (if eXists)语义,仅当键已经存在时才执行设置操作。

  • sw::redis::UpdateType::ALWAYS:默认行为,无条件设置(覆盖旧值)。

这个选项不能单独使用 ,必须搭配 set 的另一个重载版本,也就是带有过期时间参数的那个版本。过期时间参数设为 0s 表示不设置过期时间。

cpp 复制代码
// 对应 Redis 的 SET key val NX
redis.set("key", "val", 0s, sw::redis::UpdateType::NOT_EXIST);

// 对应 Redis 的 SET key val XX
redis.set("key", "val", 0s, sw::redis::UpdateType::EXIST);

当使用了 NOT_EXIST 或 EXIST 选项时,set 函数的返回值 bool 变得有意义了:

  • 返回 true:条件满足,设置成功。

  • 返回 false:条件不满足(例如 NX 时键已存在,或 XX 时键不存在),操作被忽略,Redis 返回空回复(NULL REPLY)。

mset 与 mget 函数

mset 函数原型:

cpp 复制代码
// 使用初始化列表
template <typename T>
void mset(std::initializer_list<T> il);

// 使用迭代器范围
template <typename Input>
void mset(Input first, Input last);

使用示例:

方法一:初始化列表(键值对数量固定时)

cpp 复制代码
redis.mset({
    std::make_pair("key1", "111"),
    std::make_pair("key2", "222"),
    {"key3", "333"}  // C++17 可以简化写法
});

方法二:迭代器(键值对动态生成时)

cpp 复制代码
std::vector<std::pair<std::string, std::string>> kvs = {
    {"key1", "111"},
    {"key2", "222"},
    {"key3", "333"}
};
redis.mset(kvs.begin(), kvs.end());

mget 函数原型:

cpp 复制代码
// 使用迭代器范围指定键,结果写入输出迭代器
template <typename Input, typename Output>
void mget(Input first, Input last, Output output);

// 使用初始化列表指定键
template <typename T, typename Output>
void mget(std::initializer_list<T> il, Output output);

使用示例

方法一:初始化列表

cpp 复制代码
std::vector<std::string> keys = {"key1", "key2", "key3"};
std::vector<OptionalString> values;

redis.mget(keys.begin(), keys.end(), std::back_inserter(values));

方法二:直接传初始化列表

cpp 复制代码
std::vector<OptionalString> values;
redis.mget({"key1", "key2", "key3"}, std::back_inserter(values));

exists 函数

redis-plus-plus 的 exists 函数用于检查一个或多个键是否存在。它的返回值不是简单的 bool,而是存在的键的数量 (long long 类型)

cpp 复制代码
// 检查单个键
long long exists(const StringView &key);

// 检查多个键(使用初始化列表)
long long exists(std::initializer_list<StringView> keys);

// 检查多个键(使用迭代器范围)
long long exists(Input first, Input last);

使用示例:

检查单个键:

cpp 复制代码
auto count = redis.exists("my_key");
if (count > 0) {
    // 键存在
}

批量检查多个键:

cpp 复制代码
auto count = redis.exists({"key1", "key2", "key3"});
// 如果 key1 和 key3 存在,返回 2

虽然 exists 返回的是 long long,但在只检查一个键时,你可以像判断 bool 一样使用它。不过更好的做法是明确与 0 比较,以体现语义的准确性。这与 expire、setnx 等返回 bool 的函数不同,exists 的返回值在批量操作时承载了"存在数量"的信息

del 函数

del 函数用于删除一个或多个键,其返回值是实际被删除的键的数量 (long long 类型)。

与 exists 类似,del 也提供了三种重载形式,以支持不同的传参方式

cpp 复制代码
// 删除单个键
long long del(const StringView &key);

// 删除迭代器范围内的多个键
template <typename Input>
long long del(Input first, Input last);

// 删除初始化列表中的多个键
template <typename T>
long long del(std::initializer_list<T> il);

使用示例:

删除单个键:

cpp 复制代码
long long deleted = redis.del("my_key");
// 如果 my_key 存在,deleted 为 1;不存在则为 0

批量删除多个键:

cpp 复制代码
// 使用初始化列表
long long count = redis.del({"key1", "key2", "key3"});

// 使用迭代器范围
std::vector<std::string> keys = {"key1", "key2", "key3"};
long long count = redis.del(keys.begin(), keys.end());

返回值的含义 :del 返回的是成功删除的键数量,而不是命令是否执行成功

keys 函数

函数原型

在 redis-plus-plus 中,keys 并不返回容器,而是要求你提供一个输出迭代器:

cpp 复制代码
template <typename Output>
void keys(const StringView &pattern, Output output);

基本用法

使用 std::back_inserter 将结果插入到 std::vector 中:

cpp 复制代码
std::vector<std::string> result_keys;
auto inserter = std::back_inserter(result_keys);
redis.keys("user:*", inserter);  // 查找所有以 "user:" 开头的键

这种设计是为了解耦:keys 只负责从 Redis 获取数据,而"数据存到哪里"由你通过迭代器决定,keys 不关心你用什么容器接收结果

expire 与 ttl 函数

expire 函数原型

cpp 复制代码
#include <chrono>

bool expire(const StringView &key, long long timeout);

bool expire(const StringView &key, const std::chrono::seconds &timeout);

这两个版本在功能上完全等价。传 60 和传 std::chrono::seconds(60) 效果一样,都是让键在 60 秒后过期。但是传 std::chrono::seconds(60) 语义更加清晰。更加推荐的做法,是启用 10s、1min 等等字面常量:

cpp 复制代码
#include <sw/redis++/redis++.h>
#include <chrono>

using namespace std::chrono_literals; // 启用 10s, 5min 等字面量

// ...

// 1. 给已存在的键设置 10 秒后过期
bool set = redis.expire("my_key", 10s); 
if (set) {
    // 设置成功
}

返回值:

  • 返回 true :表示过期时间设置成功 (服务端返回 1)。

  • 返回 false :表示键不存在 ,因此无法为其设置过期时间(服务端返回 0)

ttl 函数原型

cpp 复制代码
long long ttl(const StringView &key);

返回值:

返回值是一个 long long 类型的整数,需要根据其数值来判断键的状态:

  • 大于 0 :键存在,且返回值就是剩余的秒数。

  • 等于 -1 :键存在,但没有设置过期时间(永久有效)。

  • 等于 -2 :键不存在。

type 函数

函数原型

cpp 复制代码
std::string type(const StringView &key);

它的返回值是一个 std::string,表示键所存储值的数据类型

使用示例:

cpp 复制代码
std::string val = redis.type("my_key");

if (val == "string") {
    // 处理字符串类型
} else if (val == "none") {
    // 键不存在
}

3. 3 list 类型相关函数

blpop 函数

函数原型:

cpp 复制代码
OptionalStringPair blpop(const StringView &key, const std::chrono::seconds &timeout);

template <typename Input>
OptionalStringPair blpop(Input first, Input last, const std::chrono::seconds &timeout);
OptionalStringPair blpop(std::initializer_list<StringView> keys, const std::chrono::seconds &timeout);

返回值:

返回类型是 OptionalStringPair ,这是一个 std::optional<std::pair<std::string, std::string>> 的别名。当操作成功时,你可以通过 op->first 获取元素来自哪个列表键 ,通过 op->second 获取弹出的元素值 。如果超时且没有数据,返回空的 optional。

使用示例:

cpp 复制代码
#include <sw/redis++/redis++.h>
#include <chrono>
#include <iostream>

using namespace std::chrono_literals;

auto redis = sw::redis::Redis("tcp://127.0.0.1:6379");

// 从 list1 或 list2 的头部弹出元素,最多阻塞 5 秒
auto op = redis.blpop({"list1", "list2"}, 5s);

if (op) {
    std::cout << "元素来自: " << op->first 
              << ", 值为: " << op->second << std::endl;
} else {
    std::cout << "超时,没有数据" << std::endl;
}

3.4 set 类型相关函数

smembers 函数

函数原型:

复制代码
template <typename Output>
void smembers(const StringView &key, Output output);

使用示例:

你需要先创建一个容器(推荐 std::vector<std::string> 或 std::set<std::string>),然后用 std::back_inserter (使用 vector 时)或 std::inserter (使用 set 或 vector 时,因为 set 没有 push_back 方法,但是有 insert 方法)创建迭代器传入

复制代码
#include <sw/redis++/redis++.h>
#include <vector>
#include <iterator>

auto redis = sw::redis::Redis("tcp://127.0.0.1:6379");

std::vector<std::string> members;
redis.smembers("my_set", std::back_inserter(members));

// 遍历结果
for (const auto &m : members) {
    std::cout << m << std::endl;
}

// 如果希望结果自动去重且有序,可以使用 std::set 配合 std::inserter
std::set<std::string> members;
redis.smembers("my_set", std::inserter(members, members.end()));

3.5 zset 类型相关函数

zrange 函数

函数原型:

复制代码
// 只返回成员
template <typename Output>
void zrange(const StringView &key, long long start, long long stop, Output output);

// 返回成员及其分数(当 Output 是 pair<string, double> 的迭代器时自动启用)
template <typename Output>
void zrange(const StringView &key, long long start, long long stop, Output output);

redis-plus-plus 会根据你传入的输出迭代器类型,决定发送给 Redis 的原始命令是否带 WITHSCORES 选项

使用示例:

只获取成员(不带分数)

cpp 复制代码
std::vector<std::string> members;
redis.zrange("my_zset", 0, -1, std::back_inserter(members));
// 对应命令: ZRANGE my_zset 0 -1

获取成员及其分数

cpp 复制代码
std::vector<std::pair<std::string, double>> res_with_score;
redis.zrange("my_zset", 0, -1, std::back_inserter(res_with_score));
// 对应命令: ZRANGE my_zset 0 -1 WITHSCORES

当你把 std::back_inserter 指向一个 std::pair<std::string, double> 的容器时,redis-plus-plus 会自动加上 WITHSCORES 选项,返回的每个元素都带有对应的分数

相关推荐
bmxy小明同学7 小时前
2026-10-09-token与成本
java·前端·数据库
路远的数据库笔记7 小时前
Oracle报ORA-00020连接打满?从专用进程模型到静态参数,一次生产排查全记录
数据库·oracle·国产数据库·ora-00020·dba实战·sharedserver
瀚高PG实验室7 小时前
瀚高数据库序列介绍、使用及值不连续、重复写入等现象处理
数据库·postgresql·瀚高数据库
这个DBA有点耶7 小时前
PostgreSQL HOT更新:频繁更新表的索引膨胀根因与fillfactor调优
数据库·sql·postgresql
野生数据人7 小时前
日志平台运维速查表:巡检命令、阈值、参数,一张贴墙
大数据·数据库
数据库小学妹7 小时前
向量检索最小可运行实现:HNSW索引参数与召回率/延迟权衡
数据库·后端
发量惊人的中年网工8 小时前
SD-WAN 组网方案怎么选?跨地域与多分支企业的选型指南(含成本对比)
服务器·网络·数据库
xiaogai_gai8 小时前
物料主数据从ERP同步到电商中台:基于轻易云的编码映射与增量全量双轨实践
大数据·数据库
SelectDB8 小时前
信创适配速查手册:国产 CPU / OS 对照表、认证许可速查与部署检查项清单
大数据·数据库·数据分析
亚川楼宇自控系统数据中心厂家8 小时前
机房 3D 可视化技术:从平面监控到立体化管理的升级
大数据·数据库·人工智能