kevy
本页内容

从 6.2 升级到 6.3

一句话版本:你写过的东西没有一处会失效。 没有 API 移位,没有 crate 换形状,数据目录双向都能打开,6.2.x 的副本能跟 6.3.0 的主库对话。改个号就完事了。

kevy-embedded = "6.3.0"

这一页余下的部分讲的是 6.3.0 里多出来了什么、每一项适合什么场景,以及唯一一处因为原来就是错的而改掉的回复。

一句话对照表

如果你……要做的事
跑服务器或某个语言绑定换二进制 / 升包号,别的什么都不用
从 Rust 嵌入kevy-embedded 升到 6.3.0
HGETALL 从 hash 里抽样现在可以用 HRANDFIELD —— §3
客户端会在连上时设 notify-keyspace-events现在能用了;配置文件里的绕行可以删掉 —— §1
协商 HELLO 3 按类型解码有五条回复现在带上了真实类型 —— §2
处理 GEOPOS 的错误有一条畸形回复现在是干净的错误了 —— §4
ghcr.io/goliajp/kevy:latest重新拉一次;这个 tag 现在指向 6.3.0

1. CONFIG SET notify-keyspace-events 真的通到引擎了

场景。 你在用一个订阅键过期或 keyspace 事件的库——Spring Data Redis 的 RedisKeyExpirationEvent、socket.io 的 Redis adapter、若干任务队列——或者你想要过期/驱逐通知,而因为线上设不进去,只好写在配置文件里。

原来错在哪。 keyspace 通知很早就能用了,但只能从配置文件走,那里的键名是带下划线的 notify_keyspace_events。Redis 在线协议上用的是连字符,而两者之间没有桥:这个参数在连接上既读不到也写不了。一个在连接时就设置它的库——这是正常做法——会在它的第一秒撞上 ERR unknown parameter,而引擎其实一直都有这个能力。

要做什么。 除非你曾经绕过它,否则什么都不用做。如果你为了迁就一个想自己设置它的客户端而把它写进了配置文件,现在可以把那处绕行删掉,让客户端自己去干。

CONFIG SET notify-keyspace-events Ex     → +OK
CONFIG GET notify-keyspace-events        → "Ex"
CONFIG SET notify-keyspace-events ZZZ    → -ERR CONFIG SET failed for
                                           'notify-keyspace-events': unknown flag char 'Z'

标志位就是 Redis 那一套:K keyspace、E keyevent、g 通用、$ string、l list、s set、h hash、z zset、t stream、x expired、e evicted、n new-key,以及 A 作为 g$lshzxet 的别名(按 Redis 对 A 的约定,除 n 以外的全部类别)。标志在写入时校验,而不是存下来再默默忽略——所以打错字是在你打错的那一刻报错,而不是在你需要事件的那一刻沉默。


2. 五条 RESP3 回复现在带上了真实类型

场景。 你的客户端协商了 RESP3——HELLO 3——并且按回复类型解码。也就是开了 protocol=3 的 redis-py、v5 起默认如此的 node-redis,以及建立在它们之上的东西。如果你的客户端说的是 RESP2(多数场景下仍是默认),这一节跟你无关。

原来错在哪。 有五条命令在已经协商了 RESP3 的连接上,发的还是 RESP2 的形状:

命令原来发的现在发的(与 Redis 8.10.1 一致)
ZPOPMIN分数是 bulk string分数是 double
ZADD … INCR新分数是 bulk string新分数是 double
GEOPOS坐标是 bulk string坐标是 double
SPOP key count一个 array一个 set
HRANDFIELD … WITHVALUES平铺的列表成对嵌套

于是按类型解码的客户端在该拿到数字的地方拿到了字符串,在该拿到集合的地方拿到了列表。多数客户端会做隐式转换,所以它表现为数据里悄悄错掉的类型、而不是一个报错——这也是它藏了这么久的原因。

要做什么。 如果你自己在做这层转换(比如 RESP3 的 ZPOPMIN 之后 float(score)),那层转换现在是多余的,但无害。如果你有 golden file / 快照测试录下了 RESP3 连接上的 RESP2 形状,重录一次:那条测试钉住的是一个缺陷。

这是怎么发现的,因为这件事本身说明了什么可信:bench/resp3gate.sh 去问被钉住的那版 Redis——HELLO 3 之下哪些动词会换形状——然后要求 kevy 在完全相同的位置跟着动。它不是一份"我们认为哪些命令是 RESP3-aware"的手写清单,清单每一轮都从对手那里来,而且找到的数目少得不合理时门禁会拒绝通过。有 11 个动词会换形状;kevy 现在一个都不相左。


3. HRANDFIELD 实现了

场景。 你要从一个 hash 里抽取字段——功能开关、A/B 分桶、分片任务队列、"给我看几个"这类接口——而以前只能 HGETALL 把整个 hash 拉回来,在自己代码里挑。

HRANDFIELD key                      → 一个字段
HRANDFIELD key 5                    → 至多 5 个「不重复」字段(hash 更小时就更少)
HRANDFIELD key -5                   → 恰好 5 个,「允许重复」
HRANDFIELD key 5 WITHVALUES         → 每个字段连同它的值

count 的正负号就是整个 API。 正数是子集:字段互不重复,hash 比你要的小就给得更少。负数是抽样:恰好 |count| 条,同一个字段可能出现两次。这是 Redis 的约定,kevy 完全照办,包括键不存在和 count 为 0 时都返回空数组。

它省的是什么。 对一个大 hash 用 HGETALL,是把整个 hash 放上线,好让你从里面挑三个字段;HRANDFIELD key 3 放上线的就是三个。一个几千字段的 hash、每个请求抽一次,这就是每次调用一千字节和几十字节的差别——占大头的是那条回复;选取本身是在字段列表上做部分 Fisher-Yates,只打乱会被返回的那一段前缀。

kevy 的四种 hash 表示(包括打包行)它都能处理,所以你不必知道某个键此刻用的是哪一种。


4. GEOPOS 遇上类型不对的键

这是唯一变了的一条回复,也是现有代码唯一可能察觉到的地方。

原来,对一个存着字符串的键问 GEOPOS,回来的是一个数组头然后才是一个错误——*1\r\n-WRONGTYPE …——于是读这条回复的客户端看到的是"一个首元素是错误的数组"。数组头在类型被解析出来之前就已经发出去了。

现在它回 -WRONGTYPE Operation against a key holding the wrong kind of value,除此之外什么都没有,跟 Redis 一模一样。

谁会察觉。 那些检查 reply[0] 里有没有错误标记、而不是检查这条回复本身是不是一个错误的代码。这类代码现在会在错误该在的位置看到错误。多数客户端库本来就把旧形状当成畸形回复处理,所以实际效果是把错误处理修好,而不是弄坏。

另外三种情况没变:成员存在就给坐标,成员不存在和键不存在都给空数组(null array)。


什么原样带过去

  • 线协议,除了 §2 和 §4。 其余每一条 RESP2 / RESP3 回复都与 6.2.2 逐字节相同。
  • 数据目录。 AOF、快照、value log 与每一种 checkpoint 都跟以前一样打开,两个方向都行。没有迁移步骤,也没有单向门。
  • 复制。 复制流没有变:HRANDFIELD 是读命令,永远不会进流,而本次发布也没有碰复制路径的任何东西。6.2.x 副本与 6.3.0 主库两个方向都能配对,所以可以一个节点一个节点地升。
  • 所有 crate 与绑定的 API。 从 6.2.2 到 6.3.0 没有任何代码改动。

6.3.0 还改了什么——量测,不是行为

下面这些不影响任何行为,但它们决定了公布出来的数字值多少钱。

  • 每一个基准对手都钉到了确切版本,并对着各自上游的 latest stable 核过。 Redis 8.10.1、valkey 9.1.2、Dragonfly 1.40.2、postgres 18.6,外加一致性套件驱动的四个客户端库(go-redis 9.22.0、StackExchange.Redis 3.1.31、node-redis 6.2.1、redis-py 8.1.0)。这四个里有两个自 2024 年起就冻着,另两个根本没写版本。现在测量装置会问每个引擎自己是什么版本,对不上就拒绝出数——所以一张公布的表能把对手精确到修订号。抬高一个钉子是一套写下来的流程,而不是一次编辑:见 .claude/skills/competitor-anchors/SKILL.md
  • 被钉住的那版 Redis 提供的每一条命令,现在都有交代。 它的 599 条命令与子命令里,kevy 实现了 206 个动词;剩下的当中,256 条豁免且每条都写了理由,80 条由具名 RFC 认领。没有一条是未分类的,而且这个计数挂在棘轮上、只能往下走——所以将来某个 Redis 版本新增命令时,它会以"一个待做的决定"落地,而不是以沉默落地。如果你一直在想某个你需要的动词是不是缺了,答案现在写在 bench/COMMAND-COVERAGE.json 里,不用靠试出来。

6.3.0 的实测数字——lx64、三整轮、逐格取中位数、读各引擎自己的命令计数器、-c 50 -P 16

动词kevyRedis 8.10.1对比 Redis
GET7,489,119/s5,631,398/s1.33x
SET6,824,662/s2,567,607/s2.66x
INCR6,753,558/s3,294,927/s2.05x
SADD6,152,617/s3,753,131/s1.64x
HSET4,002,580/s2,966,288/s1.35x
ZADD3,242,967/s2,818,626/s1.15x
LPUSH3,142,699/s2,860,306/s1.10x

七格里 kevy 最差的一轮都赢过每个对手最好的一轮;最窄的是 LPUSH 与 ZADD,对 Redis 1.08x。本次发布没有改动服务路径,所以这是把 6.2.2 的数字重测了一遍、而不是变快了——完整条目(含 valkey、Dragonfly 与轮间离散度)在 bench/PERF-LEDGER.md


怎么拿到

# Cargo.toml
kevy-embedded = "6.3.0"
npm install @goliapkg/kevy@6.3.0          # wasm
npm install @goliapkg/kevy-node@6.3.0     # Node 原生
npm install @goliapkg/kevy-bin@6.3.0      # 服务器二进制
pip install kevy==6.3.0
go get github.com/goliajp/kevy-go/v6@v6.3.0

ghcr.io/goliajp/kevy:latest 的容器用户,下次拉取就是 6.3.0。如果你钉了 tag,它是 ghcr.io/goliajp/kevy:6.3.0