4. In-Memory State Store:内存状态存储

内存状态存储,说白了就是把数据放在 JVM 堆内存里。没有磁盘写入,没有序列化开销,速度就是它的王牌。但代价呢?你猜得到——进程一挂,数据全丢。

我个人习惯把这种 Store 叫做“快但脆”。它适合的场景其实很明确:要么数据丢了无所谓,要么你有可靠的冷备方案兜底。今天我们就把它掰开揉碎聊清楚。

4.1 核心原理

Kafka Streams 里的 In-Memory State Store,底层就是一个 ConcurrentHashMap 的变体。它实现了 KeyValueStore 接口,但所有数据只存在内存里。

我截一段简化后的核心逻辑,你一看就明白:

public class InMemoryKeyValueStore<K, V> implements KeyValueStore<K, V> {
    private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>();
    private final String name;

    public InMemoryKeyValueStore(String name) {
        this.name = name;
    }

    @Override
    public V get(K key) {
        return map.get(key);
    }

    @Override
    public void put(K key, V value) {
        map.put(key, value);
    }

    @Override
    public KeyValueIterator<K, V> all() {
        // 返回一个基于内存快照的迭代器
        return new InMemoryKeyValueIterator(map.entrySet().iterator());
    }
    // ... 其他方法类似
}

看到没?没有文件 I/O,没有序列化反序列化。读写就是一次哈希表操作。这也是为什么它的延迟能低到微秒级。

关键点:In-Memory Store 不参与 Kafka Streams 的日志机制。它没有 changelog topic,也不做写前日志(WAL)。这意味着——重启后,状态完全丢失。

4.2 适用场景

我这些年见过不少团队在错误的地方用了内存存储,结果线上出事故。这里我列几个真正适合的场景:

  • 临时计算中间结果:比如窗口聚合里的中间状态,窗口结束后数据就没用了。
  • 可重建的缓存:数据可以从原始 topic 重新消费恢复,丢失几秒数据业务能接受。
  • 测试环境:单元测试和集成测试里,用内存存储跑得飞快,不用清理磁盘。
  • 只读参考数据:从外部加载的配置表,重启后重新加载就行。

举个例子。我之前做过一个实时风控系统,规则引擎需要缓存用户最近 5 分钟的行为计数。窗口一过期,计数就没意义了。这种场景用 In-Memory Store 再合适不过。

我的建议:如果你能回答“进程重启后,状态从哪来?”这个问题,并且答案不是“从天上掉下来”,那就可以考虑用 In-Memory Store。

4.3 优缺点分析

咱们直接上表格,一目了然:

维度 优点 缺点
读写性能 微秒级延迟,无磁盘 I/O 受 GC 影响,大对象频繁创建时会有停顿
数据持久性 — 进程崩溃或重启后数据全部丢失
内存占用 无磁盘空间限制 受 JVM 堆大小限制,OOM 风险高
运维复杂度 零配置,开箱即用 无法做数据备份和恢复
适用数据量 适合小数据量(GB 级别以下) 大数据量下 GC 压力大,容易 Full GC

嗯,这里要注意一个容易被忽略的点:内存存储的“快”是有前提的。当你的数据量逼近堆内存上限时,GC 会成为瓶颈。我曾经见过一个案例,某团队把 50GB 数据塞进 16GB 堆里,结果每分钟一次 Full GC,吞吐量直接归零。

4.4 避坑指南

我踩过的坑,希望你别再踩一遍:

  • 不要在生产环境做“无备份”的内存存储。如果你必须用,至少确保上游 topic 有足够长的保留期,方便重建。
  • 监控堆内存使用率。设置告警,当堆使用超过 70% 时就要警惕了。
  • 避免存储大对象。比如把整个 JSON 字符串当 value 存,这会加剧 GC 压力。
  • 注意迭代器使用。In-Memory Store 的 all() 返回的是当前内存快照的迭代器,如果你在迭代过程中修改数据,行为是未定义的。

我曾经犯过一个低级错误:在内存存储里保存了未序列化的 Protobuf 对象。结果对象图太复杂,GC 根节点扫描耗时飙升。后来改成存 byte[],问题解决。记住:存什么格式,直接影响 GC 表现。

4.5 核心架构图

下面这张图展示了 In-Memory State Store 在 Kafka Streams 任务中的位置和数据流向:

In-Memory State Store 架构图 输入 Topic Kafka Streams 任务 Processor / Transformer In-Memory State Store ConcurrentHashMap 无 changelog / 无持久化 输出 Topic ⚠ 进程崩溃时 内存数据全部丢失 需从输入 Topic 重新消费恢复 ✅ 适用条件 数据可重建 / 临时中间结果 测试环境 / 只读参考数据 特点:零持久化、零序列化、微秒级读写、受 JVM 堆大小限制 JVM 堆内存

从图里你能看到,In-Memory Store 完全活在 JVM 堆里。它和外部系统没有数据交换通道。一旦进程崩溃,那个红色方块里的数据就彻底消失了。

4.6 何时该选它?

我总结了一个简单的决策树:

  1. 数据丢了会怎样?—— 如果答案是“重新算一下就行”,继续往下看。
  2. 数据量多大?—— 如果小于 JVM 堆的 60%,可以考虑。
  3. 延迟要求多高?—— 如果要求 P99 延迟 < 5ms,In-Memory Store 是首选。
  4. 有备份方案吗?—— 比如上游 topic 保留 7 天数据,方便重建。

如果以上四条都满足,那 In-Memory State Store 就是你的菜。否则,我建议你看看后面几章要讲的持久化存储方案。

一个小技巧:如果你不确定该不该用内存存储,先在测试环境压一把。把数据量压到生产预估的 1.5 倍,观察 GC 频率和停顿时间。这个数据比任何理论分析都靠谱。