kevy4.0

CDC——变更流(FEED.* / changes_since

kevy 把每一笔已应用的写入都作为可消费的变更流暴露出来——就是 AOF 记录、副本应用的那一批 effect 帧。典型消费者:缓存失效器、搜索索引更新器、下游镜像——也就是 RDS 栈里那张“outbox 表”干的活,只是不需要那张表。

在配置里打开:

[feed]
enabled = true
# feed_buffer_size = "64mb"   # per shard, caps at 1gb

embedded 侧则是 Config::default().with_feed(0)

游标:(generation, offset)

每一个流位置都是一对 (generation, offset)

服务器接口面

错误

Embedded 接口面

let store = kevy_embedded::Store::open(Config::default().with_feed(0))?;
let (gen, off) = store.changes_tail()?;             // start cursor
let batch = store.changes_since(gen, off, 256, &[b"user:"])?;
for change in &batch.changes { /* change.offset, change.argv */ }
let (gen, off) = batch.next;                        // resume here

feed_shards() 恒为 1——embedded 的写路径把所有 shard 串成一条流,所以同一份消费者循环对两个接口面都成立。FeedError::Resync { generation, tail } 对应 -FEEDRESYNC

投递语义

At-least-once。 在一次 resync 重建之后(以及某些切主形态下),你可能看到已经应用过的帧——消费者必须幂等(缓存失效天然是幂等的)。帧携带的是已应用的 effect(例如一次 ZINTERSTORE 会以 DEL + 普通 ZADD 的形式出现),所以不管源状态如何,应用这些帧都是确定性的。

保留窗口就是内存里的 backlog(每 shard 一份 feed_buffer_size):落后超出这个窗口,你就会拿到 -FEEDRESYNC。没有磁盘归档——耐久的那一半故事,见下面的恢复点契约。

恢复点契约(PITR)

开着 feed 时拍的快照,会把流游标记进头部,并且和快照数据冻结在同一个 no-append 窗口里:

快照 S + 从 S 的游标起的 feed 帧 = 之后任意游标处的精确状态。

kevy_persist::read_snapshot_cursor(path) 把它读回来(v2.3 之前的快照返回 None)。一次恢复演练:把快照载入一个全新的数据目录,然后从记录下来的游标开始重放 FEED.READ 的帧——得到逐字节精确的状态,可以用 PREFIX.STATS / 键 dump 验证。演练脚本就是 bench/diskgate.sh 里的 restore 那一行。

逐前缀统计

PREFIX.STATS <prefix>(服务器)/ Store::info_prefix(embedded)报告某个字节前缀下的存活键数与带 TTL 的键数——这是 O(键空间) 的遍历,给运维看板用,不给热路径用。

这不是什么

没有跨 shard 的全局顺序,没有服务端保存的消费位点,没有 consumer group,没有查询谓词(前缀只是一个字节过滤器)。如果你需要这些,你描述的是一个消息中间件或者一个 RDS——kevy 的 feed 是刻意停在视界之前的(见 designing-on-kevy.md 里的三条法则)。