Valkey 深度实战:Redis 闭源风波之后,开源内存数据库如何重生(2026 深度长文)
2024 年 3 月 20 日,Redis 官方宣布更换许可证,从 BSD 转向 SSPL + RSALv2,等于把"开源"这扇门悄悄关上。第二天,一群来自 AWS、Google、Oracle 的工程师在 Linux 基金会的旗帜下 Fork 了 Redis 7.2.4,取名 Valkey。两年过去,Valkey 不仅活了下来,还把 I/O 多线程、内存效率和搜索能力推到了新高度。本文一次把 Valkey 的来龙去脉、架构内幕、代码实战和性能调优讲透。
一、背景:一场许可证地震,催生了一个新项目
要理解 Valkey,得先理解它为什么存在。
1.1 Redis 的许可证演变
Redis 从诞生起就是 BSD 三条款许可证,这意味着任何人都可以自由使用、修改、再分发,甚至可以闭源打包卖钱。这种"宽容"是 Redis 能横扫缓存、会话、排行榜、消息队列场景的根本原因之一。
但商业现实给了公司压力。2024 年 3 月 20 日,Redis Labs 宣布从 Redis 7.4 起不再使用 BSD,改为 SSPLv1 + RSALv2 双许可证。SSPL(Server Side Public License)是 MongoDB 搞出来的"强copyleft"协议——它要求如果你把软件作为服务提供给第三方,就必须开源你的整个服务端代码。对云厂商(AWS ElastiCache、Google Memorystore、阿里云 Tair)来说,这条线直接踩中了商业命门。
真正的转折在 2025 年 5 月:Redis 8.0 又把许可证换成了 AGPLv3。虽然 AGPL 是 OSI 认可的开源协议,但大量企业的法务红线里明令禁止 AGPL——"只要你把程序通过网络提供给用户,就必须开源整个应用"这条传染性条款,让很多公司连评估都不敢评估。
于是局面变成这样:
| 版本 | 许可证 | 性质 |
|---|---|---|
| Redis ≤ 7.2 | BSD-3-Clause | 完全开源,最后一块净土 |
| Redis 7.4 | SSPLv1 + RSALv2 | 非 OSI 开源,云厂商受限 |
| Redis 8.0 | AGPLv3 | OSI 开源,但企业法务普遍禁用 |
1.2 Valkey 的诞生
2024 年 3 月 21 日,也就是 Redis 改协议第二天,Linux 基金会宣布成立 Valkey 项目——直接 Fork 自最后一个 BSD 版本的 Redis 7.2.4。创始成员包括 AWS、Google Cloud、Oracle、Ericsson、Snap 等一票重量级玩家。
Valkey 这个名字来自 "Value" + "Key" 的变体,寓意它没有忘记自己是一个 KV 存储的本质。它的核心承诺只有一句话:永久 BSD 开源,永远免费,永远社区驱动。
从工程角度看,Valkey 不是"另起炉灶",而是"接着 7.2 往下写"。这意味着它继承了 Redis 十余年沉淀的命令集、数据结构和协议,同时甩掉了商业公司的路线绑架。
1.3 为什么值得你关注
对普通开发者来说,最朴素的问题是:我凭什么要从 Redis 换到 Valkey?答案其实很硬:
- 协议兼容:Valkey 实现了 Redis 7.2 的 RESP 协议和命令集,你现在的 Redis 客户端(go-redis、redis-py、Jedis、Redisson、Lettuce)几乎零改动就能连上 Valkey。
- 性能更好:AWS Elasticache 公开的基准显示,开启 I/O 多线程后,Valkey 相比 Redis OSS 7.0 在吞吐上最高提升 270%,P99 延迟下降 70%,内存效率提升最高 40%。
- 没有许可证炸弹:BSD 意味着你不用担心哪天被发律师函。
下面我们一层层拆开看。
二、核心概念:Valkey 到底是什么
用一句话定义:Valkey 是一个 BSD 许可的、内存优先的高性能键值数据库,向后兼容 Redis 7.2 的协议与命令,并在这个基础上做了一系列架构增强。
2.1 它和 Redis 的关系
- 代码同源:Valkey 8.x / 9.x 的分支点就是 Redis 7.2.4,所以底层的 dict、ziplist、skiplist、quicklist、rax 等数据结构实现完全一致。
- 协议同源:RESP2 / RESP3 都支持,客户端握手、认证、Pub/Sub、事务、Stream 行为一致。
- 命令同源:GET/SET/HASH/ZSET/LIST/SET/STREAM 等命令签名不变,你写的 Redis 代码基本不用改。
差异主要在"增强"部分:I/O 多线程默认策略更激进、内存分配器调优、新增 Search/Bloom 等模块能力、以及对大集群的运维改进。
2.2 数据模型速览
Valkey 和 Redis 一样,是"单键多类型"的模型:
- String:二进制安全,最大 512MB,可做计数器、分布式锁、缓存值。
- Hash:字段级操作,适合存对象。
- List:双向链表(quicklist),可做队列、时间线。
- Set / Sorted Set:集合与有序集合,排行榜、标签、权重队列。
- Stream:追加日志,可做消息队列、事件溯源(支持消费者组)。
- Bitmap / HyperLogLog / GEO:位图、基数估计、地理索引。
这些类型你在 Redis 里怎么用,在 Valkey 里就怎么用。
2.3 持久化:RDB 与 AOF
Valkey 同样支持两种持久化:
- RDB:某一时刻的内存快照,恢复快,但可能丢最后一次快照之后的数据。
- AOF:把每个写命令追加到日志,可配置
appendfsync always/everysec/no,数据安全但文件大、恢复慢。
实践中常见组合是"RDB 做定期全量快照 + AOF 做增量保护",兼顾恢复速度与数据安全。
三、架构分析:Valkey 比 Redis 强在哪
这是本文的技术核心。我们重点看三个方向:I/O 多线程、内存效率、以及模块生态。
3.1 单线程模型的真相
很多人说"Redis 是单线程的"。这句话只说对了一半。
真正单线程的,是命令执行引擎——即解析命令、操作数据结构、生成响应的那部分逻辑。这样设计的好处是无需加锁,数据结构实现简单、行为可预测、没有竞态。
但网络 I/O(读 socket、解析协议、写回 socket)是可以并行的。Redis 从 6.0 起引入了 I/O 多线程,Valkey 把这个能力往前推了一大步:
- 读取阶段多线程:配置
io-threads-do-reads yes,让多个线程并行从 socket 读数据并解析协议。 - 写入阶段多线程:响应数据由多个 I/O 线程并行写回客户端。
- 默认开启更激进:Valkey 8.0 起会根据 CPU 核数自动设置
io-threads,而不像 Redis 默认io-threads 1(即关闭)。
关键点在于:命令执行仍然在单线程里串行跑,所以你永远不用担心 GET/SET 之间的原子性问题被多线程打破。多线程只发生在"搬运字节"的环节——这正是单线程正确性 + 多核吞吐量的完美折中。
客户端A ──┐
客户端B ──┼─> [I/O 线程池: 读/解析/写回] ──> [单线程命令执行引擎] ──> 数据结构
客户端C ──┘
3.2 内存效率:40% 从哪来
AWS 提到的"最高 40% 内存效率提升"并不是玄学,它来自几个真实的技术点:
- 小对象编码优化:Hash、ZSet、List 在小数据量时使用 ziplist/intset/listpack 紧凑编码,Valkey 持续打磨这些编码的内存布局。
- 更聪明的
hash-max-listpack-entries默认值:让更多对象停留在紧凑编码,而不是过早膨胀成哈希表。 - I/O 多线程减少了每个连接的服务端缓冲累积,间接降低峰值内存。
- 内存分配器(jemalloc)调优与碎片控制。
实际落地中,你可以通过 MEMORY USAGE key 和 MEMORY DOCTOR 命令观察真实占用,而不是拍脑袋。
3.3 模块生态:搜索与布隆过滤器
Valkey 项目下陆续孵化了 valkey-search、valkey-bloom 等官方模块,目标是把"在 KV 之上做二级索引、做近似集合判断"的能力标准化,而不用每次都自己在外面搭 Elasticsearch 或自己实现 Bloom Filter。
- Bloom Filter:判断"某个元素是否可能存在",误判率可控,适合去重、爬虫 URL 判重、缓存穿透防护。
- Search:在 Valkey 内做索引查询,适合中小规模的全文/字段检索场景,省去额外组件。
务实建议:大规模检索(亿级文档)仍建议用专业引擎;Valkey 的搜索模块更适合"我的数据本来就在 Valkey 里,顺手查一下"的场景。
3.4 高可用与集群
Valkey 完整继承了 Redis 的两种扩展形态:
- 主从复制(Replication):异步复制,从节点可承接读流量、做故障切换。
- 哨兵(Sentinel):自动监控、选主、通知。
- Cluster:数据按 16384 个槽(slot)分片,天然支持水平扩展和在线迁移。
Valkey 在集群运维上做了一些打磨,比如更平滑的槽迁移、对大集群拓扑传播的稳定性改进,但对外接口和 Redis Cluster 基本一致。
四、代码实战:从零跑起来
光说不练假把式。下面用真实可运行的代码,把 Valkey 用起来。
4.1 用 Docker 起一个 Valkey
# 拉取官方镜像(截至 2026-05-19,最新稳定版为 9.1.0)
docker run -d --name valkey \
-p 6379:6379 \
valkey/valkey:9.1.0
# 进入命令行
docker exec -it valkey valkey-cli ping
# 返回 PONG 即成功
官方镜像标签形如 valkey/valkey:9.1.0、valkey/valkey:9.1.0-alpine,和 redis:7 的用法几乎一致。
4.2 valkey-cli 基础操作
Valkey 自带的命令行工具和 Redis 同名命令完全通用:
$ valkey-cli
127.0.0.1:6379> SET user:1001:name "三哥" EX 3600
OK
127.0.0.1:6379> GET user:1001:name
"三哥"
# 计数器
127.0.0.1:6379> INCR page:view:home
(integer) 1
127.0.0.1:6379> INCR page:view:home
(integer) 2
# 哈希存对象
127.0.0.1:6379> HSET order:888 status "paid" amount 199 sku "KEYBOARD"
(integer) 3
127.0.0.1:6379> HGETALL order:888
1) "status"
2) "paid"
3) "amount"
4) "199"
5) "sku"
6) "KEYBOARD"
# 有序集合做排行榜
127.0.0.1:6379> ZADD leaderboard 95 alice 88 bob 100 carol
(integer) 3
127.0.0.1:6379> ZREVRANGE leaderboard 0 2 WITHSCORES
1) "carol"
2) "100"
3) "alice"
4) "95"
5) "bob"
6) "88"
4.3 Go 客户端实战(go-redis)
Valkey 与 Redis 协议兼容,所以 github.com/redis/go-redis/v9 可以直接连 Valkey,零修改。
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
// 注意:Addr 指向 Valkey,其余参数和连 Redis 完全一致
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // 生产环境务必设置 requirepass
DB: 0,
PoolSize: 50, // 连接池大小,配合 I/O 多线程吃满多核
MinIdleConns: 10,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
})
if err := rdb.Ping(ctx).Err(); err != nil {
log.Fatalf("连接 Valkey 失败: %v", err)
}
fmt.Println("连上 Valkey 了")
// 1) 基础读写 + 过期
if err := rdb.Set(ctx, "greeting", "hello valkey", 10*time.Minute).Err(); err != nil {
log.Fatal(err)
}
v, _ := rdb.Get(ctx, "greeting").Result()
fmt.Println("greeting =", v)
// 2) 用 Pipeline 批量写入,减少 RTT(高并发场景关键优化)
pipe := rdb.Pipeline()
for i := 0; i < 1000; i++ {
pipe.HSet(ctx, fmt.Sprintf("metric:%d", i),
"ts", time.Now().Unix(),
"val", i,
)
}
if _, err := pipe.Exec(ctx); err != nil {
log.Fatal(err)
}
fmt.Println("批量写入 1000 条完成")
}
要点:PoolSize 要开大。因为 Valkey 服务端 I/O 是多线程的,如果你客户端只开几个连接,多核优势根本喂不饱。这是从 Redis 迁移过来最常见的"性能没起来"原因。
4.4 分布式锁:别再用错姿势
缓存击穿、防止重复下单、抢单——这些都要用分布式锁。Valkey 上最稳的单机锁写法是 SET key value NX PX 一条命令原子完成"不存在才设 + 带过期":
// 加锁:value 用唯一随机值,释放时必须校验,防止误删别人的锁
func acquireLock(ctx context.Context, rdb *redis.Client, lockKey, token string, ttl time.Duration) (bool, error) {
return rdb.SetNX(ctx, lockKey, token, ttl).Result()
}
// 释放锁:用 Lua 保证"校验 value == token 才删除"的原子性
const releaseScript = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
func releaseLock(ctx context.Context, rdb *redis.Client, lockKey, token string) (bool, error) {
res, err := rdb.Eval(ctx, releaseScript, []string{lockKey}, token).Int()
if err != nil {
return false, err
}
return res == 1, nil
}
func worker(ctx context.Context, rdb *redis.Client, id int) {
token := fmt.Sprintf("worker-%d-%d", id, time.Now().UnixNano())
ok, _ := acquireLock(ctx, rdb, "lock:order:888", token, 3*time.Second)
if !ok {
fmt.Printf("worker %d 没抢到锁,跳过\n", id)
return
}
defer releaseLock(ctx, rdb, "lock:order:888", token)
// 拿到锁,执行临界区操作
fmt.Printf("worker %d 拿到锁,开始干活\n", id)
}
诚实提醒:单实例 Valkey 的
SET NX锁在"主从异步复制 + 主宕机"场景下可能丢锁。如果你要金融级强一致,请用 Redlock 并理解它的争议(Martin Kleppmann 和 antirez 当年的辩论值得一读),或干脆上 ZooKeeper/etcd。技术选型没有银弹,只有权衡。
4.5 Pub/Sub 与 Stream:消息能力的两种形态
简单广播用 Pub/Sub:
// 订阅端
sub := rdb.Subscribe(ctx, "newsroom")
ch := sub.Channel()
go func() {
for msg := range ch {
fmt.Printf("收到消息: %s\n", msg.Payload)
}
}()
// 发布端
rdb.Publish(ctx, "newsroom", "Valkey 9.1.0 发布了!")
但 Pub/Sub 的致命缺点是消息不持久、掉线即丢。需要"至少一次"投递、消费者组、可重放,请用 Stream:
// 生产消息
rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "order_events",
Values: map[string]interface{}{"type": "paid", "order_id": 888, "amount": 199},
})
// 消费者组消费(多个实例分摊,且支持 Pending 重投)
rdb.XGroupCreateMkStream(ctx, "order_events", "cg-1", "0")
res, _ := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "cg-1",
Consumer: "worker-A",
Streams: []string{"order_events", ">"},
Count: 10,
Block: 5 * time.Second,
})
for _, stream := range res {
for _, msg := range stream.Messages {
fmt.Printf("处理订单事件: %v\n", msg.Values)
// 处理完确认,避免重复投递
rdb.XAck(ctx, "order_events", "cg-1", msg.ID)
}
}
4.6 Python 迁移:redis-py 直接复用
如果你是从 Redis 迁过来,Python 侧几乎不动:
import redis
# 把 host/port 指向 Valkey 即可,API 完全一致
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
r.set("user:1:name", "三哥", ex=3600)
print(r.get("user:1:name"))
# 用 pipeline 批量
with r.pipeline() as pipe:
for i in range(1000):
pipe.incr(f"counter:{i}")
pipe.execute()
# 布隆过滤器(若部署了 valkey-bloom 模块,命令形如 BF.ADD / BF.EXISTS)
迁移成本总结:改连接地址 → 验证客户端版本兼容 → 压测 → 切流。多数业务可以在一个迭代内完成,因为协议和命令集是向下兼容的。
4.7 开启 I/O 多线程的配置
Valkey 的配置文件(valkey.conf)里关键几项:
# I/O 线程数,建议设为 CPU 核数(读多写多场景)
io-threads 4
# 让读取也走多线程(Redis 默认 false,Valkey 建议 true)
io-threads-do-reads yes
# 内存上限与淘汰策略
maxmemory 4gb
maxmemory-policy allkeys-lru
# 持久化
appendonly yes
appendfsync everysec
改完重启生效。用 INFO threads 可以看到 I/O 线程的运行状态。
五、性能优化:把多核吃满
Valkey 性能优化的本质,是让单线程命令引擎不被 I/O 拖累,并让多核真正参与搬运。
5.1 连接池 + I/O 线程 = 吞吐翻倍
前面 Go 例子强调过 PoolSize。这里给一个经验公式:
客户端并发连接数 ≈ 预期 QPS / 单连接有效 QPS
如果服务端开了 4 个 I/O 线程,但客户端只用 2 条连接,那 2 个核基本闲着。生产经验是连接池至少等于 I/O 线程数,高并发再往上加,并通过压测找到拐点。
5.2 基准测试:用 valkey-benchmark 说话
Valkey 自带 valkey-benchmark(对应 Redis 的 redis-benchmark):
# 100 并发、100 万次 SET,对比不同 io-threads 下的表现
valkey-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 1000000 -t set,get -q
# 测 Pipeline(每次打包 16 条),吞吐会再上一个数量级
valkey-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 1000000 -P 16 -t set -q
一个有代表性的现象:在 4 核机器上,把 io-threads 从 1 调到 4,SET 的 QPS 可能从 ~30 万涨到 ~80 万(具体看硬件和负载)。这就是 AWS 所谓"270% 吞吐提升"的真实来源——它是和"单线程 I/O 的 Redis OSS 7.0"对比出来的。
注意:如果你的瓶颈是大 value(比如单个 String 10MB)或重计算命令(如
KEYS *、SMEMBERS大集合),多线程 I/O 救不了你——因为命令执行是单线程的。这种场景要拆 key、用游标(SCAN)、控制 value 大小。
5.3 内存优化清单
- 控制 key 长度:
user:1001:profile比user_profile_information_for_id_1001省内存,亿级 key 下差别惊人。 - 用合适的编码:小 Hash 用
hash-max-listpack-entries保持紧凑;别动不动就HGETALL大 Hash。 - 设 maxmemory-policy:缓存场景用
allkeys-lru,持久数据用noeviction并监控容量。 - TTL 是好朋友:给缓存都加上过期时间,避免内存无限增长。
- 定期
MEMORY PURGE(jemalloc 碎片回收)配合监控。
5.4 与 Redis OSS 的对比(数据来源说明)
下表数据来自 AWS Elasticache 公开的 Valkey 对比口径(对比对象为 Redis OSS 7.0,开启 Valkey I/O 多线程):
| 指标 | Valkey(I/O 多线程) | 说明 |
|---|---|---|
| 吞吐 | 最高 +270% | 多核并行 I/O 搬运 |
| P99 延迟 | 最高 -70% | 读/写不再排队 |
| 内存效率 | 最高 +40% | 编码与分配器优化 |
诚实声明:这些是云厂商在特定负载下的基准,不是你业务的保证值。真实收益取决于你的命令分布、value 大小、网络环境和连接模型。务必在自己环境压测,别直接抄数字写 PPT。
六、从 Redis 迁移到 Valkey:避坑指南
迁移不是"改个 IP"就完事,下面是实战中容易踩的坑:
- 客户端版本:老旧的 Redis 客户端可能硬编码了某些 Redis 私有行为。建议升级到近一年的版本(go-redis v9+、redis-py v5+),它们都声明支持 Valkey。
- 模块命令:如果你在重度依赖 RedisBloom / RediSearch / RedisJSON,要确认 Valkey 侧对应的
valkey-bloom/valkey-search模块是否已覆盖你的用法,命令名可能不同(如BF.ADDvs 私有实现)。 - 哨兵/集群配置:
sentinel.conf、cluster的槽迁移流程和 Redis 基本一致,但建议先在预发环境完整跑一遍故障切换演练。 - 监控对接:Prometheus exporter、Grafana 面板大多复用 Redis 的指标(因为 INFO 输出兼容),但 dashboard 名字和个别字段要核对。
- 灰度切流:双写或影子读,先读 Valkey 比对结果,确认一致再切主流量。
七、总结与展望
Valkey 的故事,本质上是开源社区对"许可证霸权"的一次集体反击。它证明了:当代码同源、协议兼容、许可证真正自由时,开发者会用脚投票。
对技术人的启示有三条:
- 协议兼容性是最硬的护城河。Valkey 能快速接管生态,靠的不是颠覆,而是"你几乎无感地换了个更自由的引擎"。
- 多核时代,单线程命令引擎 + 多线程 I/O 是被验证的黄金组合。它在正确性和吞吐之间找到了优雅平衡。
- 选型要看许可证。一个今天 BSD、明天 AGPL 的项目,意味着你的技术债可能突然变成法务债。Valkey 的 BSD 承诺,是它最值钱的资产。
展望 2026 往后:Valkey 在 Search/Bloom 模块、集群运维自动化、与云原生(Kubernetes Operator、Serverless 形态)的深度融合上还在加速。对于新项目,我的建议很直接——默认选 Valkey,除非你有强依赖的 Redis 私有模块且 Valkey 尚未对齐。对于存量 Redis 项目,把它列为下半年的技术债清理项,收益明确、风险可控。
最后送一句话给还在纠结的你:技术的归技术,自由的归自由。Valkey 让我们两者兼得。
本文代码均基于 Valkey 9.1.0 与 Redis 协议兼容客户端实测可用思路编写;性能数据引用自 AWS Elasticache 公开基准,请在你自己的环境复测后再下定论。