3.1 状态存储与交互查询

Kafka Streams 状态存储深度解析:KeyValueStore 创建与配置、RocksDB 与内存状态后端对比、交互查询(Interactive Queries)的 QueryableStoreTypes、状态变更日志 changelog Topic 的恢复机制,以及状态大小监控与清理策略。

为什么需要状态存储

无状态转换(filter、map)不依赖历史数据。但很多场景需要有状态处理:

  • 去重:记录已见过的事件 ID
  • 计数:统计每用户的订单数
  • 聚合:计算每商品的累计销售额
  • Join:保存一侧流的最新值等待另一侧

Kafka Streams 内置状态存储,无需外部数据库。

KeyValueStore

import org.apache.kafka.streams.state.KeyValueStore;
import org.apache.kafka.streams.state.StoreBuilder;
import org.apache.kafka.streams.state.Stores;

// 创建状态存储
StoreBuilder<KeyValueStore<String, Long>> countStoreBuilder =
    Stores.keyValueStoreBuilder(
        Stores.persistentKeyValueStore("order-counts"),  // 名称
        Serdes.String(),                                  // key 序列化
        Serdes.Long()                                     // value 序列化
    );

// 添加到拓扑
builder.addStateStore(countStoreBuilder);
存储类型API数据位置适用
持久化Stores.persistentKeyValueStore()本地 RocksDB大状态、需要恢复
内存Stores.inMemoryKeyValueStore()JVM 堆小状态、低延迟
LRU 缓存Stores.lruMap(name, maxEntries)JVM 堆,按条数淘汰中等状态、允许丢旧数据

使用状态存储的 Processor

public class CountProcessor implements Processor<String, Order, String, Long> {
    private KeyValueStore<String, Long> countStore;
    // context 只在 init 里传进来一次,process 里要用就必须自己保存
    private ProcessorContext<String, Long> context;

    @Override
    public void init(ProcessorContext<String, Long> context) {
        this.context = context;
        this.countStore = context.getStateStore("order-counts");
    }

    @Override
    public void process(Record<String, Order> record) {
        String userId = record.key();
        Long current = countStore.get(userId);
        if (current == null) current = 0L;
        countStore.put(userId, current + 1);

        // 转发结果
        context.forward(record.withValue(current + 1));
    }
}

更简洁的 DSL 方式:

KTable<String, Long> counts = orders
    .groupByKey()
    .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>
        as("order-counts")
        .withKeySerde(Serdes.String())
        .withValueSerde(Serdes.Long()));

Changelog Topic 与恢复

状态存储的变更自动写入 changelog Topic:

应用写入状态: put("alice", 10)
    ↓
Kafka Streams 自动发送: ("alice", 10) → <app-id>-<store-name>-changelog
    ↓
其他实例或恢复时: 从 changelog Topic 重建状态存储

changelog Topic 的名称格式是 <application-id>-<store-name>-changelog,没有 __ 前缀(那是 Kafka 内部 Topic 的惯例,Kafka Streams 的 changelog 不这样命名)。

KV store 的 changelog 默认是 compacted Topic,保留策略由 broker 的 Topic 配置决定,不会因为应用重启而丢历史。窗口存储再多保留一段时间:

// 只作用于窗口存储的 changelog:窗口过期后仍保留一段,便于迟到数据恢复
props.put(StreamsConfig.WINDOW_STORE_CHANGE_LOG_ADDITIONAL_RETENTION_MS_CONFIG, 24 * 60 * 60 * 1000);

交互查询(Interactive Queries)

状态存储支持外部直接查询:

// 启动时开启交互查询
props.put(StreamsConfig.APPLICATION_SERVER_CONFIG, "localhost:7070");

KafkaStreams streams = new KafkaStreams(topology, props);
streams.start();

// 查询状态
ReadOnlyKeyValueStore<String, Long> store = streams
    .store(StoreQueryParameters.fromNameAndType(
        "order-counts", QueryableStoreTypes.keyValueStore()));

Long aliceCount = store.get("alice");
System.out.println("Alice order count: " + aliceCount);

交互查询的限制:

  • 只能查询本地状态存储中的 key(如果 key 在另一个实例上,需要自己按分区路由)
  • 使用 RPC 或 REST API 暴露查询接口
  • 读到的是本地实例已处理到的进度,不是输入 Topic 的最新位点;跨实例做全局视图必须自己做扇出和合并

eventd v3 不直接使用交互查询,而是通过 Kafka Topic 输出结果到 Redis,由面板读取。

状态大小监控

状态存储过大时:

  • RocksDB SST 文件增多 → 读写延迟增加
  • 内存压力 → GC 频繁
  • Checkpoint 变慢 → 恢复时间增加

监控指标:

// 状态存储指标挂在 stream-state-metrics 这个 group 下
for (Map.Entry<MetricName, ? extends Metric> entry : streams.metrics().entrySet()) {
    if ("stream-state-metrics".equals(entry.getKey().group())) {
        System.out.println(entry.getKey() + ": " + entry.getValue().metricValue());
    }
}

// RocksDB 后端另外暴露 rocksdb.estimate-num-keys、
// rocksdb.estimate-live-data-size 等指标,可以直接看状态规模

状态清理

Window Store 和 Session Store 支持自动过期:

// 窗口状态保留 7 天
Materialized.<String, Long, WindowStore<Bytes, byte[]>>as("window-store")
    .withRetention(Duration.ofDays(7));

// 使用 StateTtlConfig(Flink,Kafka Streams 用 retention)

Kafka Streams 的 KeyValueStore 没有内置 TTL,需要手动清理或通过 compacted Topic 的保留策略间接控制。

小结

概念说明
KeyValueStorekey-value 状态存储
persistentKeyValueStoreRocksDB 后端,持久化
inMemoryKeyValueStore内存后端,低延迟
changelog Topic状态变更日志,用于恢复
Interactive Query直接查询本地状态存储
retention窗口/会话状态的过期时间

下一节 3.2 Kafka Streams Exactly-Once 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南