超详细 Redis C++ 实战指南:从环境搭建到集群操作(附完整代码与测试结果) - 指南

补充知识

在具体学习 C++ 协同 Redis 之前,我们先来简单认识一下 RESP(Redis 序列化协议)

在网络通信里,不同层级有各自的协议分工。像传输层的 TCP/UDP、网络层的 IP 等,这些协议是系统内核或驱动里固定好的,咱们开发者只能选,改不了。

不过到了应用层就不一样啦,虽说有 HTTP 这类成熟协议,但很多场景下,为了更契合特定应用的需求,会选择自定义应用层协议。就拿 Redis 来说,它在应用层用的就是自己定制的协议,而且传输层还是基于 TCP 的。那 Redis 自定义的这个应用层协议具体是啥样的呢?这就得提到 RESP(Redis Serialization Protocol,Redis 序列化协议)了……

所以 Redis 客户端依照 Redis 规定好的应用层协议发送请求,服务器按该协议解析请求并构造响应,客户端再解析响应。通信能顺利完成,是因为客户端和服务器的开发者都清楚协议的细节,从而保证数据发送、解析等环节的一致性,实现有效交互。

所以作为第三方,想要开发一个 Redis 客户端,也是需要知道 Redis 的应用层协议!

在 RESP(Redis 序列化协议)里,借助首字节字符来区分不同数据类型,常用的有以下 5 种:

  • 单行字符串(simple string):首字节为 +,随后紧跟单行字符串,以 \r\n(CRLF)结尾。比如返回 “OK” 时,格式是 +OK\r\n
  • 错误(errors):首字节是 -,格式和单行字符串类似,只是字符串内容为异常信息。例如 -Error message\r\n
  • 数值(integers):首字节为 :,后面跟着数字形式的字符串,以 \r\n 结尾。像 :10\r\n 就是典型例子。
  • 多行字符串(bulk string):首字节为 $,支持二进制安全字符串,最大可达 512MB。若大小为 0,代表空字符串,格式为 $0\r\n\r\n;若大小为 -1,代表不存在,格式为 $-1\r\n

image-20230505213425550

  • 数组(arrays):首字节是 *,后面先跟数组元素个数,再依次跟元素,元素数据类型不限。

1653982993020

Redis 服务器通信时,不需要我们自己按照相关协议去手动解析或构造字符串。因为这套协议已经公开很久了,有很多开发者已经实现了对该协议解析和构造的功能,我们只要使用他们提供的库,就能简单方便地完成和 Redis 服务器的通信操作。

代码在我的码云上:小小企业家/Redis-C++

在后端开发中,Redis 作为高性能的键值存储数据库,被广泛用于缓存、会话存储、消息队列等场景。而 C++ 作为高性能开发的常用语言,如何通过简洁高效的方式操作 Redis 呢?本文将以 redis-plus-plus 库为核心,从环境搭建到各类数据结构的实战操作,再到集群访问,手把手带你掌握 Redis C++ 开发全流程,每个步骤都附带详细代码、执行命令和测试结果,确保零基础也能轻松上手。

一、前言:为什么选择 redis-plus-plus?

在开始之前,先明确我们选择的 Redis C++ 客户端 ——redis-plus-plus。它是基于 C 语言客户端 hiredis 封装的高级库,具备以下优势:

  • 功能全面:支持 Redis 所有核心命令(字符串、列表、集合、哈希、有序集合等),以及集群、哨兵模式。
  • 易用性强:API 设计简洁直观,与 Redis 原生命令高度一致,降低学习成本。
  • 性能优秀:底层基于 hiredis,保留 C 语言的高性能特性,同时支持异步操作(本文以同步操作为主,满足大部分场景需求)。
  • 跨平台:支持 Linux(Ubuntu/CentOS)、macOS 等主流操作系统,编译配置简单。

二、环境搭建:从依赖安装到编译配置

在使用 redis-plus-plus 前,需要先安装依赖库 hiredis,再编译安装 redis-plus-plus。以下分 Ubuntu 和 CentOS 两种主流 Linux 系统详细说明(Windows 需通过 WSL 或虚拟机模拟 Linux 环境)。

2.1 前置条件

确保你的系统已安装:

  • GCC/G++ 7.0+(支持 C++17,redis-plus-plus 依赖 C++17 特性)
  • Git(用于下载源码)
  • CMake 3.0+(用于编译 redis-plus-plus,CentOS 需单独升级)
  • Redis 服务(本地或远程,确保 6379 端口可访问,本文以本地 Redis 为例)

检查 GCC 版本:

g++ --version  # 输出需包含 "7.0" 及以上,如 g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0

2.2 步骤 1:安装 hiredis(C 语言依赖库)

redis-plus-plus 基于 hiredis 实现,必须先安装 hiredis。直接通过系统包管理器安装,无需手动编译。

操作系统安装命令说明
Ubuntusudo apt update && sudo apt install libhiredis-dev安装开发版(含头文件和库文件)
CentOSsudo yum install hiredis-devel.x86_64注意包名是 hiredis-devel,需指定 64 位架构

安装完成后,可通过以下命令验证:

# 检查头文件是否存在
ls /usr/include/hiredis/hiredis.h  # 应输出文件路径
# 检查库文件是否存在
ls /usr/lib/x86_64-linux-gnu/libhiredis.a  # Ubuntu
ls /usr/local/lib/libhiredis.a             # CentOS

2.3 步骤 2:下载 redis-plus-plus 源码

通过 Git 克隆官方源码(也可直接从 GitHub 下载 ZIP 包解压):

git clone https://github.com/sewenew/redis-plus-plus.git
cd redis-plus-plus  # 进入源码目录

2.4 步骤 3:编译并安装 redis-plus-plus

redis-plus-plus 使用 CMake 构建,Ubuntu 和 CentOS 的编译命令略有差异(主要因 CMake 版本不同)。

2.4.1 Ubuntu 编译步骤
  1. 创建构建目录(推荐单独创建 build 目录,避免污染源码):
    mkdir build && cd build
  2. 生成 Makefile(指定源码目录为上级目录 ..):
    cmake ..
  3. 编译(-j 选项可指定线程数,加速编译,如 make -j4):
    make
  4. 安装(需管理员权限,将头文件和库文件复制到系统目录):
    sudo make install
2.4.2 CentOS 编译步骤

CentOS 自带的 CMake 版本较低(通常是 2.x),需先安装 CMake 3:

sudo yum install cmake3  # 安装 CMake 3

后续步骤与 Ubuntu 类似,但需将 cmake 替换为 cmake3

mkdir build && cd build
cmake3 ..  # 使用 cmake3 生成 Makefile
make
sudo make install
2.4.3 验证安装结果

安装完成后,系统目录会新增以下文件,说明安装成功:

  • 头文件:/usr/local/include/sw/redis++/(包含 redis-plus-plus 所有头文件)
  • 库文件:/usr/local/lib/libredis++.a(静态库)或 libredis++.so(动态库)

2.5 步骤 4:项目目录结构与 Makefile 配置

为了规范管理代码,我们先创建标准的项目目录结构,再编写 Makefile 实现一键编译。

2.5.1 项目目录结构
redis-cpp-demo/  # 项目根目录
├── src/          # 源码目录
│   ├── generic.cc  # 通用命令(EXISTS、DEL、KEYS 等)
│   ├── string.cc   # 字符串操作(SET、GET、APPEND 等)
│   ├── list.cc     # 列表操作(LPUSH、LRANGE、LPOP 等)
│   ├── set.cc      # 集合操作(SADD、SMEMBERS、SINTER 等)
│   ├── hash.cc     # 哈希操作(HSET、HGET、HKEYS 等)
│   ├── zset.cc     # 有序集合操作(ZADD、ZRANGE、ZINCRBY 等)
│   └── cluster.cc  # 集群访问示例
└── Makefile       # 编译配置文件
2.5.2 编写 Makefile(关键!)

Makefile 的作用是指定编译规则,避免每次手动输入冗长的 g++ 命令。由于 Ubuntu 和 CentOS 的库文件路径略有差异,需分别配置。

Ubuntu 版 Makefile
# 目标:编译所有示例程序
all: generic string list set hash zset cluster
# 通用命令示例(generic.cc)
generic: src/generic.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 字符串操作示例(string.cc)
string: src/string.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 列表操作示例(list.cc)
list: src/list.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 集合操作示例(set.cc)
set: src/set.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 哈希操作示例(hash.cc)
hash: src/hash.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 有序集合操作示例(zset.cc)
zset: src/zset.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 集群访问示例(cluster.cc)
cluster: src/cluster.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread
# 清理编译生成的可执行文件
.PHONY: clean
clean:
	rm -rf generic string list set hash zset cluster
CentOS 版 Makefile

CentOS 的 redis-plus-plus 库文件路径是 /usr/local/lib64/,需修改库文件路径:

# 只需将所有编译命令中的 /usr/local/lib/libredis++.a 改为 /usr/local/lib64/libredis++.a
generic: src/generic.cc
	g++ -std=c++17 -g -O0 -o $@ $^ /usr/local/lib64/libredis++.a /usr/local/lib/libhiredis.a -pthread
# 其他目标(string、list 等)同理,修改 libredis++.a 路径即可
2.5.3 Makefile 关键参数说明
  • -std=c++17:指定 C++ 标准为 C++17,redis-plus-plus 依赖该标准。
  • -g -O0:生成调试信息,关闭优化(方便开发调试,发布时可改为 -O2 开启优化)。
  • $@:代表目标文件名(如 generic、string)。
  • $^:代表所有依赖文件(如 src/generic.cc)。
  • /usr/local/lib/libredis++.a:redis-plus-plus 静态库路径。
  • /lib/x86_64-linux-gnu/libhiredis.a:hiredis 静态库路径(CentOS 为 /usr/local/lib/libhiredis.a)。
  • -pthread:启用多线程支持,redis-plus-plus 内部使用线程安全机制。

2.6 步骤 5:测试

前面我们了解到和 Redis 通信不用自己手动搞协议那套,有现成库能用。那现在咱们就来实际操作试试,用 redis-plus-plus 这个库连接下 Redis 服务器,然后再用 ping 命令检测下连通性,看看是不是真的方便~

#include 
#include 
#include 
#include 
#include 
using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::unordered_map;
int main()
{
    //创建 Redis 对象的时候,需要在构造函数中,指定 redis 服务器的地址和端口
    sw::redis::Redis redis("tcp://127.0.0.1:6379");//URL 唯一资源定位符
    //此时咱们的 redis 客户端和服务端是在同一台主机上,使用本地环回就可以了
    //使用 ping 方法,让客户端给服务器发一个 PING,然后服务器就会返回一个 PONG,就通过 返回值 获取到
    string result = redis.ping();
    cout<< result << endl;
    return 0;
}
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 hello
lfz@instance-hojuqq09:~/WorkPlace/redis-c/test$ ./hello
PONG

三、核心实战:Redis 各类数据结构操作(附完整代码 + 测试结果)

接下来,我们按 通用命令 → 字符串 → 列表 → 集合 → 哈希 → 有序集合 的顺序,逐一讲解每个数据结构的核心操作。每个示例都包含「代码实现」「编译命令」「执行结果」三部分,确保你能一步一步复现。

3.1 通用命令:SET、GET、EXISTS、DEL、KEYS、EXPIRE 等

通用命令是 Redis 所有数据结构共用的命令,用于判断键是否存在、删除键、查询键、设置过期时间等。

sw::redis::set 函数,其函数原型如下:

bool set(const sw::redis::StringView &key, const sw::redis::StringView &val, bool keepttl, sw::redis::UpdateType type = sw::redis::UpdateType::ALWAYS);

返回值bool 类型,若函数执行成功,返回 true;若执行失败,返回 false

参数解释

  • const sw::redis::StringView &key:要设置值的键,sw::redis::StringView 是一种只读类型,相比 std::string 针对只读操作进行了优化,在传递键时能提升效率,采用引用传递避免了不必要的拷贝 。
  • const sw::redis::StringView &val:与键 key 对应的要设置的值,同样是 sw::redis::StringView 只读类型,也是通过引用传递。
  • bool keepttl:用于指定在设置新值时,是否保留键原有的过期时间。如果该值为 true,在更新键值对时,原有的过期时间会被保留;如果为 false ,则会根据后续是否设置过期时间相关参数来确定过期时间,若未设置相关过期参数,那么该键就没有过期时间。
  • sw::redis::UpdateType type = sw::redis::UpdateType::ALWAYS:指定更新的类型,是一个枚举类型。默认值为 sw::redis::UpdateType::ALWAYS,表示无论键是否存在,都会进行设置操作。它可能还有其他取值,比如仅在键存在时更新等,具体取决于代码中对该枚举类型的定义。

sw::redis::get 函数的原型及说明如下(参照 set 函数的风格):

std::optional get(const sw::redis::StringView &key);

返回值std::optional<sw::redis::StringView> 类型。若键存在,返回包含对应值的 optional 对象;若键不存在或操作失败,返回空 optional(可通过 has_value() 判断是否成功获取值)。

参数解释

  • const sw::redis::StringView &key:要获取值的键,使用 StringView 只读类型,通过引用传递减少拷贝,提升效率,与 set 函数的 key 参数类型一致。

核心功能:查询 Redis 中指定键对应的 value,对应 Redis 原生的 GET 命令,返回结果用 std::optional 封装,清晰区分 “键不存在” 和 “操作异常” 的场景(空 optional 统一表示未获取到有效值)。

exists 函数,函数原型

size_t exists(const sw::redis::StringView &key);
// 或支持多个键的重载版本
size_t exists(const std::initializer_list &keys);

返回值size_t 类型。返回存在的键的数量(对于单个键,存在则返回 1,不存在则返回 0)。

参数解释

  • const sw::redis::StringView &key:要检查的单个键,使用 StringView 类型减少拷贝。
  • const std::initializer_list<sw::redis::StringView> &keys:要检查的多个键的初始化列表,支持一次性检查多个键。

核心功能:检查 Redis 中指定键是否存在,对应 Redis 原生的 EXISTS 命令。对于单个键,返回 1 表示存在,0 表示不存在;对于多个键,返回存在的键的总数。

del 函数,函数原型

size_t del(const sw::redis::StringView &key);
// 或支持多个键的重载版本
size_t del(const std::initializer_list &keys);

返回值size_t 类型。返回被成功删除的键的数量(若键不存在则不计数)。

参数解释

  • const sw::redis::StringView &key:要删除的单个键。
  • const std::initializer_list<sw::redis::StringView> &keys:要删除的多个键的初始化列表。

核心功能:删除 Redis 中指定的键及其对应的值,对应 Redis 原生的 DEL 命令。返回实际被删除的键的数量(忽略不存在的键)。

keys 函数,函数原型

std::vector keys(const sw::redis::StringView &pattern);

返回值std::vector<sw::redis::StringView> 类型。返回所有匹配指定模式的键的列表;若没有匹配的键,则返回空向量。

参数解释

  • const sw::redis::StringView &pattern:匹配模式,支持通配符(如 * 匹配任意字符,? 匹配单个字符,[] 匹配指定范围的字符)。

核心功能:查找 Redis 中所有匹配指定模式的键,对应 Redis 原生的 KEYS 命令。返回值为匹配到的键的集合,需注意在大数据量场景下可能影响性能。

expire 函数,函数原型

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

返回值bool 类型。若键存在且过期时间设置成功,返回 true;若键不存在或设置失败,返回 false

参数解释

  • const sw::redis::StringView &key:要设置过期时间的键。
  • std::chrono::seconds seconds:过期时间(以秒为单位),超过该时间后键会被自动删除。

核心功能:为 Redis 中指定的键设置过期时间,对应 Redis 原生的 EXPIRE 命令。若键不存在则设置失败,返回 false;否则返回 true 表示过期时间设置成功。

3.1.1 代码实现(src/generic.cc)
#include 
#include 
#include 
#include 
#include 
#include 
// 测试通用命令
void testGenericCommands(sw::redis::Redis& redis) {
    printf("===================== Generic 系列命令 =====================\n");
    // 1. EXISTS 命令:判断键是否存在
    printf("\n【1. EXISTS 命令】\n");
    redis.flushdb();  // 清空当前数据库(避免干扰测试)
    bool set_ok = redis.set("key1", "Hello");  // 设置 key1 = Hello
    printf("redis < SET key1 \"Hello\"\n");
    printf("redis > %s\n", set_ok ? "OK" : "(nil)");
    long long exists_count = redis.exists("key1");  // 判断 key1 是否存在
    printf("redis < EXISTS key1\n");
    printf("redis > %lld\n", exists_count);  // 输出 1(存在)
    exists_count = redis.exists("nosuchkey");  // 判断不存在的键
    printf("redis < EXISTS nosuchkey\n");
    printf("redis > %lld\n", exists_count);  // 输出 0(不存在)
    redis.set("key2", "World");  // 设置 key2 = World
    printf("redis < SET key2 \"World\"\n");
    printf("redis > %s\n", set_ok ? "OK" : "(nil)");
    exists_count = redis.exists({"key1", "key2", "nosuchkey"});  // 批量判断
    printf("redis < EXISTS key1 key2 nosuchkey\n");
    printf("redis > %lld\n", exists_count);  // 输出 2(key1、key2 存在)
    // 2. DEL 命令:删除键
    printf("\n【2. DEL 命令】\n");
    redis.flushdb();
    redis.set("key1", "Hello");
    redis.set("key2", "World");
    printf("redis < SET key1 \"Hello\"\nredis > OK\n");
    printf("redis < SET key2 \"World\"\nredis > OK\n");
    long long del_count = redis.del({"key1", "key2", "key3"});  // 批量删除
    printf("redis < DEL key1 key2 key3\n");
    printf("redis > %lld\n", del_count);  // 输出 2(仅 key1、key2 被删除)
    // 3. KEYS 命令:模糊查询键
    printf("\n【3. KEYS 命令】\n");
    redis.flushdb();
    // 批量设置键值对
    std::unordered_map kvs = {
        {"firstname", "Jack"},
        {"lastname", "Stuntman"},
        {"age", "35"}
    };
    redis.mset(kvs.begin(), kvs.end());
    printf("redis < MSET firstname Jack lastname Stuntman age 35\nredis > OK\n");
    // 查询包含 "name" 的键
    std::vector keys;
    std::insert_iterator> ins(keys, keys.begin());
    redis.keys("*name*", ins);  // * 代表任意字符
    printf("redis < KEYS *name*\n");
    int n = 1;
    for (auto& key : keys) {
        printf("redis > %d) %s\n", n++, key.c_str());  // 输出 firstname、lastname
    }
    // 查询以 "a" 开头、后面跟 2 个字符的键(?? 代表任意单个字符)
    keys.clear();
    redis.keys("a??", ins);
    printf("redis < KEYS a??\n");
    n = 1;
    for (auto& key : keys) {
        printf("redis > %d) %s\n", n++, key.c_str());  // 输出 age
    }
    // 4. EXPIRE + TTL 命令:设置过期时间 + 查询剩余时间(秒)
    printf("\n【4. EXPIRE + TTL 命令】\n");
    redis.flushdb();
    redis.set("mykey", "Hello");
    printf("redis < SET mykey \"Hello\"\nredis > OK\n");
    bool expire_ok = redis.expire("mykey", std::chrono::seconds(10));  // 10 秒后过期
    printf("redis < EXPIRE mykey 10\n");
    printf("redis > %d\n", expire_ok ? 1 : 0);  // 输出 1(设置成功)
    long long ttl = redis.ttl("mykey");  // 查询剩余时间(秒)
    printf("redis < TTL mykey\n");
    printf("redis > %lld\n", ttl);  // 输出 10(接近 10 秒,因执行有延迟)
    // 5. PTTL 命令:查询剩余时间(毫秒)
    printf("\n【5. PTTL 命令】\n");
    redis.flushdb();
    redis.set("mykey", "Hello");
    redis.expire("mykey", std::chrono::seconds(10));
    printf("redis < SET mykey \"Hello\"\nredis > OK\n");
    printf("redis < EXPIRE mykey 10\nredis > 1\n");
    long long pttl = redis.pttl("mykey");  // 毫秒级剩余时间
    printf("redis < PTTL mykey\n");
    printf("redis > %lld\n", pttl);  // 输出约 10000(10 秒 = 10000 毫秒)
    // 6. TYPE 命令:判断键的数据类型
    printf("\n【6. TYPE 命令】\n");
    redis.flushdb();
    redis.set("key1", "value");          // 字符串类型
    redis.lpush("key2", "value");        // 列表类型
    redis.sadd("key3", "value");         // 集合类型
    printf("redis < SET key1 \"value\"\nredis > OK\n");
    printf("redis < LPUSH key2 \"value\"\nredis > 1\n");
    printf("redis < SADD key3 \"value\"\nredis > 1\n");
    std::string type = redis.type("key1");
    printf("redis < TYPE key1\nredis > \"%s\"\n", type.c_str());  // 输出 string
    type = redis.type("key2");
    printf("redis < TYPE key2\nredis > \"%s\"\n", type.c_str());  // 输出 list
    type = redis.type("key3");
    printf("redis < TYPE key3\nredis > \"%s\"\n", type.c_str());  // 输出 set
    printf("===========================================================\n");
}
int main() {
    // 连接本地 Redis(默认端口 6379,无密码)
    // 若 Redis 有密码,格式为:tcp://:password@127.0.0.1:6379
    sw::redis::Redis redis("tcp://127.0.0.1:6379");
    // 测试通用命令
    testGenericCommands(redis);
    return 0;
}
3.1.2 编译与执行

在项目根目录执行:

# 编译 generic 可执行文件
make generic
# 执行
./generic
3.1.3 测试结果(关键输出)
===================== Generic 系列命令 =====================
【1. EXISTS 命令】
redis < SET key1 "Hello"
redis > OK
redis < EXISTS key1
redis > 1
redis < EXISTS nosuchkey
redis > 0
redis < SET key2 "World"
redis > OK
redis < EXISTS key1 key2 nosuchkey
redis > 2
【2. DEL 命令】
redis < SET key1 "Hello"
redis > OK
redis < SET key2 "World"
redis > OK
redis < DEL key1 key2 key3
redis > 2
【3. KEYS 命令】
redis < MSET firstname Jack lastname Stuntman age 35
redis > OK
redis < KEYS *name*
redis > 1) firstname
redis > 2) lastname
redis < KEYS a??
redis > 1) age
【4. EXPIRE + TTL 命令】
redis < SET mykey "Hello"
redis > OK
redis < EXPIRE mykey 10
redis > 1
redis < TTL mykey
redis > 10
【5. PTTL 命令】
redis < SET mykey "Hello"
redis > OK
redis < EXPIRE mykey 10
redis > 1
redis < PTTL mykey
redis > 9998  # 因执行延迟,接近 10000 毫秒
【6. TYPE 命令】
redis < SET key1 "value"
redis > OK
redis < LPUSH key2 "value"
redis > 1
redis < SADD key3 "value"
redis > 1
redis < TYPE key1
redis > "string"
redis < TYPE key2
redis > "list"
redis < TYPE key3
redis > "set"
===========================================================

3.2 字符串操作:SET、GET、APPEND、INCR 等

字符串是 Redis 最基础的数据结构,常用于存储简单的键值对(如缓存用户信息、计数器等)。

3.2.1 代码实现(src/string.cc)
#include 
#include 
#include 
#include 
#include 
void testStringCommands(sw::redis::Redis& redis) {
    printf("===================== String 系列命令 =====================\n");
    // 1. SET 命令:设置键值(支持 NX/XX 选项、过期时间)
    printf("\n【1. SET 命令】\n");
    redis.flushdb();
    long long exists = redis.exists("mykey");
    printf("redis < EXISTS mykey\nredis > %lld\n", exists);  // 输出 0
    // 基础 SET:设置 mykey = Hello
    bool set_ok = redis.set("mykey", "Hello");
    printf("redis < SET mykey \"Hello\"\nredis > %s\n", set_ok ? "OK" : "(nil)");
    // GET:获取键值
    sw::redis::OptionalString value = redis.get("mykey");
    printf("redis < GET mykey\n");
    if (value) {
        printf("redis > \"%s\"\n", value->c_str());  // 输出 Hello
    } else {
        printf("redis > (nil)\n");
    }
    // SET NX:仅当键不存在时设置(NX = Not Exist)
    set_ok = redis.set("mykey", "World", std::chrono::seconds(0), sw::redis::UpdateType::NOT_EXIST);
    printf("redis < SET mykey \"World\" NX\n");
    printf("redis > %s\n", set_ok ? "OK" : "(nil)");  // 输出 (nil)(mykey 已存在)
    // 删除 mykey 后,测试 SET XX(仅当键存在时设置)
    redis.del("mykey");
    printf("redis < DEL mykey\nredis > 1\n");
    set_ok = redis.set("mykey", "World", std::chrono::seconds(0), sw::redis::UpdateType::EXIST);
    printf("redis < SET mykey \"World\" XX\n");
    printf("redis > %s\n", set_ok ? "OK" : "(nil)");  // 输出 (nil)(mykey 已删除)
    // SET EX:设置键并指定过期时间(10 秒)
    set_ok = redis.set("mykey", "Will expire in 10s", std::chrono::seconds(10));
    printf("redis < SET mykey \"Will expire in 10s\" EX 10\n");
    printf("redis > %s\n", set_ok ? "OK" : "(nil)");
    // 等待 10 秒后,键过期
    for (int i = 10; i > 0; --i) {
        printf("Waiting %ds...\n", i);
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    value = redis.get("mykey");
    printf("redis < GET mykey\n");
    if (value) {
        printf("redis > \"%s\"\n", value->c_str());
    } else {
        printf("redis > (nil)\n");  // 输出 (nil)(已过期)
    }
    // 2. APPEND 命令:追加字符串到键值末尾
    printf("\n【2. APPEND 命令】\n");
    redis.flushdb();
    long long append_len = redis.append("mykey", "Hello");  // 键不存在时,等同于 SET
    printf("redis < APPEND mykey \"Hello\"\n");
    printf("redis > %lld\n", append_len);  // 输出 5(字符串长度)
    value = redis.get("mykey");
    printf("redis < GET mykey\nredis > \"%s\"\n", value->c_str());  // 输出 Hello
    append_len = redis.append("mykey", " World");  // 追加 " World"
    printf("redis < APPEND mykey \" World\"\n");
    printf("redis > %lld\n", append_len);  // 输出 11(5+6)
    value = redis.get("mykey");
    printf("redis < GET mykey\nredis > \"%s\"\n", value->c_str());  // 输出 Hello World
    // 3. INCR/DECR 命令:整数自增/自减(计数器场景常用)
    printf("\n【3. INCR/DECR 命令】\n");
    redis.flushdb();
    // INCR:键不存在时,默认从 0 开始自增(结果为 1)
    long long incr_val = redis.incr("counter");
    printf("redis < INCR counter\nredis > %lld\n", incr_val);  // 输出 1
    // 再次 INCR:结果为 2
    incr_val = redis.incr("counter");
    printf("redis < INCR counter\nredis > %lld\n", incr_val);  // 输出 2
    // DECR:自减 1
    long long decr_val = redis.decr("counter");
    printf("redis < DECR counter\nredis > %lld\n", decr_val);  // 输出 1
    // 4. INCRBYFLOAT 命令:浮点数自增
    printf("\n【4. INCRBYFLOAT 命令】\n");
    redis.set("price", "10.50");
    printf("redis < SET price \"10.50\"\nredis > OK\n");
    double float_val = redis.incrbyfloat("price", 0.1);  // 增加 0.1
    printf("redis < INCRBYFLOAT price 0.1\n");
    printf("redis > %.2lf\n", float_val);  // 输出 10.60
    float_val = redis.incrbyfloat("price", -5);  // 减少 5
    printf("redis < INCRBYFLOAT price -5\n");
    printf("redis > %.2lf\n", float_val);  // 输出 5.60
    // 5. MSET/MGET 命令:批量设置/获取键值
    printf("\n【5. MSET/MGET 命令】\n");
    redis.flushdb();
    // 批量设置 key1=Hello、key2=World
    std::vector> kvs = {
        {"key1", "Hello"},
        {"key2", "World"}
    };
    redis.mset(kvs.begin(), kvs.end());
    printf("redis < MSET key1 \"Hello\" key2 \"World\"\nredis > OK\n");
    // 批量获取 key1、key2、nonexisting(不存在的键)
    std::vector values;
    std::insert_iterator> ins(values, values.begin());
    redis.mget({"key1", "key2", "nonexisting"}, ins);
    printf("redis < MGET key1 key2 nonexisting\n");
    int n = 1;
    for (auto& val : values) {
        if (val) {
            printf("redis > %d) %s\n", n++, val->c_str());  // 1) Hello, 2) World
        } else {
            printf("redis > %d) (nil)\n", n++);  // 3) (nil)
        }
    }
    printf("===========================================================\n");
}
int main() {
    sw::redis::Redis redis("tcp://127.0.0.1:6379");
    testStringCommands(redis);
    return 0;
}
3.2.2 编译与执行
make string
./string
3.2.3 关键测试结果
===================== String 系列命令 =====================
【1. SET 命令】
redis < EXISTS mykey
redis > 0
redis < SET mykey "Hello"
redis > OK
redis < GET mykey
redis > "Hello"
redis < SET mykey "World" NX
redis > (nil)
redis < DEL mykey
redis > 1
redis < SET mykey "World" XX
redis > (nil)
redis < SET mykey "Will expire in 10s" EX 10
redis > OK
Waiting 10s...
Waiting 9s...
...
Waiting 1s...
redis < GET mykey
redis > (nil)
【2. APPEND 命令】
redis < APPEND mykey "Hello"
redis > 5
redis < GET mykey
redis > "Hello"
redis < APPEND mykey " World"
redis > 11
redis < GET mykey
redis > "Hello World"
【3. INCR/DECR 命令】
redis < INCR counter
redis > 1
redis < INCR counter
redis > 2
redis < DECR counter
redis > 1
【4. INCRBYFLOAT 命令】
redis < SET price "10.50"
redis > OK
redis < INCRBYFLOAT price 0.1
redis > 10.60
redis < INCRBYFLOAT price -5
redis > 5.60
【5. MSET/MGET 命令】
redis < MSET key1 "Hello" key2 "World"
redis > OK
redis < MGET key1 key2 nonexisting
redis > 1) Hello
redis > 2) World
redis > 3) (nil)
===========================================================

3.3 列表操作:LPUSH、LRANGE、LPOP、BRPOP 等

Redis 列表是有序的字符串列表,支持从两端插入 / 删除元素,常用于实现消息队列、最新消息列表等场景。

3.3.1 代码实现(src/list.cc)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
void testListCommands(sw::redis::Redis& redis) {
    printf("===================== List 系列命令 =====================\n");
    // 1. LPUSH/LRANGE 命令:从左侧插入元素 + 获取列表范围
    printf("\n【1. LPUSH + LRANGE 命令】\n");
    redis.flushdb();
    // 左侧插入单个元素(world)
    long long list_len = redis.lpush("mylist", "world");
    printf("redis < LPUSH mylist \"world\"\nredis > %lld\n", list_len);  // 输出 1
    // 左侧插入单个元素(hello)
    list_len = redis.lpush("mylist", "hello");
    printf("redis < LPUSH mylist \"hello\"\nredis > %lld\n", list_len);  // 输出 2
    // 左侧批量插入元素(Redis、MySQL、MongoDB)
    list_len = redis.lpush("mylist", {"Redis", "MySQL", "MongoDB"});
    printf("redis < LPUSH mylist \"Redis\" \"MySQL\" \"MongoDB\"\nredis > %lld\n", list_len);  // 输出 5
    // LRANGE 0 -1:获取所有元素(0 是第一个元素,-1 是最后一个元素)
    std::vector elements;
    std::back_insert_iterator> ins(elements);
    redis.lrange("mylist", 0, -1, ins);
    printf("redis < LRANGE mylist 0 -1\n");
    int n = 1;
    for (auto& elem : elements) {
        printf("redis > %d) %s\n", n++, elem.c_str());  // 输出顺序:MongoDB、MySQL、Redis、hello、world
    }
    // 2. RPUSH 命令:从右侧插入元素(与 LPUSH 方向相反)
    printf("\n【2. RPUSH 命令】\n");
    redis.flushdb();
    list_len = redis.rpush("mylist", "world");  // 右侧插入 world
    printf("redis < RPUSH mylist \"world\"\nredis > %lld\n", list_len);  // 输出 1
    list_len = redis.rpush("mylist", "hello");  // 右侧插入 hello
    printf("redis < RPUSH mylist \"hello\"\nredis > %lld\n", list_len);  // 输出 2
    // 获取所有元素(顺序:world、hello)
    elements.clear();
    redis.lrange("mylist", 0, -1, ins);
    printf("redis < LRANGE mylist 0 -1\n");
    n = 1;
    for (auto& elem : elements) {
        printf("redis > %d) %s\n", n++, elem.c_str());
    }
    // 3. LPOP/RPOP 命令:从左侧/右侧删除元素
    printf("\n【3. LPOP + RPOP 命令】\n");
    redis.flushdb();
    // 批量插入元素:one、two、three、four、five
    redis.rpush("mylist", {"one", "two", "three", "four", "five"});
    printf("redis < RPUSH mylist one two three four five\nredis > 5\n");
    // LPOP:删除左侧第一个元素(one)
    sw::redis::OptionalString pop_val = redis.lpop("mylist");
    printf("redis < LPOP mylist\nredis > %s\n", pop_val->c_str());  // 输出 one
    // RPOP:删除右侧第一个元素(five)
    pop_val = redis.rpop("mylist");
    printf("redis < RPOP mylist\nredis > %s\n", pop_val->c_str());  // 输出 five
    // 剩余元素:two、three、four
    elements.clear();
    redis.lrange("mylist", 0, -1, ins);
    printf("redis < LRANGE mylist 0 -1\n");
    n = 1;
    for (auto& elem : elements) {
        printf("redis > %d) %s\n", n++, elem.c_str());
    }
    // 4. BRPOP 命令:阻塞式从右侧删除元素(消息队列核心命令)
    printf("\n【4. BRPOP 命令】\n");
    redis.flushdb();
    // 先往 list1 插入 a、b、c
    redis.rpush("list1", {"a", "b", "c"});
    printf("redis < RPUSH list1 a b c\nredis > 3\n");
    // BRPOP list1 list2 0:阻塞等待 list1 或 list2 有元素,0 表示无限阻塞
    sw::redis::OptionalStringPair brpop_res = redis.brpop({"list1", "list2"}, std::chrono::seconds(0));
    printf("redis < BRPOP list1 list2 0\n");
    if (brpop_res) {
        // 结果是 <列表名, 元素值>
        printf("redis > 1) %s\nredis > 2) %s\n", brpop_res->first.c_str(), brpop_res->second.c_str());  // 1) list1, 2) c
    }
    // 再次 BRPOP:删除 b
    brpop_res = redis.brpop({"list1", "list2"}, std::chrono::seconds(0));
    printf("redis < BRPOP list1 list2 0\n");
    printf("redis > 1) %s\nredis > 2) %s\n", brpop_res->first.c_str(), brpop_res->second.c_str());  // 1) list1, 2) b
    // 当 list1 为空时,BRPOP 等待 5 秒后返回 nil
    brpop_res = redis.brpop({"list1", "list2"}, std::chrono::seconds(5));
    printf("redis < BRPOP list1 list2 5\n");
    if (brpop_res) {
        printf("redis > 1) %s\nredis > 2) %s\n", brpop_res->first.c_str(), brpop_res->second.c_str());
    } else {
        printf("redis > (nil)\n");  // 输出 (nil)
    }
    printf("===========================================================\n");
}
int main() {
    sw::redis::Redis redis("tcp://127.0.0.1:6379");
    testListCommands(redis);
    return 0;
}
3.3.2 编译与执行
make list
./list
3.3.3 关键测试结果
===================== List 系列命令 =====================
【1. LPUSH + LRANGE 命令】
redis < LPUSH mylist "world"
redis > 1
redis < LPUSH mylist "hello"
redis > 2
redis < LPUSH mylist "Redis" "MySQL" "MongoDB"
redis > 5
redis < LRANGE mylist 0 -1
redis > 1) MongoDB
redis > 2) MySQL
redis > 3) Redis
redis > 4) hello
redis > 5) world
【2. RPUSH 命令】
redis < RPUSH mylist "world"
redis > 1
redis < RPUSH mylist "hello"
redis > 2
redis < LRANGE mylist 0 -1
redis > 1) world
redis > 2) hello
【3. LPOP + RPOP 命令】
redis < RPUSH mylist one two three four five
redis > 5
redis < LPOP mylist
redis > one
redis < RPOP mylist
redis > five
redis < LRANGE mylist 0 -1
redis > 1) two
redis > 2) three
redis > 3) four
【4. BRPOP 命令】
redis < RPUSH list1 a b c
redis > 3
redis < BRPOP list1 list2 0
redis > 1) list1
redis > 2) c
redis < BRPOP list1 list2 0
redis > 1) list1
redis > 2) b
redis < BRPOP list1 list2 5
redis > (nil)
===========================================================

3.4 集合操作:SADD、SMEMBERS、SINTER、SUNION 等

Redis 集合是无序的、唯一的字符串集合,支持交集、并集、差集等操作,常用于去重、标签管理、共同好友查询等场景。

3.4.1 代码实现(src/set.cc)
#include 
#include 
#include 
#include 
#include 
void testSetCommands(sw::redis::Redis& redis) {
    printf("===================== Set 系列命令 =====================\n");
    // 1. SADD + SMEMBERS 命令:添加元素到集合 + 获取所有元素
    printf("\n【1. SADD + SMEMBERS 命令】\n");
    redis.flushdb();
    // 添加单个元素(Hello)
    long long add_count = redis.sadd("myset", "Hello");
    printf("redis < SADD myset Hello\nredis > %lld\n", add_count);  // 输出 1
    // 添加单个元素(World)
    add_count = redis.sadd("myset", "World");
    printf("redis < SADD myset World\nredis > %lld\n", add_count);  // 输出 1
    // 添加重复元素(World):集合自动去重,返回 0
    add_count = redis.sadd("myset", "World");
    printf("redis < SADD myset World\nredis > %lld\n", add_count);  // 输出 0
    // SMEMBERS:获取所有元素(无序)
    std::unordered_set members;
    redis.smembers("myset", std::inserter(members, members.begin()));
    printf("redis < SMEMBERS myset\n");
    int n = 1;
    for (auto& mem : members) {
        printf("redis > %d) %s\n", n++, mem.c_str());  // 输出 Hello、World(顺序不确定)
    }
    // 2. SISMEMBER 命令:判断元素是否在集合中
    printf("\n【2. SISMEMBER 命令】\n");
    redis.flushdb();
    redis.sadd("myset", "one");
    printf("redis < SADD myset one\nredis > 1\n");
    bool is_member = redis.sismember("myset", "one");
    printf("redis < SISMEMBER myset one\nredis > %d\n", is_member ? 1 : 0);  // 输出 1
    is_member = redis.sismember("myset", "two");
    printf("redis < SISMEMBER myset two\nredis > %d\n", is_member ? 1 : 0);  // 输出 0
    // 3. SINTER 命令:求两个集合的交集
    printf("\n【3. SINTER 命令】\n");
    redis.flushdb();
    // 集合 key1:a、b、c
    redis.sadd("key1", {"a", "b", "c"});
    printf("redis < SADD key1 a b c\nredis > 3\n");
    // 集合 key2:c、d、e
    redis.sadd("key2", {"c", "d", "e"});
    printf("redis < SADD key2 c d e\nredis > 3\n");
    // 交集:c
    std::unordered_set intersect;
    redis.sinter({"key1", "key2"}, std::inserter(intersect, intersect.begin()));
    printf("redis < SINTER key1 key2\n");
    n = 1;
    for (auto& mem : intersect) {
        printf("redis > %d) %s\n", n++, mem.c_str());  // 输出 1) c
    }
    // 4. SUNION 命令:求两个集合的并集
    printf("\n【4. SUNION 命令】\n");
    std::unordered_set union_set;
    redis.sunion({"key1", "key2"}, std::inserter(union_set, union_set.begin()));
    printf("redis < SUNION key1 key2\n");
    n = 1;
    for (auto& mem : union_set) {
        printf("redis > %d) %s\n", n++, mem.c_str());  // 输出 a、b、c、d、e
    }
    // 5. SDIFF 命令:求两个集合的差集(key1 - key2)
    printf("\n【5. SDIFF 命令】\n");
    std::unordered_set diff_set;
    redis.sdiff({"key1", "key2"}, std::inserter(diff_set, diff_set.begin()));
    printf("redis < SDIFF key1 key2\n");
    n = 1;
    for (auto& mem : diff_set) {
        printf("redis > %d) %s\n", n++, mem.c_str());  // 输出 a、b
    }
    printf("===========================================================\n");
}
int main() {
    sw::redis::Redis redis("tcp://127.0.0.1:6379");
    testSetCommands(redis);
    return 0;
}
3.4.2 编译与执行
make set
./set
3.4.3 关键测试结果
===================== Set 系列命令 =====================
【1. SADD + SMEMBERS 命令】
redis < SADD myset Hello
redis > 1
redis < SADD myset World
redis > 1
redis < SADD myset World
redis > 0
redis < SMEMBERS myset
redis > 1) Hello
redis > 2) World
【2. SISMEMBER 命令】
redis < SADD myset one
redis > 1
redis < SISMEMBER myset one
redis > 1
redis < SISMEMBER myset two
redis > 0
【3. SINTER 命令】
redis < SADD key1 a b c
redis > 3
redis < SADD key2 c d e
redis > 3
redis < SINTER key1 key2
redis > 1) c
【4. SUNION 命令】
redis < SUNION key1 key2
redis > 1) a
redis > 2) b
redis > 3) c
redis > 4) d
redis > 5) e
【5. SDIFF 命令】
redis < SDIFF key1 key2
redis > 1) a
redis > 2) b
===========================================================

3.5 哈希操作:HSET、HGET、HKEYS、HVALS 等

Redis 哈希是键值对的集合(类似 C++ 的 unordered_map),适合存储结构化数据(如用户信息、商品详情),支持单独修改某个字段,无需更新整个对象。

3.5.1 代码实现(src/hash.cc)
#include 
#include 
#include 
#include 
#include 
using std::string;
using std::vector;
using sw::redis::Redis;
using sw::redis::OptionalString;
void testHashCommand(Redis& redis) {
    printf("===================== Hash 系列命令 =====================\n");
    // 1. HSET + HGET 命令:设置哈希字段 + 获取哈希字段值
    printf("\n【1. HSET + HGET 命令】\n");
    redis.flushdb();
    // 设置哈希 key 的 name 字段 = zhangsan
    redis.hset("user", "name", "zhangsan");
    printf("redis < HSET user name zhangsan\nredis > 1\n");
    // 设置哈希 key 的 age 字段 = 20
    redis.hset("user", "age", "20");
    printf("redis < HSET user age 20\nredis > 1\n");
    // 获取 name 字段值
    OptionalString name = redis.hget("user", "name");
    printf("redis < HGET user name\nredis > name = %s\n", name->c_str());  // 输出 zhangsan
    // 获取 age 字段值
    OptionalString age = redis.hget("user", "age");
    printf("redis < HGET user age\nredis > age = %s\n", age->c_str());  // 输出 20
    // 2. HEXISTS + HDEL 命令:判断哈希字段是否存在 + 删除哈希字段
    printf("\n【2. HEXISTS + HDEL 命令】\n");
    redis.flushdb();
    redis.hset("user", "name", "zhangsan");
    printf("redis < HSET user name zhangsan\nredis > 1\n");
    bool exists = redis.hexists("user", "name");
    printf("redis < HEXISTS user name\nredis > ok = %d\n", exists);  // 输出 1
    // 删除 name 字段
    long long del_count = redis.hdel("user", "name");
    printf("redis < HDEL user name\nredis > n = %lld\n", del_count);  // 输出 1
    // 再次判断 name 字段:已删除
    exists = redis.hexists("user", "name");
    printf("redis < HEXISTS user name\nredis > ok = %d\n", exists);  // 输出 0
    // 3. HKEYS + HVALS 命令:获取所有哈希字段名 + 获取所有哈希字段值
    printf("\n【3. HKEYS + HVALS 命令】\n");
    redis.flushdb();
    redis.hset("user", "name", "zhangsan");
    redis.hset("user", "age", "20");
    printf("redis < HSET user name zhangsan\nredis > 1\n");
    printf("redis < HSET user age 20\nredis > 1\n");
    // HKEYS:获取所有字段名(name、age)
    vector keys;
    auto key_ins = std::back_inserter(keys);
    redis.hkeys("user", key_ins);
    printf("redis < HKEYS user\n");
    for (auto& key : keys) {
        printf("redis > key: %s\n", key.c_str());  // 输出 name、age
    }
    // HVALS:获取所有字段值(zhangsan、20)
    vector vals;
    auto val_ins = std::back_inserter(vals);
    redis.hvals("user", val_ins);
    printf("redis < HVALS user\n");
    for (auto& val : vals) {
        printf("redis > val: %s\n", val.c_str());  // 输出 zhangsan、20
    }
    // 4. HMGET 命令:批量获取哈希字段值
    printf("\n【4. HMGET 命令】\n");
    vector values;
    auto val_ins_batch = std::back_inserter(values);
    vector fields = {"name", "age"};
    // 批量获取 name 和 age 字段
    redis.hmget("user", fields.begin(), fields.end(), val_ins_batch);
    printf("redis < HMGET user name age\n");
    for (auto& val : values) {
        printf("redis > val: %s\n", val->c_str());  // 输出 zhangsan、20
    }
    printf("===========================================================\n");
}
int main() {
    Redis redis("tcp://127.0.0.1:6379");
    testHashCommand(redis);
    return 0;
}
3.5.2 编译与执行
make hash
./hash
3.5.3 关键测试结果
===================== Hash 系列命令 =====================
【1. HSET + HGET 命令】
redis < HSET user name zhangsan
redis > 1
redis < HSET user age 20
redis > 1
redis < HGET user name
redis > name = zhangsan
redis < HGET user age
redis > age = 20
【2. HEXISTS + HDEL 命令】
redis < HSET user name zhangsan
redis > 1
redis < HEXISTS user name
redis > ok = 1
redis < HDEL user name
redis > n = 1
redis < HEXISTS user name
redis > ok = 0
【3. HKEYS + HVALS 命令】
redis < HSET user name zhangsan
redis > 1
redis < HSET user age 20
redis > 1
redis < HKEYS user
redis > key: name
redis > key: age
redis < HVALS user
redis > val: zhangsan
redis > val: 20
【4. HMGET 命令】
redis < HMGET user name age
redis > val: zhangsan
redis > val: 20
===========================================================

3.6 有序集合操作:ZADD、ZRANGE、ZINCRBY、ZINTERSTORE 等

Redis 有序集合(Zset)是带分数(score)的集合,元素唯一且按分数排序,常用于排行榜、优先级队列等场景(如游戏战力排行、任务优先级排序)。

3.6.1 代码实现(src/zset.cc)
#include 
#include 
#include 
#include 
#include 
using std::string;
using std::vector;
using sw::redis::Redis;
using sw::redis::OptionalString;
using sw::redis::OptionalDouble;
void testZsetCommand(Redis& redis) {
    printf("===================== Zset 系列命令 =====================\n");
    // 1. ZADD + ZRANGE 命令:添加元素到有序集合 + 获取排序后的元素
    printf("\n【1. ZADD + ZRANGE 命令】\n");
    redis.flushdb();
    // 添加元素(成员,分数):分数越高,排名越靠后(默认升序)
    redis.zadd("hero_rank", "吕布", 100);
    redis.zadd("hero_rank", "赵云", 98);
    redis.zadd("hero_rank", "典韦", 95);
    redis.zadd("hero_rank", "关羽", 92);
    redis.zadd("hero_rank", "刘备", 70);
    printf("redis < ZADD hero_rank 100 吕布\n");
    printf("redis < ZADD hero_rank 98 赵云\n");
    printf("redis < ZADD hero_rank 95 典韦\n");
    printf("redis < ZADD hero_rank 92 关羽\n");
    printf("redis < ZADD hero_rank 70 刘备\n");
    // ZRANGE 0 4:获取前 5 个元素(升序,分数从低到高)
    vector members;
    auto mem_ins = std::back_inserter(members);
    redis.zrange("hero_rank", 0, 4, mem_ins);
    printf("redis < ZRANGE hero_rank 0 4\n");
    int n = 1;
    for (auto& mem : members) {
        printf("redis > member=%s\n", mem->c_str());  // 输出刘备、关羽、典韦、赵云、吕布
    }
    // ZRANGE ... WITHSCORES:获取元素及对应的分数
    vector> members_with_score;
    auto mem_score_ins = std::back_inserter(members_with_score);
    redis.zrange("hero_rank", 0, 4, mem_score_ins);
    printf("redis < ZRANGE hero_rank 0 4 WITHSCORES\n");
    for (auto& item : members_with_score) {
        printf("redis > member=%s, score=%.2f\n", item.first.c_str(), item.second);
    }
    // 2. ZINCRBY 命令:增加元素的分数(排行榜更新战力常用)
    printf("\n【2. ZINCRBY 命令】\n");
    // 吕布分数增加 10(从 100 变为 110)
    double new_score = redis.zincrby("hero_rank", 10, "吕布");
    printf("redis < ZINCRBY hero_rank 10 吕布\n");
    printf("redis > score=%.2f\n", new_score);  // 输出 110.00
    // 吕布分数减少 20(从 110 变为 90)
    new_score = redis.zincrby("hero_rank", -20, "吕布");
    printf("redis < ZINCRBY hero_rank -20 吕布\n");
    printf("redis > score=%.2f\n", new_score);  // 输出 90.00
    // 3. ZRANK 命令:获取元素的排名(升序,从 0 开始)
    printf("\n【3. ZRANK 命令】\n");
    // 重置分数,方便测试
    redis.flushdb();
    redis.zadd("hero_rank", "吕布", 100);
    redis.zadd("hero_rank", "赵云", 98);
    redis.zadd("hero_rank", "典韦", 95);
    redis.zadd("hero_rank", "关羽", 92);
    redis.zadd("hero_rank", "刘备", 70);
    // ZRANK:升序排名(刘备第 0,吕布第 4)
    auto rank = redis.zrank("hero_rank", "吕布");
    printf("redis < ZRANK hero_rank 吕布\n");
    printf("redis > rank=%lld\n", rank.value());  // 输出 4
    // ZREVRANK:降序排名(吕布第 0,刘备第 4)
    auto rev_rank = redis.zrevrank("hero_rank", "吕布");
    printf("redis < ZREVRANK hero_rank 吕布\n");
    printf("redis > rev_rank=%lld\n", rev_rank.value());  // 输出 0
    // 4. ZINTERSTORE 命令:求两个有序集合的交集(保留分数和)
    printf("\n【4. ZINTERSTORE 命令】\n");
    redis.flushdb();
    // 集合 1:三国英雄战力
    redis.zadd("hero1", "吕布", 100);
    redis.zadd("hero1", "赵云", 98);
    redis.zadd("hero1", "典韦", 95);
    // 集合 2:三国英雄人气
    redis.zadd("hero2", "吕布", 100);
    redis.zadd("hero2", "赵云", 98);
    redis.zadd("hero2", "关羽", 92);
    // 交集:吕布、赵云(两个集合都有的英雄),分数为两个集合分数之和
    long long inter_count = redis.zinterstore("hero_inter", {"hero1", "hero2"});
    printf("redis < ZINTERSTORE hero_inter hero1 hero2\n");
    printf("redis > count=%lld\n", inter_count);  // 输出 2
    // 获取交集结果
    vector> inter_members;
    auto inter_ins = std::back_inserter(inter_members);
    redis.zrange("hero_inter", 0, -1, inter_ins);
    printf("redis < ZRANGE hero_inter 0 -1 WITHSCORES\n");
    for (auto& item : inter_members) {
        printf("redis > member=%s, score=%.2f\n", item.first.c_str(), item.second);  // 赵云 196(98+98)、吕布 200(100+100)
    }
    printf("===========================================================\n");
}
int main() {
    Redis redis("tcp://127.0.0.1:6379");
    testZsetCommand(redis);
    return 0;
}
3.6.2 编译与执行
make zset
./zset
3.6.3 关键测试结果
===================== Zset 系列命令 =====================
【1. ZADD + ZRANGE 命令】
redis < ZADD hero_rank 100 吕布
redis < ZADD hero_rank 98 赵云
redis < ZADD hero_rank 95 典韦
redis < ZADD hero_rank 92 关羽
redis < ZADD hero_rank 70 刘备
redis < ZRANGE hero_rank 0 4
redis > member=刘备
redis > member=关羽
redis > member=典韦
redis > member=赵云
redis > member=吕布
redis < ZRANGE hero_rank 0 4 WITHSCORES
redis > member=刘备, score=70.00
redis > member=关羽, score=92.00
redis > member=典韦, score=95.00
redis > member=赵云, score=98.00
redis > member=吕布, score=100.00
【2. ZINCRBY 命令】
redis < ZINCRBY hero_rank 10 吕布
redis > score=110.00
redis < ZINCRBY hero_rank -20 吕布
redis > score=90.00
【3. ZRANK 命令】
redis < ZRANK hero_rank 吕布
redis > rank=4
redis < ZREVRANK hero_rank 吕布
redis > rev_rank=0
【4. ZINTERSTORE 命令】
redis < ZINTERSTORE hero_inter hero1 hero2
redis > count=2
redis < ZRANGE hero_inter 0 -1 WITHSCORES
redis > member=赵云, score=196.00
redis > member=吕布, score=200.00
===========================================================

四、进阶:Redis 集群访问

当 Redis 单机性能不足时,会使用集群模式(将数据分片存储到多个节点)。redis-plus-plus 提供 RedisCluster 类,用法与单机 Redis 类几乎一致,只需修改连接方式。

4.1 代码实现(src/cluster.cc)

#include 
#include 
#include 
using std::string;
using sw::redis::RedisCluster;
using sw::redis::OptionalString;
int main() {
    // 连接 Redis 集群:只需填入任意一个节点的地址(集群会自动发现其他节点)
    // 格式:tcp://[节点IP]:[端口],若有密码则为 tcp://:password@IP:端口
    RedisCluster cluster("tcp://127.0.0.1:6379");
    // 后续操作与单机完全一致:设置键值、获取键值
    cluster.set("user:1:name", "zhangsan");  // 设置用户 1 的姓名
    cluster.set("user:1:age", "25");        // 设置用户 1 的年龄
    // 获取键值
    OptionalString name = cluster.get("user:1:name");
    OptionalString age = cluster.get("user:1:age");
    if (name && age) {
        printf("User 1: name=%s, age=%s\n", name->c_str(), age->c_str());  // 输出 User 1: name=zhangsan, age=25
    }
    return 0;
}

4.2 编译与执行

确保 Makefile 中已添加 cluster 目标(参考第二章 2.5.2 节),然后执行:

make cluster
./cluster

4.3 关键测试结果

User 1: name=zhangsan, age=25

4.4 集群访问注意事项

  1. 确保集群中所有节点的 6379 端口(及集群总线端口 16379)可访问。
  2. 若集群启用了密码,连接字符串需包含密码(如 tcp://:123456@127.0.0.1:6379)。
  3. RedisCluster 类的 API 与 Redis 类完全兼容,支持所有命令(如 hsetzadd 等)。

五、常见问题与解决方案

在实战过程中,可能会遇到以下问题,这里提供针对性解决方案:

5.1 编译错误:找不到 redis++ 头文件

错误信息fatal error: sw/redis++/redis++.h: No such file or directory原因:redis-plus-plus 安装路径不正确,或编译时未指定头文件路径。解决方案

  1. 确认头文件是否在 /usr/local/include/sw/redis++/ 目录:
    ls /usr/local/include/sw/redis++/redis++.h
  2. 若不在,重新执行 sudo make install 安装 redis-plus-plus。
  3. 若仍报错,在 Makefile 中手动指定头文件路径(添加 -I/usr/local/include):
    g++ -std=c++17 -g -O0 -o $@ $^ -I/usr/local/include /usr/local/lib/libredis++.a /lib/x86_64-linux-gnu/libhiredis.a -pthread

5.2 链接错误:undefined reference to sw::redis::Redis::Redis(...)

错误信息undefined reference to sw::redis::Redis::Redis(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)原因:未链接 redis-plus-plus 或 hiredis 静态库,或库文件路径错误。解决方案

  1. 确认库文件路径是否正确(Ubuntu:/usr/local/lib/libredis++.a,CentOS:/usr/local/lib64/libredis++.a)。
  2. 检查 Makefile 中是否包含两个库文件(libredis++.a 和 libhiredis.a)。

5.3 运行错误:Could not connect to Redis server

错误信息sw::redis::ConnectionError: Could not connect to Redis server at 127.0.0.1:6379原因

  1. Redis 服务未启动。
  2. Redis 配置中 bind 选项限制了 IP 访问(如仅绑定 127.0.0.1,远程无法访问)。
  3. 防火墙阻止了 6379 端口。

解决方案

  1. 启动 Redis 服务:sudo systemctl start redis
  2. 若远程访问,修改 Redis 配置文件(redis.conf):
    • 将 bind 127.0.0.1 改为 bind 0.0.0.0(允许所有 IP 访问)。
    • 将 protected-mode yes 改为 protected-mode no(关闭保护模式)。
  3. 开放 6379 端口(Ubuntu):
    sudo ufw allow 6379

六、总结

本文从环境搭建到实战操作,详细讲解了 Redis C++ 开发的全流程,核心要点总结如下:

  1. 环境搭建:先安装 hiredis 依赖,再编译安装 redis-plus-plus,通过 Makefile 简化编译。
  2. 核心操作
    • 通用命令:EXISTS(判断键存在)、DEL(删除键)、KEYS(模糊查询)。
    • 字符串:SET/GET(设置 / 获取)、APPEND(追加)、INCR(自增)。
    • 列表:LPUSH/RPUSH(插入)、LRANGE(获取范围)、BRPOP(阻塞删除,消息队列)。
    • 集合:SADD(添加)、SINTER(交集)、SUNION(并集)、SDIFF(差集)。
    • 哈希:HSET/HGET(设置 / 获取字段)、HKEYS/HVALS(获取字段名 / 值)。
    • 有序集合:ZADD(添加)、ZRANGE(排序获取)、ZINCRBY(更新分数)。
  3. 集群访问:使用 RedisCluster 类,连接方式改为集群节点地址,API 与单机一致。

所有代码均经过实际测试,可直接复制到项目中使用。若需进一步学习,可参考 redis-plus-plus 官方文档(GitHub)或 Redis 官方命令手册(Redis Commands)。

希望本文能帮助你快速掌握 Redis C++ 开发,如有疑问,欢迎在评论区留言讨论!

posted on 2025-11-06 20:11  blfbuaa  阅读(30)  评论(0)    收藏  举报