在 Canal 社区日益沉寂的背景下,Debezium 已成为业界事实上的 CDC 标准方案。它能实时捕获数据库变更并投递至 Kafka、Redis、Pulsar 等下游系统,广泛应用于:
- 搜索索引同步
- 缓存更新
- 实时数据管道
- 其他增量数据场景
然而,在生产环境中,单点部署的 Debezium Server 存在致命风险:
- 进程崩溃 → 数据同步中断
- 主机宕机 → 变更丢失(offset 未持久化)
- 升级维护 → 服务不可用,影响下游链路
这在对 7×24 小时数据连续性 有要求的系统中,是不可接受的。
Debezium 本质上是一个 CDC 引擎 ,负责解析数据库日志并生成变更事件。而 Debezium Server 则是 Debezium 官方提供的 即用型运行程序:
- 无需 Kafka Connect
- 无需复杂集群
- 通过配置即可直接投递数据
- 支持的 Sink 包括:Kafka、Redis、Pulsar、Kinesis、自定义 Sink(SPI)
这使得 Debezium Server 非常适合中小规模、轻量级 CDC 场景。
在 Debezium 的官方推荐架构中,Kafka 几乎是标配组件:
- Kafka 负责消息缓冲
- Kafka Connect 负责 HA
- Consumer Group 负责负载均衡
但 Kafka 运维复杂度高(ZK / KRaft、磁盘、监控),对资源要求高(CPU / IO),对小规模 CDC 场景性来说价比低。
在我们的业务中,CDC 任务数量有限,更关注 稳定性而非吞吐量,因此我们希望在不引入 Kafka 的前提下,实现 Debezium Server 的高可用。
为保障数据管道的 7×24 小时连续性 ,我们结合 Redis 设计了 Active-Standby 高可用架构:
- 多个 Debezium 实例同时运行
- 仅一个处于 ACTIVE 状态处理任务
- 其余为 STANDBY 状态实时待命
- ACTIVE 故障时,STANDBY 自动秒级接管
Redis 成为理想选择,原因如下:
- 分布式锁:实现 Active 节点选举
- 接收器:存储偏移量 和 内部模式历史记录
- 团队已具备 Redis 运维能力

配置文件(application.properties)
properties
# --- Sink: 输出到 Redis Stream ---
debezium.sink.type=redis
debezium.sink.redis.address=192.168.0.205:6379
debezium.sink.redis.password=HBcjy@1303
debezium.sink.redis.stream.name=dbz-events
# --- Source:Mysql 连接
debezium.source.connector.class=io.debezium.connector.mysql.MySqlConnector
debezium.source.database.hostname=localhost
debezium.source.database.port=3310
debezium.source.database.user=root
debezium.source.database.password=123456
debezium.source.database.server.name=test
debezium.source.database.include.list=safety_check
debezium.source.table.include.list=safety_check.device_track
debezium.source.database.server.id=2013306
debezium.source.topic.prefix=mysql-prod
# --- Offset 存储 ---
debezium.source.offset.storage=io.debezium.storage.redis.offset.RedisOffsetBackingStore
debezium.source.offset.storage.redis.key=dbz-offsets
# --- Schema History ---
debezium.source.schema.history.internal=io.debezium.storage.redis.history.RedisSchemaHistory
debezium.source.schema.history.internal.redis.key=dbz-history
# --- 其他 ---
debezium.format.value.converter.schemas.enable=false
quarkus.log.level=INFO
启动脚本(run-with-lock.sh)
sh
#!/bin/bash
set -e
export JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
export PATH="$JAVA_HOME/bin:$PATH"
# ===== 配置区 =====
REDIS_HOST="192.168.0.101"
REDIS_PORT=6379
REDIS_PASSWORD="YOUR_REDIS_PASSWORD"
LOCK_KEY="debezium-lock"
LOCK_TTL=30 # 锁过期时间(秒)
DEBEZIUM_HOME="/data/debezium/debezium-server"
CONFIG_DIR="/data/debezium/debezium-server/conf"
LOG_DIR="/data/debezium/debezium-server/logs"
# 自动生成唯一实例 ID(hostname + PID)
INSTANCE_ID="$(hostname)-$$"
LOG_FILE="$LOG_DIR/debezium-$INSTANCE_ID.log"
# Redis 连接选项
REDIS_OPTS="-h $REDIS_HOST -p $REDIS_PORT"
if [[ -n "$REDIS_PASSWORD" ]]; then
REDIS_OPTS="$REDIS_OPTS -a $REDIS_PASSWORD"
fi
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# 尝试获取 Redis 锁
acquire_lock() {
local result
result=$(redis-cli $REDIS_OPTS SET "$LOCK_KEY" "$INSTANCE_ID" NX EX "$LOCK_TTL" 2>/dev/null)
[[ "$result" == "OK" ]]
}
# 后台心跳续期
renew_lock() {
while true; do
sleep $((LOCK_TTL / 3))
redis-cli $REDIS_OPTS \
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end" \
1 "$LOCK_KEY" "$INSTANCE_ID" "$((LOCK_TTL * 1000))" >/dev/null 2>&1
done
}
# 释放锁
release_lock() {
redis-cli $REDIS_OPTS \
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" \
1 "$LOCK_KEY" "$INSTANCE_ID" >/dev/null 2>&1
log "Released lock."
}
# ===== 主逻辑 =====
log "Starting Debezium HA node with ID: $INSTANCE_ID"
while true; do
if acquire_lock; then
log "Acquired lock! Starting Debezium Server as ACTIVE"
trap release_lock EXIT
# 启动心跳
renew_lock &
RENEW_PID=$!
# 启动 Debezium(前台)
cd "$DEBEZIUM_HOME"
./run.sh --config-dir="$CONFIG_DIR" >> "$LOG_FILE" 2>&1 &
DEBEZIUM_PID=$!
log "Debezium Server started (PID: $DEBEZIUM_PID)"
# 等待进程退出
wait $DEBEZIUM_PID
kill $RENEW_PID 2>/dev/null || true
log "Debezium stopped. Releasing lock and retrying in 5s..."
sleep 5
else
log "Could not acquire lock. Retrying in 10s..."
sleep 10
fi
done
- TTL + 心跳续期:防止进程僵死导致锁永久持有
- Lua 脚本原子操作:确保只有持有者能续期/释放
- 自动重试机制:STANDBY 节点持续抢锁
正常数据同步
- 操作 :向
safety_check.device_track表持续插入 1000 条记录 - 每条变更事件写入 Redis Stream
dbz-events - 结果 :Redis 中
XRANGE dbz-events - + COUNT 5可见完整事件
ACTIVE 节点主动停止
- 操作 :
kill -15主动终止 Instance A(ACTIVE) - Instance B 或 C 在 ≤30 秒内接管,继续同步
- 结果 :
- 第 28 秒,Instance B 获取
debezium-lock - 新变更事件继续写入
dbz-events - 无事件丢失(对比 MySQL 最终行数 vs Stream 总数)
- 第 28 秒,Instance B 获取
ACTIVE 节点异常崩溃
- 操作 :
kill -9强制杀死 Instance A - 锁因 TTL 过期自动释放,STANDBY 接管
- 结果 :
- 第 31 秒,Instance C 成功抢锁并启动 connector
- 从 Redis 中
dbz-offsets读取最新位点,精确续传 - 无重复消费(检查主键去重)
网络分区模拟(Redis 短暂不可达)
- 操作 :使用
iptables阻断 Debezium 到 Redis 的连接 15 秒 - ACTIVE 节点暂停消费,恢复后继续
- 结果:
- Debezium 日志报
RedisConnectionException,进入重试 - 连接恢复后自动续传,未丢数据
- 锁未被抢占(因未超 TTL)
- Debezium 日志报
Redis 宕机
- 操作:停止 Redis 服务
- 结果:
- 所有 Debezium 实例无法读写 offset/schema,暂停消费
- Redis 恢复后,ACTIVE 实例自动重连并继续
- 数据最终一致,但存在中断窗口
该高可用方案在绝大多数生产场景下可靠有效 ,利用 Redis 统一实现 协调、存储、传输 ,架构简洁;Redis 本身也需高可用,否则成为单点故障;不适用于多 ACTIVE 并行处理场景(如分库分表);锁 TTL 需根据网络稳定性权衡(太短易误切,太长恢复慢)。