技术栈:C / C++17 · Redis 7.x · hiredis 1.1.0 · redis-plus-plus(sw/redis++)1.3.x
配套环境:Windows 11 / Linux(本文示例以 CMake + vcpkg/FetchContent 集成为主)
1. 背景
1.1 工业数采的双路径存储模型
工业数据采集(CNC / PLC / 传感器)的落地链路通常分为两条路径:
- 历史流:设备点位数据以消息形式进入 Kafka,最终落入 TDengine 等时序数据库,用于离线分析、告警回溯、OEE 统计。特点是数据量大、只追加、不需要毫秒级响应。
- 实时层 :设备最新值 、命令下发队列 、多网关互斥锁 、实时告警等场景需要毫秒级读写,这类数据用内存缓存(Redis)承载,再定期/按需下沉到历史存储。
在这条链路中,Go 侧通常使用 go-redis(已有专篇),而 C++ 采集网关(尤其是直接对接 FANUC FOCAS、三菱 MC 协议、西门子 snap7 的采集端)也需要一个可靠、高性能的 Redis 客户端来维护"点位最新值缓存"、"命令队列"、"网关分布式锁"。
1.2 为什么选择 hiredis 与 redis-plus-plus
C/C++ 生态的 Redis 客户端主要有三条路线:
| 库 | 语言 | 定位 | 特点 |
|---|---|---|---|
| hiredis | C | 官方推荐的底层客户端 | 极简、零依赖、同步+异步双模型、RESP 解析器独立可用;是 redis-plus-plus、很多框架(如 Drogon 的 redis 插件)的底层 |
| redis-plus-plus(sw/redis++) | C++17 | hiredis 之上的现代 C++ 封装 | Redis/RedisCluster 类、命令族方法、Optional 返回值、Pipeline/Transaction、连接池、订阅回调、Lua 脚本 |
| cpp-redis / acre | C++ | 第三方封装 | 生态小,维护不活跃 |
hiredis 适合对性能与控制力要求极高的场景(手写事件循环、嵌入式采集器);redis-plus-plus 适合工程化程度高的 C++ 项目(自动连接池、类型安全的命令 API、异常体系)。两者可以混用:redis-plus-plus 本身依赖 hiredis,你可以在一个工程里同时使用。
2. Redis 核心概念速览(客户端视角必须知道的部分)
2.1 RESP 协议(REdis Serialization Protocol)
hiredis 本质就是一个 RESP 协议解析器。客户端与 Redis 之间传输的每一条命令/回复都是 RESP 编码的文本帧:
| 类型 | 首字节 | 示例 | 说明 |
|---|---|---|---|
| Simple String | + | +OK\r\n | 状态回复 |
| Error | - | -ERR unknown command\r\n | 错误回复 |
| Integer | : | :1000\r\n | 整数回复(INCR 等) |
| Bulk String | $ | $5\r\nhello\r\n | 二进制安全的字符串 |
| Array | * | *2\r\n3\\r\\nfoo\\r\\n3\r\nbar\r\n | 多条回复 |
客户端发送命令时把参数构造成 Array 帧:*3\r\n3\\r\\nSET\\r\\n3\r\nfoo\r\n$3\r\nbar\r\n。
2.2 五种基本数据结构与底层编码
| 结构 | 常用命令 | 底层编码(Redis 7) |
|---|---|---|
| String | SET / GET / INCR / SETNX | int、embstr(≤44 字节)、raw |
| Hash | HSET / HGET / HGETALL / HINCRBY | listpack(小)、hashtable |
| List | LPUSH / RPOP / BRPOP / LLEN | quicklist(listpack 节点链表) |
| Set | SADD / SMEMBERS / SISMEMBER | intset(整数小集合)、hashtable |
| ZSet | ZADD / ZSCORE / ZREVRANGE / ZRANGEBYSCORE | listpack(小)、skiplist + dict |
此外还有 Bitmap、HyperLogLog、Geo、Stream 等扩展类型。
2.3 关键语义:TTL、过期与淘汰
- SET key val EX 30 / SETEX / EXPIRE:给实时点位最新值设置 TTL,自动过期清理"失联设备"的残留数据。
- Redis 的过期删除是惰性删除 + 定期删除,客户端不该依赖"过期后立刻不可见"(有窗口期)。
- 内存淘汰策略(maxmemory-policy)决定内存满时的行为,生产建议 allkeys-lru 或 noeviction(宁可报错不丢数据)。
3. API 说明
3.1 hiredis 核心 API
3.1.1 连接管理
cpp
#include <hiredis/hiredis.h>
redisContext *redisConnect(const char *ip, int port); // 同步连接
redisContext *redisConnectWithTimeout(const char *ip, int port,
const struct timeval tv); // 带连接超时
void redisFree(redisContext *c); // 释放连接
int redisSetTimeout(redisContext *c, const struct timeval tv); // 设置读写超时
redisContext 关键字段:
cpp
typedef struct redisContext {
int err; // 0=正常;REDIS_ERR_IO / REDIS_ERR_EOF / REDIS_ERR_PROTOCOL / REDIS_ERR_OOM / REDIS_ERR_OTHER
char errstr[128]; // 错误描述
int fd; // 套接字,-1 表示未连接
int flags; // REDIS_BLOCK / REDIS_CONNECTED 等
} redisContext;
3.1.2 同步命令
cpp
void *redisCommand(redisContext *c, const char *format, ...); // printf 风格格式化
void *redisCommandArgv(redisContext *c, int argc, const char **argv,
const size_t *argvlen); // 数组形式,二进制安全
// 返回值是 redisReply*,必须用 freeReplyObject() 释放
typedef struct redisReply {
int type; // REDIS_REPLY_STRING / ERROR / INTEGER / NIL / STATUS / ARRAY / DOUBLE ...
long long integer; // type==INTEGER 时有效
size_t len; // str 长度(二进制安全)
char *str; // type==STRING/ERROR/STATUS 时有效
size_t elements; // type==ARRAY 时元素个数
struct redisReply **element; // 元素数组
} redisReply;
void freeReplyObject(void *reply);
3.1.3 管道(Pipelining)
cpp
// 写入不等待回复
redisAppendCommand(c, "SET foo bar");
redisAppendCommand(c, "GET foo");
// 逐个读取(每个 Append 必须对应一次 GetReply)
redisReply *reply = NULL;
redisGetReply(c, (void **)&reply); // SET 的结果
freeReplyObject(reply);
redisGetReply(c, (void **)&reply); // GET 的结果
3.1.4 异步模型
cpp
redisAsyncContext *redisAsyncConnect(const char *ip, int port);
void redisAsyncFree(redisAsyncContext *ac);
int redisAsyncSetConnectCallback(redisAsyncContext *ac,
redisConnectCallback fn); // fn(ac, status)
int redisAsyncSetDisconnectCallback(redisAsyncContext *ac,
redisDisconnectCallback fn); // fn(ac, status)
int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn,
void *privdata, const char *format, ...);
// 事件循环驱动(核心!必须接入 epoll/select/libevent/uv 等)
void redisAsyncHandleRead(redisAsyncContext *ac);
void redisAsyncHandleWrite(redisAsyncContext *ac);
异步回调原型:void callback(redisAsyncContext *c, void *reply, void *privdata);(reply 用完调用释放函数;回调内不要阻塞)。
3.1.5 发布订阅(同步方式)
cpp
redisReply *reply = redisCommand(c, "SUBSCRIBE chan:alarm");
freeReplyObject(reply);
while (redisGetReply(c, (void **)&reply) == REDIS_OK) {
// reply->type == REDIS_REPLY_ARRAY
// element[0]=subscribe/message, element[1]=channel, element[2]=payload
freeReplyObject(reply);
}
3.2 redis-plus-plus 核心 API
3.2.1 连接与配置
3.2.2 String / Hash / List / Set / ZSet 命令族(节选)
cpp
// String
auto val = redis.get("foo"); // OptionalString,std::optional<std::string>
redis.set("foo", "bar");
redis.set("foo", "bar", std::chrono::seconds(30)); // 带 TTL
auto n = redis.incr("counter"); // 返回 long long
// Hash
redis.hset("dev:1", "axis_x", "123.45");
auto x = redis.hget("dev:1", "axis_x");
std::unordered_map<std::string, std::string> kv;
redis.hgetall("dev:1", std::inserter(kv, kv.begin()));
// List
redis.lpush("cmd:queue", "MOVE_AXIS 10");
auto item = redis.rpop("cmd:queue"); // OptionalString
redis.brpop("cmd:queue", std::chrono::seconds(1)); // 阻塞弹出
// Set / ZSet
redis.sadd("online_gateways", "gw-1");
bool online = redis.sismember("online_gateways", "gw-1");
redis.zadd("alarm_rank", "high", 10); // 注意:redis-plus-plus 的 zadd 参数顺序是 score, member
auto rank = redis.zrevrange("alarm_rank", 0, 9);
注意:redis-plus-plus 命令方法的参数顺序与原生 Redis 命令不一定一致(如 zadd(key, score, member)),以头文件签名 / IDE 提示为准,这是最常见的踩坑点之一。
3.2.3 Pipeline 与 Transaction
cpp
// Pipeline:批量发送、一次 exec() 取结果
auto pipe = redis.pipeline();
pipe.set("a", "1");
pipe.set("b", "2");
auto results = pipe.exec(); // vector<OptionalString> 等
// Transaction:MULTI/EXEC,可配合 WATCH
auto tx = redis.multi();
tx.set("a", "1");
tx.incr("counter");
auto tx_results = tx.exec();
3.2.4 订阅与 Lua
cpp
// 订阅(阻塞运行,回调在订阅线程执行,回调内勿阻塞)
redis.subscribe({"chan:alarm"}, [](const std::string &chan,
const std::string &msg) {
std::cout << chan << ": " << msg << std::endl;
});
// Lua 脚本(原子执行)
auto script = R"(
local v = redis.call('GET', KEYS[1])
if v == false then return 0 end
redis.call('DEL', KEYS[1])
return 1
)";
long long n = redis.eval(script, {"key"}, {}, 0); // 注意签名:eval(script, keys, args, num_keys)
3.2.5 RedisCluster
cpp
RedisCluster cluster("tcp://127.0.0.1:7000,tcp://127.0.0.1:7001,tcp://127.0.0.1:7002");
cluster.set("foo", "bar"); // 自动按 CRC16 路由到正确分片
4. 详细使用说明
4.0 环境搭建
# 方式一:vcpkg
# vcpkg install hiredis redis-plus-plus
find_package(hiredis CONFIG REQUIRED)
find_package(redis++ CONFIG REQUIRED)
target_link_libraries(app PRIVATE hiredis::hiredis redis++::redis++)
# 方式二:FetchContent(redis-plus-plus 需要 hiredis 作为依赖)
FetchContent_Declare(hiredis URL https://github.com/redis/hiredis/archive/refs/tags/v1.1.0.tar.gz)
FetchContent_Declare(redis-plus-plus URL https://github.com/sewenew/redis-plus-plus/archive/refs/tags/1.3.11.tar.gz)
4.1 hiredis 最小同步示例(点位读写)
cpp
#include <hiredis/hiredis.h>
#include <cstdio>
#include <cstdlib>
int main() {
redisContext *c = redisConnect("127.0.0.1", 6379);
if (c == nullptr || c->err) {
std::fprintf(stderr, "connect failed: %s\n", c ? c->errstr : "no mem");
if (c) redisFree(c);
return 1;
}
// SET
redisReply *r = (redisReply *)redisCommand(c, "SET dev:1:axis_x 123.45");
if (r == nullptr) { std::fprintf(stderr, "cmd error: %s\n", c->errstr); redisFree(c); return 1; }
freeReplyObject(r); // 必须释放!
// GET + 判断类型
r = (redisReply *)redisCommand(c, "GET dev:1:axis_x");
if (r->type == REDIS_REPLY_STRING)
std::printf("axis_x = %.*s\n", (int)r->len, r->str);
else
std::printf("not found / nil\n");
freeReplyObject(r);
// INCR 返回 INTEGER
r = (redisReply *)redisCommand(c, "INCR dev:1:cmd_seq");
if (r->type == REDIS_REPLY_INTEGER)
std::printf("seq = %lld\n", r->integer);
freeReplyObject(r);
redisFree(c);
return 0;
}
4.2 hiredis 管道批量写入(采集网关周期性批量上报最新值)
cpp
redisAppendCommand(c, "MSET dev:1:axis_x 100.1 dev:1:axis_y 200.2");
redisAppendCommand(c, "MSET dev:2:axis_x 300.3 dev:2:axis_y 400.4");
// 读取两个回复
redisReply *r = nullptr;
redisGetReply(c, (void **)&r); freeReplyObject(r);
redisGetReply(c, (void **)&r); freeReplyObject(r);
也可以直接用 redisCommandArgv 构造 MSET 参数数组(参数值无需转义、天然二进制安全)。
4.3 hiredis 异步 + 事件循环(对接 libuv 风格集成)
cpp
void onReply(redisAsyncContext *ac, void *reply, void *privdata) {
if (reply == nullptr) { /* 连接已断开 */ return; }
redisReply *r = (redisReply *)reply;
if (r->type == REDIS_REPLY_STRING) {
std::printf("async got: %.*s\n", (int)r->len, r->str);
}
// hiredis 异步回调里 reply 无需手动释放(框架会处理),注意别 freeReplyObject!
}
int main() {
redisAsyncContext *ac = redisAsyncConnect("127.0.0.1", 6379);
if (ac->err) { std::fprintf(stderr, "async connect: %s\n", ac->errstr); return 1; }
redisAsyncSetConnectCallback(ac, [](redisAsyncContext *c, int status) {
if (status != REDIS_OK) { std::fprintf(stderr, "connect fail\n"); return; }
redisAsyncCommand(c, onReply, nullptr, "GET dev:1:axis_x");
});
redisAsyncSetDisconnectCallback(ac, [](redisAsyncContext *c, int status) {
std::fprintf(stderr, "disconnected: %d\n", status);
});
// 事件循环驱动(示意):libevent / libuv / 自行 select
// 注册 ac->fd 的可读/可写事件后分别调用 redisAsyncHandleRead / redisAsyncHandleWrite
// ...
redisAsyncFree(ac);
return 0;
}
4.4 redis-plus-plus 工程化示例:连接池 + 管道 + 分布式锁(工业数采)
cpp
#include <sw/redis++/redis++.h>
#include <iostream>
using namespace sw::redis;
int main() {
ConnectionOptions opts;
opts.host = "127.0.0.1";
opts.port = 6379;
opts.socket_timeout = std::chrono::milliseconds(300);
opts.connect_timeout = std::chrono::milliseconds(200);
ConnectionPoolOptions pool_opts;
pool_opts.pool_size = 16;
pool_opts.wait_timeout = std::chrono::milliseconds(200);
Redis redis(opts, pool_opts);
// 1) 点位最新值 + TTL
redis.set("dev:1:axis_x", "123.45", std::chrono::seconds(60));
auto v = redis.get("dev:1:axis_x");
if (v) std::cout << "axis_x=" << *v << std::endl;
// 2) 管道批量写入
auto pipe = redis.pipeline();
pipe.set("dev:1:axis_y", "456.78");
pipe.set("dev:1:axis_z", "789.01");
pipe.expire("dev:1:axis_y", std::chrono::seconds(60));
auto res = pipe.exec();
// 3) 分布式锁(Lua 保证原子性:SETNX + EXPIRE)
auto lock_script = R"(
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
else
return 0
end
)";
long long locked = redis.eval(lock_script, {"gw:lock:axis"}, {"gw-1", "10"}, 1);
if (locked == 1) std::cout << "lock acquired" << std::endl;
// 释放:先校验 token 再 DEL(避免误删他人锁)
auto unlock_script = R"(
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
)";
redis.eval(unlock_script, {"gw:lock:axis"}, {"gw-1"}, 1);
// 4) 实时告警 PubSub
redis.publish("chan:alarm", "axis_x overflow");
return 0;
}
4.5 工业数采链路建议(C++ 采集网关视角)
FANUC/MC/snap7 设备
│ FOCAS / MC 协议 / S7 协议
▼
C++ 采集网关
│ hiredis / redis-plus-plus
▼
Redis 实时层(点位最新值 TTL / 命令下发队列 List / 网关分布式锁 / 告警 PubSub)
│ 定时批量
▼
Kafka(历史流)→ TDengine(时序落库)→ 分析 / 告警平台
- 点位最新值:String + TTL,键 dev:{id}:{point}。
- 命令下发:List cmd:{dev},采集线程 BRPOP 阻塞取命令执行后回写状态。
- 多网关互斥:Lua 分布式锁,避免多网关同时下发冲突命令。
- 实时告警:PubSub chan:alarm,监控线程订阅并转 MQTT/日志。
5. 底层实现剖析
5.1 hiredis 结构:连接层与解析器分离
hiredis 的核心设计是同步/异步共用同一套 RESP 解析器:
- net.c:非阻塞 socket 建立、超时设置(SO_RCVTIMEO / SO_SNDTIMEO 或 poll 轮询)。
- reader(read.c):纯内存状态机,输入字节流,输出一棵 redisReply 树。不依赖 socket,所以异步模型复用同一解析器。
- redisContext 的 reader 字段是解析器实例;obuf 是待发送缓冲。
5.2 RESP 解析状态机(reader 内部要点)
解析器按首字节分派:
- 读到 (Bulk String):先读 <len>\r\n,若 len == -1 表示 nil,否则读 len 字节 + \r\n,构造 REDIS_REPLY_STRING。
- 读到 *(Array):先读 *<count>\r\n,若 count == -1 表示 nil array,否则递归解析 count 个元素,构造 REDIS_REPLY_ARRAY。
- 读到 + / - / ::读一行,分别是 STATUS / ERROR / INTEGER。
每次 redisGetReply 会尝试从 reader 取一棵完整回复树;若字节流不完整(半包),返回 REDIS_ERR_IO(err=REDIS_ERR_EOF 语义),下次再调用继续解析剩余字节------这就是"粘包/半包"天然被 hiredis 处理掉的原因。
5.3 异步模型:事件循环驱动
- redisAsyncContext 包装 redisContext,把命令封装成 redisCallback 链表挂在连接上。
- redisAsyncCommand 只是把命令追加到输出缓冲;真正发送发生在事件循环调用 redisAsyncHandleWrite(fd 可写)时。
- 回复到达后 redisAsyncHandleRead 解析,按命令顺序触发回调(hiredis 用 FIFO 保证命令-回调配对)。
- 没有事件循环 = 异步永远不会推进,这是初学 hiredis 最大的困惑来源。
5.4 redis-plus-plus 的封装层次
- Redis 持有 ConnectionPool(hiredis 连接数组 + 互斥借用归还),每个命令方法从池中借连接执行再归还。
- RedisCluster 内部维护 slot → node 映射(16384 个 CRC16 slot),收到 MOVED/ASK 后自动重路由。
- Pipeline/Transaction 封装成 PipelineImpl/TransactionImpl,延迟到 exec() 统一发送+收集。
- 异常体系:Error、TimeoutError、OOMError、ClosedError、IoError 等,所有命令可能抛 Error,工程上必须捕获。
6. 常错点 / 坑(Top 20)
| 坑点 | 后果 | 正确姿势 | |
|---|---|---|---|
| 1 | 不检查 redisContext 的 err / NULL | 空指针解引用、静默失败 | 连接后立即检查 c && !c->err |
| 2 | redisCommand 返回 NULL 还继续访问 | 崩溃 / 未定义行为 | 判空后先看 c->errstr |
| 3 | redisReply 用完不 freeReplyObject | 内存泄漏(长连接下尤其严重) | 每条回复必释放 |
| 4 | 不判断 r->type 直接用 r->str | 类型错读(INTEGER 无 str) | 按 type 分派处理 |
| 5 | 管道 Append 次数与 GetReply 次数不匹配 | 回复错位、逻辑错误 | 每 Append 必须有对应 GetReply |
| 6 | 异步回调里 freeReplyObject(reply) | 二次释放崩溃 | 异步回调 reply 由框架管理 |
| 7 | 异步连接不接入任何事件循环 | 命令永远发不出去/收不到 | 注册 fd 可读可写事件并调用 HandleRead/Write |
| 8 | 多线程共享同一个同步 context | 数据竞争、协议错乱 | 每线程独立连接,或用异步+互斥 |
| 9 | 同步订阅后在同一连接上再发普通命令 | 阻塞、命令排队无人消费 | 订阅用专用连接/线程 |
| 10 | 未设超时(redisSetTimeout) | 网络抖动时无限阻塞 | 显式设置读写超时 |
| 11 | 只查 connect 成功就以为一切 OK | 密码错误/AUTH 失败在首次命令才暴露 | 连接后主动 PING |
| 12 | Cluster 直连单节点 | MOVED/ASK 错误 | 用 Cluster 客户端或处理重定向 |
| 13 | get 返回值不判空直接解引用 | 未命中时崩溃 | 用 if (val) / val.has_value() |
| 14 | zadd(key, member, score) 参数顺序记反 | 数据写错 | 顺序是 zadd(key, score, member) |
| 15 | 命令抛异常不捕获 | 进程中断 | 捕获 sw::redis::Error 系列 |
| 16 | 池耗尽 + wait_timeout 设 0 | 快速失败或无限等待 | 评估峰值并发合理配置 |
| 17 | brpop 入参顺序错误 | 弹出错误方向/顺序错乱 | 按签名传(先 key 后 timeout 语义看 IDE) |
| 18 | 订阅回调里执行阻塞/耗时操作 | 卡住整个订阅线程、丢消息 | 回调只做投递,用队列交给工作线程 |
| 19 | 多个命令想原子执行却用多个调用 | 中间状态可见 | 用 Lua / Transaction |
| 20 | 误把 exec() 返回值当单个结果 | 类型不匹配编译/运行错误 | Pipeline 返回 vector,Transaction 同理 |
7. 性能优化清单
- 管道化批量:采集网关 100ms 周期把 N 个点位 MSET/Pipeline 一次发送,吞吐可提升 10~50 倍。
- 连接池:redis-plus-plus 默认池化;hiredis 自行维护连接复用,避免每次命令建连。
- 避免大 Key:单 Key 超过几十 KB 会拖慢整个实例;点位数据按点拆 Key。
- 合理 TTL:实时点位设 TTL,防止僵尸 Key 堆积。
- 序列化最小化:数值点位用字符串化数字即可,不要套 JSON。
- Lua 原子化:分布式锁/计数/校验用 Lua,省 RTT 且原子。
- 订阅线程独立:PubSub 高频消息不要与业务命令共用连接。
- 关闭 AOF 或调整策略:纯缓存场景可 appendonly no;需要持久化用 everysec。
- 客户端侧超时收敛:socket_timeout 统一几百 ms,避免雪崩式阻塞。
- 监控:INFO stats / SLOWLOG 治理慢命令;客户端侧统计命令耗时。
8. FAQ 速查表
Q1: hiredis 和 redis-plus-plus 该选哪个? A: 需要极致控制力/零依赖/嵌入式 → hiredis;工程化开发(连接池、类型安全、Cluster、订阅封装)→ redis-plus-plus。两者可同工程混用。
Q2: hiredis 同步 API 是线程安全的吗? A: 不是。一个 context 只能被一个线程使用;多线程请各自建连接或加锁。
Q3: 为什么异步连接明明 connect 成功却收不到回调? A: 因为没有事件循环驱动 redisAsyncHandleRead/Write。必须把 ac->fd 注册到 epoll/select/libevent/libuv。
Q4: redis-plus-plus 的 Redis 对象是线程安全的吗? A: 是的,Redis 内部用连接池 + 互斥保证多线程安全,可跨线程共享。
Q5: Pipeline 和 Transaction 的区别? A: Pipeline 只做批量发送减少 RTT,不保证原子;Transaction(MULTI/EXEC)保证原子性。
Q6: 为什么用 Lua 做分布式锁? A: SETNX + EXPIRE 两条命令非原子,进程崩溃会死锁;Lua 单次执行原子,配合 token 校验释放避免误删。
Q7: Redis 会丢数据吗? A: 默认内存为主。纯缓存可接受;要求可靠性用 AOF everysec 或直接走 Kafka 历史流。
Q8: 大流量采集网关怎么组织 Key? A: 每设备每点位独立 Key + TTL;批量上报用 Pipeline/MSET;命令下发用 List + BRPOP。
9. 总结
C++ 侧访问 Redis 的主流路径是 hiredis(官方 C 客户端,RESP 解析器 + 同步/异步双模型) 与其上层的 redis-plus-plus(C++17 现代封装)。对工业数采网关而言,Redis 实时层承担"点位最新值、命令下发、分布式锁、实时告警"四类职责,与 Kafka/TDengine 的历史流形成"实时 + 历史"双通道,与 Go 侧 go-redis 构成跨语言对照。掌握本文的 API 分层、RESP 解析原理、异步事件驱动模型与 20 条高频坑点,即可在生产采集链路上稳定落地。