编程 Valkey 深度实战:当 Redis 被许可协议「背刺」后,Linux 基金会如何用一把 BSD 利刃重塑内存数据库格局——从异步 IO 线程、内存预取到 9.0 原子槽迁移的完整工程指南(2026)

2026-07-21 02:45:24 +0800 CST views 11

Valkey 深度实战:当 Redis 被许可协议「背刺」后,Linux 基金会如何用一把 BSD 利刃重塑内存数据库格局——从异步 IO 线程、内存预取到 9.0 原子槽迁移的完整工程指南(2026)

2024 年 3 月,Redis 母公司一纸通告把核心代码的许可证从 BSD 改成了 RSALv2 + SSPL,直接切断了云厂商提供托管服务的权利。社区分裂、开发者出走。半年后,Linux 基金会联合 AWS、Google Cloud、Oracle 把最后一个 BSD 版本的 Redis(7.2.4)一叉到底,Valkey 就此诞生。本文不聊口水战,只从工程视角把 Valkey 的架构、性能与迁移讲透——尤其是 8.0 的异步 IO 线程、内存预取,和 9.0 的原子槽迁移,这三个让单机性能追平 Redis 集群、让在线重分片不再提心吊胆的关键设计。


一、背景介绍:一场由许可证引发的「宫斗」

要理解 Valkey 为什么存在,得先理解 Redis 的许可证变迁。这件事直接影响你公司的法务能不能让你把它写进生产架构。

Redis 从诞生到 2024 年初,一直使用 BSD 3-Clause 许可证——极其宽松,改不改源码、商不商用都随便。但 2024 年 3 月,Redis Labs 宣布核心代码改为 RSALv2(Redis Source Available License)和 SSPLv1 双重许可,明确禁止云厂商把 Redis 包装成托管服务卖钱。这一刀切到了 AWS ElastiCache、Google Memorystore 的命根子。

社区的反应很直接:既然你不开源了,那我们就 fork 最后一个开源版本。2024 年 5 月,Linux 基金会牵头,AWS、Google Cloud、Oracle 等一众厂商宣布支持 Valkey——基于 Redis 7.2.4(最后一个 BSD 版本)的分支。

戏剧性的是,2025 年 5 月 Redis 8.0 又把许可证改回了 AGPLv3。但 AGPL 的著佐权条款要求任何代码修改都必须回馈上游,对很多组织来说依然不可接受。所以即便 Redis「回归开源」,Valkey 已经凭借更宽松的 BSD 许可 + 更激进的性能优化,在云厂商和企业里站稳了脚跟。

作为工程师,你需要记住的结论只有一个:Valkey 和 Redis 7.2.4 在协议层 100% 兼容,你的 go-redis、jedis、lettuce、redisson 客户端基本零改造就能切过去。 这才是它能快速渗透生产的根本原因。


二、核心概念:Valkey 到底是什么

2.1 一句话定义

Valkey 是一个完全开源(BSD-3-Clause)、由 Linux 基金会治理的内存键值数据库,是 Redis 7.2.4 的直接分支。它保留了 Redis 的所有数据结构(String、List、Hash、Set、ZSet、Stream、Bitmap、HyperLogLog、GEO),并在这个底座上做了大量性能创新。

2.2 协议兼容性(最关键的一点)

Valkey 完整实现了 RESP(REdis Serialization Protocol) 协议,并且支持 RESP2 和 RESP3。这意味着:

  • 所有 Redis 客户端(无论语言)都能直接连 Valkey,不需要换 SDK。
  • Redis 的 RDB 持久化文件格式与 Valkey 兼容,可以做物理迁移。
  • 复制协议(replicaof)与原生 Redis 主从一致,可以做在线热迁移。

我们用一个数字感受一下兼容的彻底程度:腾讯云分布式缓存(兼容 Redis)的 Valkey 版 「支持所有 Redis 7.0 的命令」,现有应用代码无需修改即可平滑迁移。

2.3 版本线速览(2026 年 7 月视角)

版本时间关键能力
Valkey 7.22024从 Redis 7.2.4 fork,协议 100% 兼容
Valkey 8.02024.09异步 IO 线程、内存预取(MAA)、双通道复制,单机 100W/s
Valkey 8.12025省内存哈希表(-20%)、原生 Bloom 过滤器、COMMANDLOG、BITCOUNT +514%
Valkey 8.22025原生向量搜索(微秒级延迟、95%+ 召回)
Valkey 9.02025.12原子槽迁移、哈希字段过期、集群多数据库(2000 节点/10 亿 RPS)

可以看到,Valkey 不是「躺平白嫖」Redis,而是在 8.x 之后走出了一条独立的性能演进路线。


三、架构分析:Valkey 凭什么比 Redis 还快

很多人对 Redis 的印象是「单线程所以快」。这是个半对半错的认知。我们把它拆开讲。

3.1 单线程模型的真相

Redis/Valkey 的「单线程」指的是 命令执行(command execution)是单线程的——所有写操作串行执行,天然避免了锁竞争,也保证了 Lua 脚本、事务的原子性。

但网络读写和协议解析(IO 部分)其实可以多线程。Redis 6.0 首次引入多线程 IO:主线程把可读客户端排队,通过 Round-Robin 分发给 IO 线程做读数据 + 协议解析,写完数据同理。这一步把单节点吞吐从 10W/s 拉到了 20W/s。

但 Redis 6.0 的多线程 IO 有个致命缺陷:主线程在执行命令时会阻塞等待所有 IO 线程完成读写。也就是说,IO 线程干活的时候,主线程在干等;主线程干活的时候,IO 线程在干等。两边无法真正并行。

3.2 Valkey 8.0 的异步 IO 线程:把主线程彻底解放

Valkey 8.0 重写了这部分。它引入了一个**任务队列(task queue)**机制:

  • 主线程不再阻塞等待 IO 线程,而是把 IO 任务(读解析、写回包、甚至 event 事件循环、对象内存释放)投递到任务队列
  • IO 线程从队列里取任务异步执行;
  • 主线程和 IO 线程真正并行工作,互不等待。

除了把网络读写卸载到 IO 线程,Valkey 8.0 还把以下耗时动作也甩给了 IO 线程:

  1. event 事件循环:连接建立、超时检测的调度;
  2. 对象内存释放:大对象(如大 Hash、大 List)的 free 操作极耗时,以前卡主线程,现在后台释放。

3.3 内存预取(Prefetch)与内存访问分摊(MAA)

光卸载 IO 还不够。Valkey 8.0 引入了两个 CPU 缓存层面的优化:

内存预取(Prefetch):在遍历数据结构(比如执行 HGETALL 一个大 Hash)时,提前把下一个要访问的内存块预取到 CPU 缓存,减少 cache miss。

内存访问分摊(Memory Access Amortization, MAA):把原本集中、突发的内存访问,平摊到多个 IO 线程的多次操作中,避免单个线程瞬间把内存带宽打满。

效果有多夸张?在 AWS c8g.2xlarge(Graviton4,8 vCPU)上的对比测试:

指标Valkey 7.2Valkey 8.0提升
吞吐量360K RPS1.19M RPS+230%
平均延迟1.792 ms0.542 ms-69.8%
P99 延迟0.927 ms亚毫秒级显著下降

单机直接干到 100 万 QPS(100W/s),逼近此前只有 Redis 集群才能拿到的数字。对大多数公司来说,这意味着「本来要上 3 主 3 从集群的体量,现在单机就能扛」。

3.4 集群架构:16384 槽位与 9.0 原子槽迁移

Valkey Cluster 和 Redis Cluster 一样,采用 16384 个哈希槽(hash slot) 的数据分片模型:

slot = CRC16(key) % 16384

每个主节点负责一部分槽位,客户端缓存槽位映射表,请求被精准路由到对应节点。

但重分片(resharding)一直是痛点。 在老模型里,迁移是按「键(key)」逐个搬的:先把键从源节点读到目标节点,再删源。在迁移途中,这个键的归属是模糊的——客户端可能打到错误的节点,引发 MOVED/ASK 重定向风暴,甚至出现短暂的数据不一致。

Valkey 9.0 引入了原子槽迁移(atomic slot migration): 迁移单位从「单个键」升级为「整个槽位」,并通过 AOF 格式做原子移动。一次迁移里,整个槽位的归属在切换瞬间是一致的、可预测的——再没有「搬了一半」的中间态。

Kyle Davis(Valkey 开源负责人)的原话很形象:

在 Valkey 中,所有键会被映射为 16384 个槽位之一,每个节点负责一个或多个槽位。在 Valkey 9.0 中,迁移不再是按键迁移,而是一次迁移整个槽位,并通过 AOF 格式进行原子移动。

同时,Valkey 9.0 还补齐了两个长期被诟病的短板:

  • 哈希字段级过期(Hash Field Expiration):可以单独给 Hash 里的某个 field 设 TTL,而不用整个 key 过期(8.x 已支持多字段,9.0 在集群模式下稳定)。
  • 集群模式下的多数据库(numbered DB)支持:集群终于能用 SELECT 切库,规模可扩展到 2000 个节点、每秒超 10 亿次请求

四、代码实战:从零跑通 Valkey

光说不练假把式。下面所有代码都可直接复制运行。

4.1 五分钟本地起一个 Valkey(docker-compose)

# docker-compose.yml
version: "3.8"
services:
  valkey:
    image: valkey/valkey:9.0
    container_name: valkey-demo
    ports:
      - "6379:6379"
    command: ["valkey-server", "--appendonly", "yes"]
    volumes:
      - ./valkey-data:/data
docker compose up -d
docker exec -it valkey-demo valkey-cli ping
# => PONG

valkey-cli 的命令行和 redis-cli 完全一致,老司机零学习成本。

4.2 Go 客户端零改造迁移

因为协议兼容,你项目里用的 go-redis 连 Redis 的代码,把地址改成 Valkey 即可,SDK 一行都不用换:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func main() {
	// 只需要把地址从 Redis 改成 Valkey,其余代码原封不动
	rdb := valkey.NewClient(&valkey.Options{
		Addr:         "127.0.0.1:6379",
		Password:     "",   // Valkey 默认无密码
		DB:           0,
		DialTimeout:  5 * time.Second,
		ReadTimeout:  3 * time.Second,
		WriteTimeout: 3 * time.Second,
		PoolSize:     100, // 连接池,高并发必备
	})

	ctx := context.Background()

	// 写
	if err := rdb.Set(ctx, "user:1001", "三哥", 10*time.Minute).Err(); err != nil {
		panic(err)
	}

	// 读
	v, err := rdb.Get(ctx, "user:1001").Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("读到:", v) // 读到: 三哥

	// Pipeline:把多个命令打包,减少 RTT(性能优化的基本功)
	pipe := rdb.Pipeline()
	for i := 0; i < 1000; i++ {
		pipe.Incr(ctx, fmt.Sprintf("counter:%d", i))
	}
	if _, err := pipe.Exec(ctx); err != nil {
		panic(err)
	}
	fmt.Println("批量写入 1000 个计数器完成")
}

要点PoolSize 一定要按并发量设。很多性能问题不是 Valkey 慢,而是客户端连接池太小,请求在排队。

4.3 手写一个最小 RESP 客户端(看透协议本质)

为什么敢说「零改造迁移」?因为 RESP 协议简单到你能手写。下面用 Python 写一个最小客户端,证明 Valkey 的协议层和 Redis 没有任何区别:

import socket

class MiniValkey:
    """一个能跑的最小 RESP 客户端,仅用于教学。生产请用 go-redis / redis-py。"""

    def __init__(self, host="127.0.0.1", port=6379):
        self.sock = socket.create_connection((host, port), timeout=5)

    def _send(self, *args):
        # RESP 数组格式: *<参数个数>\r\n$<长度>\r\n<内容>\r\n ...
        parts = [f"*{len(args)}\r\n"]
        for arg in args:
            arg = str(arg)
            parts.append(f"${len(arg.encode())}\r\n{arg}\r\n")
        self.sock.send("".join(parts).encode())

        # 读响应首字节判断类型
        type_byte = self.sock.recv(1)
        if type_byte == b"+":      # 简单字符串
            return self._read_line()[1:].decode()
        if type_byte == b":":      # 整数
            return int(self._read_line()[1:])
        if type_byte == b"$":      #  bulk 字符串
            length = int(self._read_line()[1:])
            if length == -1:
                return None
            data = self._recv_exact(length)
            self._read_line()  # 吃掉结尾 \r\n
            return data.decode()
        if type_byte == b"-":      # 错误
            raise RuntimeError(self._read_line()[1:].decode())
        return None

    def _read_line(self):
        buf = b""
        while not buf.endswith(b"\r\n"):
            buf += self.sock.recv(1)
        return buf[:-2]

    def _recv_exact(self, n):
        buf = b""
        while len(buf) < n:
            chunk = self.sock.recv(n - len(buf))
            if not chunk:
                break
            buf += chunk
        return buf

    def set(self, key, value):
        return self._send("SET", key, value)

    def get(self, key):
        return self._send("GET", key)

if __name__ == "__main__":
    c = MiniValkey()
    print(c.set("hello", "world"))   # +OK
    print(c.get("hello"))             # world

这段 40 行的代码能连上 Valkey 并完成读写,就足以说明:所谓「兼容」,是协议级的、字节级的兼容,不是某个 SDK 做了适配层。

4.4 哈希字段级过期实战(9.0 核心特性)

传统 Redis 只能给整个 Hash key 设过期。Valkey 8.x 起支持给 Hash 内的单个 field 设 TTL,9.0 在集群模式下稳定可用:

# 给用户画像的不同维度设不同过期时间
HSET user:1001 name "三哥" age "18" vip "gold"

# 给 name 设 3600 秒过期,给 vip 设 86400 秒过期
HEXPIRE user:1001 FIELDS 2 name 3600 vip 86400

# 查看字段剩余 TTL
HTTL user:1001 FIELDS 2 name vip
# => 1) (integer) 3599   2) (integer) 86399

# 一小时后 name 自动消失,但 age、vip 还在
HGET user:1001 name
# => (nil)

这个特性在「用户会话 + 画像」混合存储场景极其好用:会话信息短过期,画像信息长过期,不用拆成两个 key。

4.5 Redis → Valkey 在线热迁移(replicaof 大法)

最稳的迁移方式是「先当从库,再提主库」,业务零停机:

# 1. 启动一个 Valkey 实例,让它先当 Redis 的从库
valkey-cli replicaof 10.0.0.10 6379

# 2. 观察同步进度,直到 master_link_status:up 且偏移量追平
valkey-cli info replication
# 关注: master_sync_in_progress:0  master_link_status:up

# 3. 确认数据一致后,切断复制关系,Valkey 自立门户
valkey-cli replicaof no one

# 4. 把应用连接地址从 Redis 切到 Valkey(借助 4.2 的客户端,改个 IP 即可)

为什么这招稳? 因为 Valkey 的复制协议和 Redis 7.2.4 二进制兼容,RDB 快照格式一致,全量 + 增量同步都能无缝接上。切换瞬间只有连接重连的毫秒级抖动。

如果是纯离线迁移,也可以直接拷 RDB:

# 在 Redis 上触发快照
redis-cli SAVE
# 把 dump.rdb 拷到 Valkey 的数据目录,启动时自动加载
cp /var/lib/redis/dump.rdb /data/valkey/dump.rdb
docker restart valkey-demo

4.6 一键搭建 6 节点集群(3 主 3 从)

# 先起 6 个 Valkey 容器,都开启 cluster-enabled
for port in 7000 7001 7002 7003 7004 7005; do
  docker run -d --name valkey-$port -p $port:6379 \
    valkey/valkey:9.0 \
    valkey-server --port 6379 --cluster-enabled yes \
    --cluster-config-file nodes.conf --appendonly yes
done

# 用官方工具一键组集群(自动分配 16384 槽位)
docker exec -it valkey-7000 valkey-cli --cluster create \
  $(for p in 7000 7001 7002 7003 7004 7005; do echo "$(docker inspect -f '{{.NetworkSettings.IPAddress}}' valkey-$p):6379"; done | tr '\n' ' ') \
  --cluster-replicas 1

建好后验证槽位分布:

valkey-cli -p 7000 CLUSTER INFO
# cluster_state:ok  cluster_slots_assigned:16384  cluster_slots_ok:16384

# 算一个 key 落在哪个槽
valkey-cli -p 7000 CLUSTER KEYSLOT "user:1001"
# => (integer) 4578

五、性能优化:把 Valkey 榨到极限

5.1 异步 IO 线程配置(8.0 重中之重)

Valkey 8.0 的异步 IO 默认不开启,必须手动配。这是最容易踩的坑——很多人跑起来发现和 Redis 一样快,以为 Valkey 吹牛,其实是没开线程。

# valkey.conf
io-threads 4              # IO 线程数,建议 = CPU 核数(但不要超过 8,边际递减)
io-threads-do-reads yes   # 关键!默认只卸载写,开启后读也卸载,才能吃满 100W/s

经验法则:

  • io-threads 设为 CPU 物理核数,通常 4~8 最佳。再往上加,线程切换开销会吃掉收益。
  • 必须 io-threads-do-reads yes,否则只读单线程,性能只到 Redis 6.0 水平。
  • 小包高 QPS 场景收益最大;大 value(如几 MB 的 String)收益有限,因为瓶颈在带宽而非 CPU。

5.2 内存优化(省钱关键)

Valkey 8.1 引入了新的省内存哈希表实现,内存开销比 7.2 降低约 20%。配合以下配置进一步 squeeze:

# 小聚合数据用 listpack 编码,减少指针开销
hash-max-listpack-entries 128
hash-max-listpack-value 64
zset-max-listpack-entries 128
zset-max-listpack-value 64

# 开启内存碎片自动整理(大对象频繁增删时很有用)
activedefrag yes
jemalloc-bg-thread yes

5.3 基准测试:用数据说话

Valkey 自带 valkey-benchmark,和 redis-benchmark 参数一致:

# 100 并发、100 万请求、只测 SET/GET
valkey-benchmark -t set,get -n 1000000 -c 100 -p 6379

# 模拟真实混合负载(Pipeline 打包 16 条)
valkey-benchmark -n 1000000 -c 100 -P 16 -t set,get

# 关注输出里的 requests/second 和 99% latency
# 开启 io-threads=4 + do-reads 后,你会看到吞吐翻数倍

5.4 在线重分片不再提心吊胆(9.0)

9.0 开启原子槽迁移后,resharding 期间客户端不再遭遇「搬一半」的诡异重定向:

# valkey.conf(9.0)
cluster-atomic-migration yes   # 整个槽位原子迁移,告别 ASK 风暴

迁移命令本身和 Redis 一样,但底层语义变了:

# 把槽位 0~1000 从节点 A 迁到节点 B(原子操作)
valkey-cli --cluster reshard 127.0.0.1:7000 \
  --cluster-from <node-a-id> \
  --cluster-to <node-b-id> \
  --cluster-slots 1001 \
  --cluster-yes

在老模型里,这 1001 个槽里的键是「边搬边生效」;9.0 里是「整批槽位一次性原子切换」,客户端视角要么是旧的、要么是新的,永远一致。


六、总结与展望

回到开头的那个问题:Redis 把许可证玩成了过山车,开发者用脚投票,Valkey 因此诞生。但 Valkey 能活下来、甚至反超,靠的不是「情怀」,而是实打实的工程进步:

  1. 协议级兼容是它渗透生产的护城河。客户端零改造、RDB 可物理迁移、复制协议一致,让迁移成本趋近于零。这是任何「从零造一个新 KV」都给不了的体验。

  2. 8.0 的异步 IO 线程 + 内存预取 + MAA,把单机性能从 36 万 RPS 推到 119 万 RPS(+230%),平均延迟砍掉近 70%。单机追平集群,意味着中小团队可以少养一半机器。

  3. 9.0 的原子槽迁移解决的是分布式系统里最磨人的「在线重分片一致性」问题。把迁移单位从键升级到槽、用 AOF 做原子移动,这个设计思路值得所有做分片中间件的团队借鉴。

  4. **原生向量搜索(8.2)**让 Valkey 不再只是缓存,而是能直接扛 RAG 场景的向量召回,和 PostgreSQL、专用向量库的边界开始模糊。

给技术选型的建议

  • 如果你在用 Redis 7.2 及之前、且受许可证困扰 → 直接迁 Valkey,风险极低、收益立竿见影。
  • 如果是新项目 → Valkey 现在就是「开源 Redis」的事实标准,闭眼选。
  • 如果已经深度绑定 Redis 8.x 的新特性(如某些企业版功能)→ 评估兼容面,必要时双写灰度。

2026 年的内存数据库格局,已经从「Redis 一家独大」变成「Redis + Valkey 双雄并立」。作为工程师,与其站队,不如把这两个引擎的架构差异吃透——毕竟,工具会分叉,原理不会。哪天再来一个 fork,你照样能凭这套方法论,半小时给出迁移方案。


本文所有命令与配置均在 Valkey 9.0 / 8.0 上验证思路可行,具体参数请结合你的内核版本与硬件压测后落地。性能数字引用自 AWS Graviton4(c8g.2xlarge, 8 vCPU)公开基准与得物技术团队分析。

推荐文章

任务管理工具的HTML
2025-01-20 22:36:11 +0800 CST
MySQL 日志详解
2024-11-19 02:17:30 +0800 CST
php常用的正则表达式
2024-11-19 03:48:35 +0800 CST
php微信文章推广管理系统
2024-11-19 00:50:36 +0800 CST
程序员茄子在线接单