Files
microser/redisstream/RedisStreamMQ.cpp

1263 lines
40 KiB
C++
Raw Normal View History

2026-06-24 16:37:32 +08:00
#include "RedisStreamMQ.h"
#include "../rocketmq/SimpleProducer.h"
#include <hiredis/hiredis.h>
#include <zlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include <set>
#include <algorithm>
#include <stdexcept>
#include <ctime>
#include <cstdlib>
#include <cstdio>
#include "../log4cplus/log4.h"
#include "../mms/db_interface.h"
2026-06-24 16:37:32 +08:00
/*
* lnk20260622 Redis Streams
*
* RedisStream_xxx
* InitializeProducer / ShutdownAndDestroyProducer
* rocketmq_producer_send
* InitializeConsumer / ShutdownAndDestroyConsumer
* rocketmq_consumer_receive
*
* SimpleProducer.cpp [MQ] Backend
* RocketMQ RedisStream
*/
extern std::string G_ROCKETMQ_PRODUCER; // 保留原配置名Redis 模式下仅用于日志
extern std::string G_ROCKETMQ_IPPORT; // Redis 模式下建议填 127.0.0.1:6379[/db]
extern std::string G_MQCONSUMER_IPPORT; // Redis 模式下消费者连接地址
extern std::string G_ROCKETMQ_CONSUMER; // Redis consumer group
extern std::string FRONT_INST;
// lnk20260626 修改Redis 模式复用 RocketMq/ConsumerSecretKey 作为 Redis 密码
extern std::string G_MQCONSUMER_SECRETKEY;
2026-06-24 16:37:32 +08:00
extern int g_front_seg_index;
extern char subdir[128];
static const int64_t REDIS_BLOCK_MS = 1000; //XREADGROUP 阻塞等待时间
static const int64_t REDIS_IDLE_CLAIM_MS = 60000; //消息空闲多久被其他消费者抢占
static const int REDIS_READ_COUNT = 16; //每次 XREADGROUP 的最大读取数量
static const size_t GZIP_THRESHOLD_BYTES = 1024; //超过这个大小的消息才压缩
static const char* SPOOL_DIR = "/FeProject/data/redisstream_spool"; //本地缓存目录
// lnk20260626 修改XADD 退避与 Redis 内存保护
static const int64_t REDIS_XADD_OOM_BACKOFF_MS = 30000; // OOM 后 30 秒内不再 XADD
static const int64_t REDIS_XADD_FAIL_BACKOFF_MS = 5000; // 普通连接失败后 5 秒退避
static const int64_t REDIS_MEMORY_CHECK_INTERVAL_MS = 5000; // 每 5 秒检查一次 Redis 内存
static const int REDIS_MEMORY_HIGH_WATERMARK = 85; // Redis 内存超过 85% 后延迟 XADD
static const int64_t REDIS_SPOOL_FLUSH_INTERVAL_MS = 2000; // spool 最多 2 秒补发一次
static const int REDIS_SPOOL_FLUSH_LIMIT = 50; // 每次最多补发 50 条,防止恢复后瞬间冲击 Redis
2026-06-24 16:37:32 +08:00
static int64_t now_ms() //当前时间,单位毫秒
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
/*
* RocketMQ SimpleProducer.cpp G_APP_START_MS / G_START_SKEW_MS
*
*/
static int64_t redis_app_start_ms()//RedisStream 后端的程序启动时间
{
static int64_t v = now_ms();
return v;
}
static const int64_t REDIS_START_SKEW_MS = 1000;
static std::string to_string_i64(int64_t v)
{
char buf[64];
snprintf(buf, sizeof(buf), "%lld", (long long)v);
return std::string(buf);
}
static std::string shell_safe_name(const std::string& s)
{
std::string r;
for (size_t i = 0; i < s.size(); ++i) {
unsigned char c = (unsigned char)s[i];
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') {
r.push_back((char)c);
} else {
r.push_back('_');
}
}
return r.empty() ? "empty" : r;
}
static std::string build_stream_name(const std::string& topic, const std::string& env)//构造stream名称格式env_shortTopic
{
if (!topic.empty() && topic.find('%') != std::string::npos) {
std::string shortTopic = topic.substr(topic.find('%') + 1);
if (!env.empty()) return env + "_" + shortTopic;
return shortTopic;
}
if (!env.empty()) {
const std::string prefix = env + "_";
if (topic.find(prefix) == 0) return topic;
return prefix + topic;
}
return topic;
}
static void ensure_dir(const std::string& dir)//创建目录,如果不存在
{
if (dir.empty()) return;
if (access(dir.c_str(), F_OK) == 0) return;
if (mkdir(dir.c_str(), 0755) != 0 && errno != EEXIST) {
std::cerr << "[REDIS_STREAM][SPOOL] mkdir failed: " << dir
<< " errno=" << errno << " " << strerror(errno) << std::endl;
}
}
static std::string base64_encode(const std::string& input)//编码为 base64
{
static const char* table =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
int val = 0;
int valb = -6;
for (size_t i = 0; i < input.size(); ++i) {
unsigned char c = (unsigned char)input[i];
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(table[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(table[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
static std::string gzip_compress(const std::string& input)//压缩字符串,返回 gzip 格式
{
if (input.empty()) return std::string();
z_stream zs;
memset(&zs, 0, sizeof(zs));
static bool zlib_version_logged = false;
if (!zlib_version_logged) {
zlib_version_logged = true;
std::cerr << "[REDIS_STREAM][ZLIB] compile=" << ZLIB_VERSION
<< " runtime=" << zlibVersion()
<< " z_stream_size=" << sizeof(z_stream)
<< std::endl;
}
2026-06-24 16:37:32 +08:00
int ret = deflateInit2(&zs, Z_BEST_SPEED, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
if (ret != Z_OK) {
char buf[64];
snprintf(buf, sizeof(buf), "deflateInit2 failed ret=%d", ret);
throw std::runtime_error(buf);
2026-06-24 16:37:32 +08:00
}
zs.next_in = (Bytef*)input.data();
zs.avail_in = (uInt)input.size();
char outbuffer[32768];
std::string out;
do {
zs.next_out = reinterpret_cast<Bytef*>(outbuffer);
zs.avail_out = sizeof(outbuffer);
ret = deflate(&zs, Z_FINISH);
if (out.size() < zs.total_out) {
out.append(outbuffer, zs.total_out - out.size());
}
} while (ret == Z_OK);
deflateEnd(&zs);
if (ret != Z_STREAM_END) {
throw std::runtime_error("gzip deflate failed");
}
return out;
}
static bool gzip_decompress(const std::string& input, std::string& output) //解压缩 gzip 格式字符串
{
output.clear();
if (input.empty()) return true;
z_stream zs;
memset(&zs, 0, sizeof(zs));
int ret = inflateInit2(&zs, 15 + 32);
if (ret != Z_OK) return false;
zs.next_in = (Bytef*)input.data();
zs.avail_in = (uInt)input.size();
char outbuffer[32768];
do {
zs.next_out = reinterpret_cast<Bytef*>(outbuffer);
zs.avail_out = sizeof(outbuffer);
ret = inflate(&zs, 0);
if (output.size() < zs.total_out) {
output.append(outbuffer, zs.total_out - output.size());
}
if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
inflateEnd(&zs);
return false;
}
} while (ret != Z_STREAM_END);
inflateEnd(&zs);
return true;
}
static int base64_value(char c)
{
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
static bool base64_decode(const std::string& input, std::string& output) //解码 base64
{
output.clear();
int val = 0;
int valb = -8;
for (size_t i = 0; i < input.size(); ++i) {
char c = input[i];
if (c == '=') break;
int d = base64_value(c);
if (d == -1) {
if (c == '\r' || c == '\n' || c == ' ' || c == '\t') continue;
return false;
}
val = (val << 6) + d;
valb += 6;
if (valb >= 0) {
output.push_back(char((val >> valb) & 0xFF));
valb -= 8;
}
}
return true;
}
static bool decode_body(const std::string& enc, //解压缩
const std::string& body,
std::string& out)
{
if (enc == "gzip") {
std::string gz;
if (!base64_decode(body, gz)) return false;
return gzip_decompress(gz, out);
}
out = body;
return true;
}
static void encode_body(const std::string& json,
std::string& enc,
std::string& body)
{
static bool gzip_disabled = false;
if (json.size() >= GZIP_THRESHOLD_BYTES && !gzip_disabled) {
try {
enc = "gzip";
2026-06-24 16:37:32 +08:00
body = base64_encode(gzip_compress(json)); //大的压缩后编码
} catch (const std::exception& e) {
std::cerr << "[REDIS_STREAM][GZIP_FALLBACK] " << e.what()
<< ", send plain body" << std::endl;
if (std::string(e.what()).find("ret=-2") != std::string::npos) {
gzip_disabled = true;
std::cerr << "[REDIS_STREAM][GZIP_DISABLED] zlib init stream error, keep sending plain body"
<< std::endl;
}
enc = "plain";
body = json;
}
2026-06-24 16:37:32 +08:00
} else {
enc = "plain";
body = json; //小的保持json
}
}
static std::string parse_host(const std::string& addr)//解析主机地址,去掉协议、端口和数据库部分
{
std::string s = addr;
if (s.find("redis://") == 0) s = s.substr(8);
size_t slash = s.find('/');
if (slash != std::string::npos) s = s.substr(0, slash);
size_t colon = s.rfind(':');
if (colon == std::string::npos) return s;
return s.substr(0, colon);
}
static int parse_port(const std::string& addr)//提取端口
{
std::string s = addr;
if (s.find("redis://") == 0) s = s.substr(8);
size_t slash = s.find('/');
if (slash != std::string::npos) s = s.substr(0, slash);
size_t colon = s.rfind(':');
if (colon == std::string::npos) return 6379;
return atoi(s.substr(colon + 1).c_str());
}
static int parse_db(const std::string& addr) //提取数据库
{
size_t slash = addr.rfind('/');
if (slash == std::string::npos) return -1;
std::string db = addr.substr(slash + 1);
if (db.empty()) return -1;
for (size_t i = 0; i < db.size(); ++i) {
if (!isdigit((unsigned char)db[i])) return -1;
}
return atoi(db.c_str());
}
class RedisConnection { //Redis 连接封装
public:
RedisConnection() : ctx_(NULL), db_(-1) {}
~RedisConnection() { close(); }
void configure(const std::string& addr)
{
addr_ = addr;
host_ = parse_host(addr);
port_ = parse_port(addr);
db_ = parse_db(addr);
// lnk20260626 修改Redis 模式复用 RocketMq/ConsumerSecretKey 作为密码
password_ = G_MQCONSUMER_SECRETKEY;
2026-06-24 16:37:32 +08:00
if (host_.empty()) host_ = "127.0.0.1";
}
redisContext* get()
{
if (ctx_ && ctx_->err == 0) return ctx_;
reconnect();
return ctx_;
}
redisReply* command(const char* fmt, ...)
{
redisContext* c = get();
if (!c) return NULL;
va_list ap;
va_start(ap, fmt);
void* r = redisvCommand(c, fmt, ap);
va_end(ap);
if (!r) {
close();
return NULL;
}
return (redisReply*)r;
}
bool ping()
{
redisReply* r = command("PING");
if (!r) return false;
bool ok = (r->type == REDIS_REPLY_STATUS && r->str && std::string(r->str) == "PONG");
freeReplyObject(r);
return ok;
}
void close()
{
if (ctx_) {
redisFree(ctx_);
ctx_ = NULL;
}
}
private:
void reconnect()
{
close();
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 500000;
ctx_ = redisConnectWithTimeout(host_.c_str(), port_, tv);
if (!ctx_ || ctx_->err) {
if (ctx_) {
std::cerr << "[REDIS_STREAM] connect failed: " << ctx_->errstr << std::endl;
close();
} else {
std::cerr << "[REDIS_STREAM] connect failed: null context" << std::endl;
}
return;
}
redisSetTimeout(ctx_, tv);
// lnk20260626 修改Redis 密码认证
if (!password_.empty()) {
redisReply* r = (redisReply*)redisCommand(
ctx_,
"AUTH %b",
password_.data(),
password_.size());
if (!r || r->type == REDIS_REPLY_ERROR) {
std::cerr << "[REDIS_STREAM] auth failed" << std::endl;
if (r) {
if (r->str) {
std::cerr << "[REDIS_STREAM] auth error: " << r->str << std::endl;
}
freeReplyObject(r);
}
close();
return;
}
freeReplyObject(r);
}
2026-06-24 16:37:32 +08:00
if (db_ >= 0) {
redisReply* r = (redisReply*)redisCommand(ctx_, "SELECT %d", db_);
if (!r || (r->type == REDIS_REPLY_ERROR)) {
std::cerr << "[REDIS_STREAM] select db failed: " << db_ << std::endl;
if (r) freeReplyObject(r);
close();
return;
}
freeReplyObject(r);
}
}
private:
redisContext* ctx_;
std::string addr_;
std::string host_;
std::string password_; // lnk20260626 修改Redis AUTH 密码
2026-06-24 16:37:32 +08:00
int port_;
int db_;
};
// lnk20260626 修改:区分 XADD 成功、OOM、普通失败
enum RedisXAddResult {
REDIS_XADD_OK = 0,
REDIS_XADD_OOM = 1,
REDIS_XADD_FAIL = 2
};
2026-06-24 16:37:32 +08:00
class RedisStreamProducer { //RedisStream 生产者封装
public:
explicit RedisStreamProducer(const std::string& addr)
: xadd_pause_until_ms_(0),
last_flush_ms_(0),
last_memory_check_ms_(0),
cached_memory_high_(false),
last_pressure_log_ms_(0)
2026-06-24 16:37:32 +08:00
{
conn_.configure(addr);
ensure_dir(SPOOL_DIR);
if (!conn_.ping()) {
std::cerr << "[REDIS_STREAM][PRODUCER] Redis not ready, spool enabled" << std::endl;
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_FAIL_BACKOFF_MS;
2026-06-24 16:37:32 +08:00
}
}
void sendMessage(const std::string& json,
const std::string& topic,
const std::string& tag,
const std::string& key)
{
const int64_t n = now_ms();
2026-06-24 16:37:32 +08:00
std::string enc;
std::string body;
encode_body(json, enc, body);
const std::string stream = build_stream_name(topic, tag);
const std::string ts = to_string_i64(n);
/*
* lnk20260626
* Redis 退 XADD spool
* XADD
*/
if (n < xadd_pause_until_ms_) {
spool(stream, key, tag, enc, body, ts);
logPressureOnce("[REDIS_STREAM][DELAY_XADD] redis in backoff, message spooled");
return;
}
/*
* lnk20260626
* Redis XADD OOM
*/
if (isRedisMemoryHigh()) {
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_OOM_BACKOFF_MS;
spool(stream, key, tag, enc, body, ts);
logPressureOnce("[REDIS_STREAM][MEM_HIGH] redis memory high, delay xadd and spool message");
return;
}
/*
* lnk20260626
* spool flush Redis
*/
if (n - last_flush_ms_ >= REDIS_SPOOL_FLUSH_INTERVAL_MS) {
flushSpool();
last_flush_ms_ = n;
}
2026-06-24 16:37:32 +08:00
RedisXAddResult ret = xadd(stream, key, tag, enc, body, ts);
if (ret == REDIS_XADD_OK) {
return;
}
2026-06-24 16:37:32 +08:00
/*
* lnk20260626
* XADD OOM 退 spool
*/
if (ret == REDIS_XADD_OOM) {
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_OOM_BACKOFF_MS;
2026-06-24 16:37:32 +08:00
spool(stream, key, tag, enc, body, ts);
logPressureOnce("[REDIS_STREAM][OOM_BACKOFF] XADD OOM, delay xadd and spool message");
return;
2026-06-24 16:37:32 +08:00
}
/*
* 退
*/
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_FAIL_BACKOFF_MS;
spool(stream, key, tag, enc, body, ts);
logPressureOnce("[REDIS_STREAM][XADD_FAIL_BACKOFF] XADD failed, message spooled");
2026-06-24 16:37:32 +08:00
}
void shutdown() {}
private:
void logPressureOnce(const char* msg)
{
const int64_t n = now_ms();
/*
* 10 OOM
*/
if (n - last_pressure_log_ms_ >= 10000) {
last_pressure_log_ms_ = n;
std::cerr << msg << std::endl;
DIY_ERRORLOG_CODE("process", 0, LOG_CODE_MQ,
"【WARN】前置的%s%d号进程 Redis Stream写入延迟消息已落本地缓存",
get_front_msg_from_subdir(), g_front_seg_index);
}
}
static bool parse_info_number(const std::string& info,
const std::string& key,
long long& value)
{
value = 0;
std::string pattern = key + ":";
size_t pos = info.find(pattern);
if (pos == std::string::npos) return false;
pos += pattern.size();
size_t end = info.find_first_of("\r\n", pos);
std::string num = info.substr(pos, end == std::string::npos ? std::string::npos : end - pos);
if (num.empty()) return false;
value = atoll(num.c_str());
return value > 0;
}
bool isRedisMemoryHigh()
{
const int64_t n = now_ms();
if (n - last_memory_check_ms_ < REDIS_MEMORY_CHECK_INTERVAL_MS) {
return cached_memory_high_;
}
last_memory_check_ms_ = n;
cached_memory_high_ = false;
redisReply* r = conn_.command("INFO memory");
if (!r) {
return false;
}
if (r->type != REDIS_REPLY_STRING || !r->str) {
freeReplyObject(r);
return false;
}
std::string info(r->str, r->len);
freeReplyObject(r);
long long used_memory = 0;
long long maxmemory = 0;
if (!parse_info_number(info, "used_memory", used_memory)) {
return false;
}
if (!parse_info_number(info, "maxmemory", maxmemory)) {
return false;
}
/*
* maxmemory=0 Redis
* 2G maxmemory
*/
if (maxmemory <= 0) {
return false;
}
if (used_memory * 100 >= maxmemory * REDIS_MEMORY_HIGH_WATERMARK) {
cached_memory_high_ = true;
std::cerr << "[REDIS_STREAM][MEM_HIGH] used_memory=" << used_memory
<< " maxmemory=" << maxmemory
<< " percent=" << (used_memory * 100 / maxmemory)
<< "% watermark=" << REDIS_MEMORY_HIGH_WATERMARK
<< "%" << std::endl;
}
return cached_memory_high_;
}
RedisXAddResult xadd(const std::string& stream,
const std::string& key,
const std::string& tag,
const std::string& enc,
const std::string& body,
const std::string& ts)
2026-06-24 16:37:32 +08:00
{
redisReply* r = conn_.command(
"XADD %b * key %b tag %b enc %b body %b ts %b",
stream.data(), stream.size(),
key.data(), key.size(),
tag.data(), tag.size(),
enc.data(), enc.size(),
body.data(), body.size(),
ts.data(), ts.size());
if (!r) {
return REDIS_XADD_FAIL;
}
if (r->type == REDIS_REPLY_STRING) {
freeReplyObject(r);
return REDIS_XADD_OK;
}
if (r->type == REDIS_REPLY_ERROR) {
std::string err = r->str ? r->str : "";
std::cerr << "[REDIS_STREAM][XADD_ERROR] " << err << std::endl;
2026-06-24 16:37:32 +08:00
freeReplyObject(r);
/*
* lnk20260626
* noeviction + maxmemory Redis OOM
*/
if (err.find("OOM") != std::string::npos ||
err.find("maxmemory") != std::string::npos ||
err.find("command not allowed") != std::string::npos) {
return REDIS_XADD_OOM;
}
return REDIS_XADD_FAIL;
2026-06-24 16:37:32 +08:00
}
2026-06-24 16:37:32 +08:00
freeReplyObject(r);
return REDIS_XADD_FAIL;
2026-06-24 16:37:32 +08:00
}
void spool(const std::string& stream,
const std::string& key,
const std::string& tag,
const std::string& enc,
const std::string& body,
const std::string& ts)
{
ensure_dir(SPOOL_DIR);
char uniq[128];
snprintf(uniq, sizeof(uniq), "%lld_%d_%llu",
(long long)now_ms(),
(int)getpid(),
(unsigned long long)rand());
std::string fn = std::string(SPOOL_DIR) + "/" + uniq + "_" +
shell_safe_name(stream) + "_" + shell_safe_name(key) + ".msg.tmp";
2026-06-24 16:37:32 +08:00
std::string done = fn.substr(0, fn.size() - 4);
std::ofstream out(fn.c_str(), std::ios::out | std::ios::binary);
if (!out) {
std::cerr << "[REDIS_STREAM][SPOOL] open failed: " << fn << std::endl;
return;
}
out << "stream=" << stream << "\n";
out << "key=" << key << "\n";
out << "tag=" << tag << "\n";
out << "enc=" << enc << "\n";
out << "ts=" << ts << "\n";
out << "body=" << body << "\n";
out.close();
if (rename(fn.c_str(), done.c_str()) != 0) {
std::cerr << "[REDIS_STREAM][SPOOL] rename failed errno=" << errno << std::endl;
}
}
static bool parse_spool_file(const std::string& path,
std::string& stream,
std::string& key,
std::string& tag,
std::string& enc,
std::string& body,
std::string& ts)
{
std::ifstream in(path.c_str(), std::ios::in | std::ios::binary);
if (!in) return false;
std::string line;
while (std::getline(in, line)) {
if (line.find("stream=") == 0) stream = line.substr(7);
else if (line.find("key=") == 0) key = line.substr(4);
else if (line.find("tag=") == 0) tag = line.substr(4);
else if (line.find("enc=") == 0) enc = line.substr(4);
else if (line.find("ts=") == 0) ts = line.substr(3);
else if (line.find("body=") == 0) {
body = line.substr(5);
std::string rest;
while (std::getline(in, rest)) {
body += "\n";
body += rest;
}
break;
}
}
return !stream.empty() && !enc.empty() && !ts.empty();
}
void flushSpool()
{
const int64_t n = now_ms();
if (n < xadd_pause_until_ms_) {
return;
}
if (isRedisMemoryHigh()) {
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_OOM_BACKOFF_MS;
logPressureOnce("[REDIS_STREAM][SPOOL_DELAY] redis memory high, stop flushing spool");
return;
}
2026-06-24 16:37:32 +08:00
DIR* dir = opendir(SPOOL_DIR);
if (!dir) return;
std::vector<std::string> files;
struct dirent* ent = NULL;
while ((ent = readdir(dir)) != NULL) {
std::string name = ent->d_name;
if (name == "." || name == "..") continue;
if (name.size() >= 4 && name.substr(name.size() - 4) == ".tmp") continue;
files.push_back(std::string(SPOOL_DIR) + "/" + name);
}
closedir(dir);
std::sort(files.begin(), files.end());
int flushed = 0;
2026-06-24 16:37:32 +08:00
for (size_t i = 0; i < files.size(); ++i) {
if (flushed >= REDIS_SPOOL_FLUSH_LIMIT) {
break;
}
2026-06-24 16:37:32 +08:00
std::string stream, key, tag, enc, body, ts;
if (!parse_spool_file(files[i], stream, key, tag, enc, body, ts)) {
continue;
}
RedisXAddResult ret = xadd(stream, key, tag, enc, body, ts);
if (ret == REDIS_XADD_OK) {
2026-06-24 16:37:32 +08:00
unlink(files[i].c_str());
++flushed;
2026-06-24 16:37:32 +08:00
std::cout << "[REDIS_STREAM][SPOOL_FLUSHED] " << files[i] << std::endl;
continue;
}
if (ret == REDIS_XADD_OOM) {
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_OOM_BACKOFF_MS;
logPressureOnce("[REDIS_STREAM][SPOOL_OOM] XADD OOM while flushing spool");
2026-06-24 16:37:32 +08:00
break;
}
xadd_pause_until_ms_ = now_ms() + REDIS_XADD_FAIL_BACKOFF_MS;
logPressureOnce("[REDIS_STREAM][SPOOL_FAIL] XADD failed while flushing spool");
break;
2026-06-24 16:37:32 +08:00
}
}
private:
RedisConnection conn_;
// lnk20260626 修改:延迟 XADD 控制
int64_t xadd_pause_until_ms_;
int64_t last_flush_ms_;
// lnk20260626 修改Redis 内存高水位缓存
int64_t last_memory_check_ms_;
bool cached_memory_high_;
// lnk20260626 修改:压力日志限频
int64_t last_pressure_log_ms_;
2026-06-24 16:37:32 +08:00
};
class RedisStreamConsumer { //RedisStream 消费者封装
public:
RedisStreamConsumer(const std::string& group, const std::string& addr)
: group_(group), stop_(false)
{
conn_.configure(addr);
if (group_.empty()) group_ = "front_default";
int64_t pid = (int64_t)getpid();
int64_t start_ms = now_ms();
2026-06-24 16:37:32 +08:00
char buf[128];
snprintf(buf, sizeof(buf), "_%s_%d_%lld_%lld",
subdir, g_front_seg_index,
(long long)pid, (long long)start_ms);
group_ += buf;
consumer_ = group_;
2026-06-24 16:37:32 +08:00
}
void subscribe(const std::string& topic,
const std::string& tag,
MessageCallBack cb)
{
Sub s;
s.topic = topic;
s.tag = tag;
s.stream = build_stream_name(topic, tag);
s.cb = cb;
subs_.push_back(s);
callbacks_[std::make_pair(topic, tag)] = cb;
callbacks_[std::make_pair(s.stream, tag)] = cb;
size_t pos = topic.find('%');
if (pos != std::string::npos) {
std::string shortTopic = topic.substr(pos + 1);
callbacks_[std::make_pair(shortTopic, tag)] = cb;
}
createGroup(s.stream);
std::cout << "[REDIS_STREAM][SUB] stream=" << s.stream
<< " topic=" << topic << " tag=" << tag
<< " group=" << group_ << " consumer=" << consumer_ << std::endl;
}
void start()
{
stop_ = false;
while (!stop_) {
claimOnce();
readOnce();
}
}
void shutdown()
{
stop_ = true;
conn_.close();
}
private:
struct Sub {
std::string topic;
std::string tag;
std::string stream;
MessageCallBack cb;
};
void createGroup(const std::string& stream)
{
redisReply* r = conn_.command("XGROUP CREATE %b %b $ MKSTREAM",
stream.data(), stream.size(),
group_.data(), group_.size());
if (!r) {
std::cerr << "[REDIS_STREAM][GROUP] create failed: " << stream << std::endl;
return;
}
if (r->type == REDIS_REPLY_ERROR) {
std::string err = r->str ? r->str : "";
if (err.find("BUSYGROUP") == std::string::npos) {
std::cerr << "[REDIS_STREAM][GROUP_ERROR] " << stream << " " << err << std::endl;
}
}
freeReplyObject(r);
}
bool parseFields(redisReply* arr,
std::map<std::string, std::string>& fields)
{
fields.clear();
if (!arr || arr->type != REDIS_REPLY_ARRAY) return false;
for (size_t i = 0; i + 1 < arr->elements; i += 2) {
redisReply* k = arr->element[i];
redisReply* v = arr->element[i + 1];
if (!k || !v || k->type != REDIS_REPLY_STRING || v->type != REDIS_REPLY_STRING) continue;
fields[std::string(k->str, k->len)] = std::string(v->str, v->len);
}
return true;
}
static std::string getField(const std::map<std::string, std::string>& m,
const std::string& k)
{
std::map<std::string, std::string>::const_iterator it = m.find(k);
if (it == m.end()) return "";
return it->second;
}
rocketmq::MQMessageExt makeMsg(const Sub& sub,
const std::string& id,
const std::map<std::string, std::string>& fields)
{
std::string enc = getField(fields, "enc");
std::string bodyRaw = getField(fields, "body");
std::string body;
2026-06-24 16:37:32 +08:00
if (!decode_body(enc, bodyRaw, body)) {
throw std::runtime_error("decode body failed");
}
std::string key = getField(fields, "key");
std::string tag = getField(fields, "tag");
std::string ts = getField(fields, "ts");
if (tag.empty()) {
tag = sub.tag;
}
2026-06-24 16:37:32 +08:00
int64_t born = ts.empty() ? now_ms() : atoll(ts.c_str());
rocketmq::MQMessageExt msg;
msg.setTopic(sub.topic);
msg.setTags(tag);
msg.setKeys(key);
msg.setBody(body);
msg.setBornTimestamp(born);
/*
* RocketMQ MQMessageExt.h setMsgId
*
*/
// msg.setMsgId(id);
return msg;
2026-06-24 16:37:32 +08:00
}
MessageCallBack findCallback(const std::string& topic,
const std::string& stream,
const std::string& tag)
{
std::map<std::pair<std::string, std::string>, MessageCallBack>::iterator it;
it = callbacks_.find(std::make_pair(topic, tag));
if (it != callbacks_.end()) return it->second;
it = callbacks_.find(std::make_pair(stream, tag));
if (it != callbacks_.end()) return it->second;
return NULL;
}
void ack(const std::string& stream, const std::string& id)
{
redisReply* r = conn_.command("XACK %b %b %b",
stream.data(), stream.size(),
group_.data(), group_.size(),
id.data(), id.size());
if (r) freeReplyObject(r);
}
void handleOne(const Sub& sub, const std::string& id, redisReply* fieldsReply)
{
try {
std::map<std::string, std::string> fields;
if (!parseFields(fieldsReply, fields)) {
std::cerr << "[REDIS_STREAM][BAD_MSG] no fields stream=" << sub.stream << " id=" << id << std::endl;
ack(sub.stream, id);
return;
}
rocketmq::MQMessageExt msg = makeMsg(sub, id, fields);
MessageCallBack cb = findCallback(sub.topic, sub.stream, msg.getTags());
if (!cb) {
std::cout << "[REDIS_STREAM][NO_CALLBACK] stream=" << sub.stream
<< " tag=" << msg.getTags() << std::endl;
ack(sub.stream, id);
return;
}
rocketmq::ConsumeStatus ret = cb(msg);
if (ret == rocketmq::CONSUME_SUCCESS) {
ack(sub.stream, id);
} else {
std::cout << "[REDIS_STREAM][RETRY_LATER] stream=" << sub.stream
<< " id=" << id << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "[REDIS_STREAM][HANDLE_EXCEPTION] " << e.what()
<< " stream=" << sub.stream << " id=" << id << std::endl;
} catch (...) {
std::cerr << "[REDIS_STREAM][HANDLE_UNKNOWN_EXCEPTION] stream="
<< sub.stream << " id=" << id << std::endl;
}
}
void readOnce()
{
for (size_t i = 0; i < subs_.size(); ++i) {
Sub& sub = subs_[i];
redisReply* r = conn_.command(
"XREADGROUP GROUP %b %b COUNT %d BLOCK %lld STREAMS %b >",
group_.data(), group_.size(),
consumer_.data(), consumer_.size(),
REDIS_READ_COUNT,
(long long)REDIS_BLOCK_MS,
sub.stream.data(), sub.stream.size());
if (!r) {
usleep(500000);
continue;
}
if (r->type == REDIS_REPLY_NIL) {
freeReplyObject(r);
continue;
}
if (r->type == REDIS_REPLY_ERROR) {
std::string err = r->str ? r->str : "";
std::cerr << "[REDIS_STREAM][XREADGROUP_ERROR] " << err << std::endl;
if (err.find("NOGROUP") != std::string::npos) {
createGroup(sub.stream);
}
freeReplyObject(r);
continue;
}
if (r->type == REDIS_REPLY_ARRAY) {
for (size_t si = 0; si < r->elements; ++si) {
redisReply* streamPair = r->element[si];
if (!streamPair || streamPair->type != REDIS_REPLY_ARRAY || streamPair->elements < 2) continue;
redisReply* messages = streamPair->element[1];
if (!messages || messages->type != REDIS_REPLY_ARRAY) continue;
for (size_t mi = 0; mi < messages->elements; ++mi) {
redisReply* msgPair = messages->element[mi];
if (!msgPair || msgPair->type != REDIS_REPLY_ARRAY || msgPair->elements < 2) continue;
redisReply* idReply = msgPair->element[0];
redisReply* fieldsReply = msgPair->element[1];
if (!idReply || idReply->type != REDIS_REPLY_STRING) continue;
std::string id(idReply->str, idReply->len);
handleOne(sub, id, fieldsReply);
}
}
}
freeReplyObject(r);
}
}
void claimOnce()
{
for (size_t i = 0; i < subs_.size(); ++i) {
Sub& sub = subs_[i];
redisReply* r = conn_.command(
"XAUTOCLAIM %b %b %b %lld 0-0 COUNT %d",
sub.stream.data(), sub.stream.size(),
group_.data(), group_.size(),
consumer_.data(), consumer_.size(),
(long long)REDIS_IDLE_CLAIM_MS,
REDIS_READ_COUNT);
if (!r) continue;
if (r->type == REDIS_REPLY_ERROR) {
std::string err = r->str ? r->str : "";
if (err.find("NOGROUP") != std::string::npos) createGroup(sub.stream);
freeReplyObject(r);
continue;
}
if (r->type == REDIS_REPLY_ARRAY && r->elements >= 2) {
redisReply* messages = r->element[1];
if (messages && messages->type == REDIS_REPLY_ARRAY) {
for (size_t mi = 0; mi < messages->elements; ++mi) {
redisReply* msgPair = messages->element[mi];
if (!msgPair || msgPair->type != REDIS_REPLY_ARRAY || msgPair->elements < 2) continue;
redisReply* idReply = msgPair->element[0];
redisReply* fieldsReply = msgPair->element[1];
if (!idReply || idReply->type != REDIS_REPLY_STRING) continue;
std::string id(idReply->str, idReply->len);
std::cout << "[REDIS_STREAM][CLAIM] stream=" << sub.stream
<< " id=" << id << std::endl;
handleOne(sub, id, fieldsReply);
}
}
}
freeReplyObject(r);
}
}
private:
RedisConnection conn_;
std::string group_;
std::string consumer_;
bool stop_;
std::vector<Sub> subs_;
std::map<std::pair<std::string, std::string>, MessageCallBack> callbacks_;
};
static RedisStreamProducer* g_redis_producer = NULL; //全局生产者实例
static RedisStreamConsumer* g_redis_consumer = NULL; //全局消费者实例
bool RedisStream_should_process_after_start(const rocketmq::MQMessageExt& msg) //判断消息是否在程序启动后产生
{
const int64_t born_ts = static_cast<int64_t>(msg.getBornTimestamp());
return born_ts >= (redis_app_start_ms() - REDIS_START_SKEW_MS);
}
void RedisStream_InitializeProducer() //初始化生产者
{
if (g_redis_producer == NULL) {
std::string addr = G_ROCKETMQ_IPPORT.empty() ? G_MQCONSUMER_IPPORT : G_ROCKETMQ_IPPORT;
g_redis_producer = new RedisStreamProducer(addr);
std::cout << "[REDIS_STREAM] producer initialized addr=" << addr << std::endl;
}
}
void RedisStream_ShutdownAndDestroyProducer()//关闭生产者
{
if (g_redis_producer != NULL) {
g_redis_producer->shutdown();
delete g_redis_producer;
g_redis_producer = NULL;
}
}
void RedisStream_producer_send(const std::string& body, //发送消息
const std::string& topic,
const std::string& tags,
const std::string& keys)
{
if (g_redis_producer == NULL) RedisStream_InitializeProducer();
try {
g_redis_producer->sendMessage(body, topic, tags, keys);
} catch (const std::exception& e) {
std::cerr << "[REDIS_STREAM][SEND_FAIL] " << e.what() << std::endl;
DIY_ERRORLOG_CODE("process",0,LOG_CODE_MQ,
"【ERROR】前置的%s%d号进程 redis stream发送失败,已尝试本地落盘",
get_front_msg_from_subdir(), g_front_seg_index);
} catch (...) {
std::cerr << "[REDIS_STREAM][SEND_FAIL] unknown exception" << std::endl;
DIY_ERRORLOG_CODE("process",0,LOG_CODE_MQ,
"【ERROR】前置的%s%d号进程 redis stream发送未知异常",
get_front_msg_from_subdir(), g_front_seg_index);
}
}
void RedisStream_InitializeConsumer(const std::string& consumerName, //初始化消费者
const std::string& nameServer,
const std::vector<Subscription>& subscriptions)
{
if (g_redis_consumer == NULL) {
g_redis_consumer = new RedisStreamConsumer(consumerName, nameServer);
for (size_t i = 0; i < subscriptions.size(); ++i) {
g_redis_consumer->subscribe(subscriptions[i].topic,
subscriptions[i].tag,
subscriptions[i].callback);
}
}
}
void RedisStream_ShutdownAndDestroyConsumer()//关闭消费者
{
if (g_redis_consumer != NULL) {
g_redis_consumer->shutdown();
delete g_redis_consumer;
g_redis_consumer = NULL;
}
}
void RedisStream_consumer_receive(const std::string& consumerName, //启动消费者接收消息
const std::string& nameServer,
const std::vector<Subscription>& subscriptions)
{
RedisStream_InitializeConsumer(consumerName, nameServer, subscriptions);
if (g_redis_consumer) {
g_redis_consumer->start();
}
}