Flink 实时去重:State、Redis 与 Bitmap 怎么选
从去重范围、恢复语义与容量估算出发,比较三种方案及其容易忽略的失败窗口。
Read in English →工程学习与设计笔记。示例使用合成场景;适用版本与参考资料见正文。
去重不能只描述成“见过这个 ID 就丢掉”。设计之前必须回答:谁的 ID、保留多久、跨不跨作业、崩溃后允许重复还是允许丢失。本文以 Flink 1.15 的 DataStream 为版本基线,讨论有限保留期内的精确去重。
先把业务边界写出来
假设来源保证 event_id 在来源内唯一,重复最长延迟一天。可以将 (source, event_id) 作为逻辑键,但仍要确认重放一周前数据是否应该被视为重复。如果需要任意历史范围内去重,一天 TTL 显然无法保证。
容量先做粗估:每秒十万条唯一事件、保留一天,约有 86.4 亿个活跃键。即使仅按每键 32 字节计算,也约是 276 GB 十进制容量;这个数字还没包含序列化、索引、时间戳和后端开销。它是量级示例,不是任何真实集群的基准结果。
Keyed State:让状态随作业一起恢复
对于单条 Flink 链路,按完整去重键 keyBy,每个 key 保存一个 seen 标记通常最直观。状态进入 checkpoint 后,读取位置与去重状态可以一致恢复。不过输出能否端到端恰好一次,还取决于 sink 的事务或幂等协议。
onEvent(event):
if seen.value() is absent:
seen.update(true)
emit(event)
这段伪代码仅表达逻辑。Flink 1.15 的 State TTL 基于处理时间;过期可见性、何时刷新 TTL、后台清理方式都是单独选择。逻辑过期不等于文件大小立刻下降。Flink 1.15:Working with State
如果契约是按事件时间保留,需要显式设计桶与定时器,并处理 watermark 停滞、迟到数据与桶边界。不能把处理时间 TTL 当作事件时间窗口。恢复后会重放的数据、允许的最晚到达、时钟边界也必须进入保留期设计。
Redis:共享判重,换来外部一致性问题
多个服务需要共享判重结果时,Redis 很方便。SET key 1 NX EX 86400 能把“不存在才写”和过期时间放进一次命令,避免先查再写的竞争。Redis SET
但原子写 Redis 不等于原子完成业务处理:
- Redis 写入成功。
- 作业向下游输出前崩溃。
- 恢复后重放,发现 Redis 已有标记,于是丢弃。
这个窗口可能丢业务结果。调换顺序则可能重复输出。因此顺序必须与恢复协议一起设计。如果选择先可靠交付、后记录完成,sink 需要按事件键幂等,以吸收完成标记写入前的重复。如果保留先占位,需要持久化 pending / completed 状态,并在恢复后重试交付或对账,不能把 pending 当成已完成而丢弃。单独增加幂等 sink 无法修复前述丢失窗口,因为事件根本没有到达 sink。把 Redis 键称为“已完成”时,必须有证据证明完成,而不是仅仅被接收。
另外应明确 Redis 超时时是阻塞、隔离还是放行。盲目重试可能扩大尾延迟;直接放行会降低去重保证;直接丢弃则可能造成数据损失。选择应随数据用途变化,计费与探索性指标不该共享一个默认策略。
Bitmap:压缩表示不会消除身份问题
RoaringBitmap 适合整数集合,并针对不同分布采用压缩表示。是否省内存取决于 ID 分布,不能只按元素数量判断。RoaringBitmap 官方仓库
如果输入本来就是稳定、无碰撞的整数 ID,可以按时间桶保存 bitmap。若输入是字符串,先 hash 成 32 位整数会引入碰撞,把不同事件误判为重复;升级到更长 hash 只是降低概率,并未得到数学上的精确去重。严格场景需要无碰撞映射或二次核验。
按天分桶还存在午夜问题:当天重试前一天事件,不能只查当天桶。应根据明确的业务保留区间查询相关桶,再清理已经不需要的桶。桶过粗会延长保留,过细会增加元数据、查询和序列化开销。
选择与验收
| 方案 | 适合起点 | 重点验证 |
|---|---|---|
| Keyed State | 单作业内去重 | checkpoint 恢复、TTL、状态容量 |
| Redis | 多链路共享判重 | 外部写入失败窗口、超时和可用性 |
| Bitmap | 整数 ID 集合与批量集合运算 | 身份映射、分布、桶边界 |
压测不要只测稳定 TPS。至少加入热点来源、重复突发、跨保留期重放、状态恢复和 sink 失败。正确性验收使用已知唯一事件集合,分别统计重复输出和缺失输出;性能验收同时看处理延迟、checkpoint 大小、恢复耗时与外部调用量。
如果方案在故障测试里表现不同,先选择满足交付语义的实现,再优化成本。去重漏掉一条合法事件,比多占一点状态更难从报表中发现。