【Redis 初阶】C++ 客户端实战:从 RESP 协议到 redis-plus-plus 工程化用法


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. RESP:Redis 通信的底层协议](#一. RESP:Redis 通信的底层协议)
    • [1.1 为什么需要应用层协议](#1.1 为什么需要应用层协议)
    • [1.2 请求与响应格式](#1.2 请求与响应格式)
  • [二. redis-plus-plus 环境搭建](#二. redis-plus-plus 环境搭建)
    • [2.1 安装依赖:hiredis](#2.1 安装依赖:hiredis)
    • [2.2 编译安装 redis-plus-plus](#2.2 编译安装 redis-plus-plus)
    • [2.3 编译配置(Makefile 示例)](#2.3 编译配置(Makefile 示例))
  • [三. 基础入门:连接与通用命令](#三. 基础入门:连接与通用命令)
    • [3.1 Hello World:建立连接](#3.1 Hello World:建立连接)
    • [3.2 接口设计的三个核心范式](#3.2 接口设计的三个核心范式)
    • [3.3 通用键命令](#3.3 通用键命令)
  • [四. 五大核心数据类型实战](#四. 五大核心数据类型实战)
    • [4.1 String 类型](#4.1 String 类型)
    • [4.2 List 类型](#4.2 List 类型)
    • [4.3 Set 类型](#4.3 Set 类型)
    • [4.4 Hash 类型](#4.4 Hash 类型)
    • [4.5 ZSet 类型](#4.5 ZSet 类型)
  • 结尾:

前言:

前面我们一直在 redis-cli 中通过命令行理解 Redis 的数据类型与底层原理,但真实业务开发最终都要落到代码层面。很多 C++ 开发者使用 Redis 客户端时只会机械调用 API,遇到网络异常、返回值异常、编译报错就无从下手,本质上是对通信协议、库的设计逻辑缺乏认知。本文顺着「底层协议 → 环境搭建 → 接口设计 → 全类型实战」的完整脉络,先拆解 RESP 序列化协议的核心设计,再系统讲解 redis-plus-plus 这个主流 C++ 客户端的安装、接口范式与五大类型常用操作,配合源码设计视角解读,带你从 "会用函数" 进阶到 "懂原理、能避坑"。


一. RESP:Redis 通信的底层协议

1.1 为什么需要应用层协议

Redis 基于 TCP 传输数据,但 TCP 本身只是面向字节流的传输层协议,没有业务语义。客户端发一串字节,服务端要能识别成哪条命令;服务端返回一串字节,客户端要能解析成对应结果 ------ 这就需要双方约定一套统一的数据格式规范,也就是 RESP(REdis Serialization Protocol)

和 HTTP 这类通用应用层协议不同,RESP 是 Redis 专门设计的序列化协议,也是所有客户端库的底层基础。它有几个鲜明特点:

  1. 实现简单,解析高效:格式规则极少,解析逻辑不复杂,性能开销很低;
  2. 肉眼可读:本质是文本协议,抓包后可以直接看懂内容,调试非常友好;
  3. 与传输层弱耦合:默认跑在 TCP 上,但理论上也能适配其他可靠传输层;
  4. 一问一答模型:客户端发送一条命令请求,服务端返回一个对应响应,模型简单清晰。

1.2 请求与响应格式

请求侧 :所有 Redis 命令最终都会被序列化为「批量字符串数组」的形式发送。比如 SET key value 会被编码成包含 3 个元素的数组,每个元素对应命令的一部分。

响应侧 :服务端根据命令类型返回不同类型的 RESP 数据,主要包括简单字符串、错误信息、整数、批量字符串、嵌套数组等。例如 GET 一个不存在的 key 会返回空批量字符串,SET 成功返回简单字符串 OKLLEN 返回整数。

实际开发中我们不需要手写协议解析,hiredis 等底层库已经把序列化和解析的工作封装好了。但理解 RESP 的价值在于:抓包排查线上问题、理解返回值的本质、甚至自研轻量客户端时,能做到心里有数。


二. redis-plus-plus 环境搭建

C++ 生态下有多个 Redis 客户端库,我们选用功能最完善、接口设计最现代的 redis-plus-plus。它基于 hiredis 做底层封装,完整支持 C++11/14/17,接口风格统一,是工业界常用的 C++ Redis SDK。

2.1 安装依赖:hiredis

redis-plus-plus 本身不做协议解析,底层依赖 C 语言实现的 hiredis 库处理网络通信与 RESP 编解码,需要先安装依赖:

bash 复制代码
# Ubuntu / Debian
apt install libhiredis-dev

# CentOS / RHEL
yum install hiredis-devel.x86_64

2.2 编译安装 redis-plus-plus

该库需要通过源码编译安装,采用标准 CMake 流程,建议使用 build 目录隔离编译产物,避免污染源码:

bash 复制代码
# 进入源码目录,创建编译目录
mkdir build && cd build

# 生成 Makefile
cmake ..

# 编译,-j 后接 CPU 核心数加快速度
make -j4

# 安装到系统目录
make install

安装完成后,核心文件分布:

  • 库文件:/usr/local/lib/ 下的 libredis++.a(静态库)与 libredis++.so(动态库);
  • 头文件:/usr/local/include/sw/redis++/,入口头文件为 redis++.h

2.3 编译配置(Makefile 示例)

编写业务代码时,编译链接需要包含三部分:redis++ 库、hiredis 库、pthread 线程库。一个最简 Makefile 如下:

makefile 复制代码
all: hello

hello: hello.cc
	g++ -std=c++17 -o $@ $^ \
		/usr/local/lib/libredis++.a \
		/usr/lib/x86_64-linux-gnu/libhiredis.a \
		-pthread

.PHONY: clean
clean:
	rm -rf hello

三. 基础入门:连接与通用命令

3.1 Hello World:建立连接

核心类是 sw::redis::Redis,构造函数传入服务端地址,格式为 tcp://IP:端口

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

int main() {
    // 连接本地 Redis 服务,默认端口 6379
    sw::redis::Redis redis("tcp://127.0.0.1:6379");

    // ping 检测连通性
    std::string res = redis.ping();
    std::cout << res << std::endl; // 输出 PONG

    return 0;
}

编译运行后输出 PONG,即代表连接正常。

3.2 接口设计的三个核心范式

redis-plus-plus 的接口设计高度统一,掌握这三个规律,所有命令都可以举一反三,不用死记硬背。

1. 只读参数使用 StringView

类似 C++17 标准库的 std::string_view,它是只读字符串视图,避免不必要的字符串拷贝,针对只读场景做了性能优化。所有输入的 key、member 等只读参数,都采用这个类型。

2. 空值语义用 Optional 表达

对于可能返回 nil 的命令(比如 get 不存在的 key),库中使用 std::optional 包装返回值,例如 OptionalString

⚠️ 重要踩坑点:不要直接调用 .value() 取值 。如果 optional 处于无效状态,直接取值会抛出 std::bad_optional_access 异常,导致程序崩溃。 正确做法是先通过 if 判断,optional 可隐式转换为 bool 类型:

cpp 复制代码
auto val = redis.get("not_exist_key");
if (val) {
    std::cout << "值为:" << val.value() << std::endl;
} else {
    std::cout << "key 不存在" << std::endl;
}

3. 多结果返回使用输出迭代器

所有返回多个元素的命令(keys、lrange、smembers 等),都不直接返回固定容器,而是要求传入一个输出迭代器。 这是典型的泛型解耦设计:库不限制用户使用 vector、set 还是自定义容器,只要支持对应插入动作就能用,灵活性非常高。

cpp 复制代码
std::vector<std::string> keys;
redis.keys("*", std::back_inserter(keys));

3.3 通用键命令

exists / del

既支持单个 key,也支持初始化列表批量操作,返回值为成功操作的 key 数量。

cpp 复制代码
// 判断单个 key 是否存在
long long exist_cnt = redis.exists("key");

// 批量删除多个 key
long long del_cnt = redis.del({"key1", "key2", "key3"});

expire / ttl

过期时间支持 std::chrono 时间类型,语义清晰,避免魔法数字。

cpp 复制代码
#include <chrono>
using namespace std::chrono_literals;

redis.expire("key", 10s);
long long remain = redis.ttl("key");

type

返回 key 对应 value 的类型字符串,和命令行输出一致。

cpp 复制代码
std::string type = redis.type("key"); // 返回 string / list / hash / set / zset

四. 五大核心数据类型实战

4.1 String 类型

基础读写与 NX/XX 选项

set 支持传入过期时间和写入策略,UpdateType::EXIST 对应命令行的 XX(仅更新),UpdateType::NOT_EXIST 对应 NX(仅新增)。

cpp 复制代码
// 仅当 key 不存在时写入
redis.set("key", "value", 0s, sw::redis::UpdateType::NOT_EXIST);

mset / mget

批量操作支持初始化列表和迭代器两种传参方式,适合批量写入与批量读取场景,减少网络 IO。

cpp 复制代码
// 批量写入
redis.mset({
    {"k1", "v1"},
    {"k2", "v2"},
    {"k3", "v3"}
});

// 批量读取
std::vector<sw::redis::OptionalString> result;
redis.mget({"k1", "k2", "k4"}, std::back_inserter(result));

其他常用命令

getrange / setrange 做子串读写,incr / decr 做原子增减,返回 long long 类型的计算后的值,用法和命令行一一对应。

4.2 List 类型

lpush / rpush / lrange

插入支持单个元素、初始化列表、迭代器三种形式;范围查询通过输出迭代器接收结果。

cpp 复制代码
redis.lpush("list", {"a", "b", "c"});

std::vector<std::string> res;
redis.lrange("list", 0, -1, std::back_inserter(res));

lpop / rpop

弹出列表一端的元素,返回 OptionalString,列表为空时返回无效值。

blpop / brpop

阻塞弹出命令,返回 OptionalStringPair:pair 的 first 是数据所属的 key,second 是弹出的元素。支持同时监听多个 key,哪个列表先有元素就返回哪个。

cpp 复制代码
auto res = redis.blpop({"list1", "list2"}, 10s);
if (res) {
    std::cout << "来源key: " << res->first 
              << ", 元素: " << res->second << std::endl;
}

4.3 Set 类型

sadd / smembers

smembers 的结果用 std::set 容器存储更贴合语义。注意 set 没有 push_back 方法,不能用 back_inserter,要使用 std::inserter

cpp 复制代码
redis.sadd("tag_set", {"java", "cpp", "redis"});

std::set<std::string> result;
redis.smembers("tag_set", std::inserter(result, result.end()));

sismember / spop / scard

  • sismember:判断元素是否存在,返回 1 或 0;
  • spop:随机弹出并返回一个元素,返回 OptionalString
  • scard:获取集合元素总数,返回 long long

sinter / sinterstore

集合交集运算,同样通过输出迭代器接收结果,也支持将运算结果存入新的 key。

4.4 Hash 类型

hset / hget

hset 写法非常灵活,支持单个键值对、std::pair、初始化列表、迭代器多种形式。

cpp 复制代码
// 单字段写入
redis.hset("user:1", "name", "张三");

// 批量写入
redis.hset("user:1", {
    {"age", "22"},
    {"score", "95"}
});

hexists / hdel / hlen

分别用于判断字段是否存在、删除字段、获取字段总数,语义和命令行完全一致。

hkeys / hvals / hmget

均为多结果返回,通过输出迭代器写入容器,适合批量获取字段名、字段值。

4.5 ZSet 类型

zadd / zrange

zadd 传入 member 与对应分数,支持批量写入。 zrange 有两种接收模式,库会根据迭代器指向的容器类型自动适配:

  • 容器元素为 string:只返回 member;
  • 容器元素为 pair<string, double>:同时返回 member 和 score。
cpp 复制代码
redis.zadd("rank", "吕布", 99);
redis.zadd("rank", {{"赵云", 98}, {"典韦", 97}});

// 只获取成员
std::vector<std::string> members;
redis.zrange("rank", 0, -1, std::back_inserter(members));

// 同时获取成员与分数
std::vector<std::pair<std::string, double>> res;
redis.zrange("rank", 0, -1, std::back_inserter(res));

zcard / zrem / zscore / zrank

  • zscore:查询元素分数,返回 OptionalDouble
  • zrank:查询元素的升序排名,返回 optional 包装的整数。

源码视角:redis-plus-plus 的设计哲学

站在 C++ 系统编程的角度看,这个库的架构设计非常值得借鉴,核心有三点:

1. 高度统一的接口约定

所有多入参命令都支持「单个值 + 初始化列表 + 迭代器」三种写法;所有多返回值都用输出迭代器;所有可能为空的返回都用 optional 包装。使用者学会一个命令就能类推所有命令,学习成本极低,也不容易用错。

2. 泛型编程解耦容器

用输出迭代器替代直接返回 vector,是典型的 STL 式设计思路:库不假设用户用什么容器,用户可以根据场景选择 vector、set、deque 甚至自定义容器,灵活性和扩展性都更强。

3. 不重复造轮子的分层封装

redis-plus-plus 并没有从零实现协议解析和网络通信,而是基于成熟的 hiredis C 库做上层封装,把原始的 RESP 响应转换成类型安全、语义清晰的 C++ 接口。底层做性能、上层做易用性,是工业界非常经典的分层封装思路。

核心考点总结

  1. RESP 协议
    1. Redis 的应用层序列化协议,基于 TCP,采用一问一答模型;
    2. 请求为批量字符串数组,响应包含简单字符串、整数、批量字符串等多种类型;
    3. 核心特点:实现简单、解析高效、文本可读。
  2. 环境搭建
    1. 底层依赖 hiredis 做协议与网络处理,redis-plus-plus 需源码编译安装;
    2. 编译必须链接 redis++、hiredis、pthread 三个库。
  3. 接口设计范式
    1. 入参:支持单个值、初始化列表、迭代器三种形式;
    2. 多返回值:通过输出迭代器接收,解耦容器类型;
    3. 空值:使用 std::optional,先判断再取值,禁止直接调用 value ()。
  4. 各类型核心用法
    1. String:set 的 NX/XX 策略、mset/mget 批量操作;
    2. List:blpop 返回 key + 元素 pair,支持多 key 监听;
    3. Set:smembers 适配 set 容器需使用 std::inserter
    4. ZSet:zrange 根据容器类型自动适配是否返回分数。
  5. 常见坑点
    1. optional 空值取值触发异常;
    2. set 容器误用 back_inserter 导致编译失败;
    3. 遗漏 pthread 链接导致符号未定义。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:从 RESP 字节级协议,到 hiredis C 语言基础库,再到 redis-plus-plus 现代 C++ 封装,整个客户端体系是一层一层的语义抽象:底层负责性能与协议,上层负责易用与安全。理解了这个分层逻辑,再遇到连接异常、返回值异常、编译报错,就能快速定位到网络层、协议层还是业务层的问题。掌握基础命令之后,后续还可以继续深入管道、事务、连接池、哨兵与集群客户端等进阶能力,进一步适配生产环境的高可用、高性能需求。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
yulingfeng591 小时前
虚拟机开机时自动挂载目录
linux·运维·服务器
郝学胜-神的一滴1 小时前
C++11 工程级应用 10:减少拷贝,让容器跑得更快
服务器·开发语言·c++·游戏引擎·opengl
张宇Joaquin1 小时前
S920X00机器CentOS 7.6下大批量容器执行docker exec卡顿
linux·docker·centos
广州浮点FLOATLIC1 小时前
ANSYS许可证排队时怎样确认是模块还是并发数不足
运维·服务器·网络
玖玥拾1 小时前
Lua 基础语法(八) Unity Huatuo (HybridCLR)
开发语言·unity·游戏引擎·lua
dunge20261 小时前
2026年9月11日|ChatGPT Pro + Codex:GPT‑6 Astra 数据库性能优化
数据库·gpt·chatgpt
小此方1 小时前
Linux网络(九):从 URL 到 HTTP 报文:彻底搞懂 HTTP 请求与响应,并手写一个简单的 HTTP 服务器
linux·服务器·网络
NeilYuen1 小时前
【kv存储】结合faiss构建向量内存数据库
数据库·faiss
xixiaoyunya1 小时前
数据备份方式全解析:三个核心维度的分类原理与组合策略
网络