编程 Valkey 深度实战:Redis 闭源风波之后,开源内存数据库如何重生(2026 深度长文)

2026-07-14 02:44:06 +0800 CST views 10

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.2BSD-3-Clause完全开源,最后一块净土
Redis 7.4SSPLv1 + RSALv2非 OSI 开源,云厂商受限
Redis 8.0AGPLv3OSI 开源,但企业法务普遍禁用

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?答案其实很硬:

  1. 协议兼容:Valkey 实现了 Redis 7.2 的 RESP 协议和命令集,你现在的 Redis 客户端(go-redis、redis-py、Jedis、Redisson、Lettuce)几乎零改动就能连上 Valkey。
  2. 性能更好:AWS Elasticache 公开的基准显示,开启 I/O 多线程后,Valkey 相比 Redis OSS 7.0 在吞吐上最高提升 270%,P99 延迟下降 70%,内存效率提升最高 40%
  3. 没有许可证炸弹: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% 内存效率提升"并不是玄学,它来自几个真实的技术点:

  1. 小对象编码优化:Hash、ZSet、List 在小数据量时使用 ziplist/intset/listpack 紧凑编码,Valkey 持续打磨这些编码的内存布局。
  2. 更聪明的 hash-max-listpack-entries 默认值:让更多对象停留在紧凑编码,而不是过早膨胀成哈希表。
  3. I/O 多线程减少了每个连接的服务端缓冲累积,间接降低峰值内存。
  4. 内存分配器(jemalloc)调优与碎片控制

实际落地中,你可以通过 MEMORY USAGE keyMEMORY DOCTOR 命令观察真实占用,而不是拍脑袋。

3.3 模块生态:搜索与布隆过滤器

Valkey 项目下陆续孵化了 valkey-searchvalkey-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.0valkey/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 内存优化清单

  1. 控制 key 长度user:1001:profileuser_profile_information_for_id_1001 省内存,亿级 key 下差别惊人。
  2. 用合适的编码:小 Hash 用 hash-max-listpack-entries 保持紧凑;别动不动就 HGETALL 大 Hash。
  3. 设 maxmemory-policy:缓存场景用 allkeys-lru,持久数据用 noeviction 并监控容量。
  4. TTL 是好朋友:给缓存都加上过期时间,避免内存无限增长。
  5. 定期 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"就完事,下面是实战中容易踩的坑:

  1. 客户端版本:老旧的 Redis 客户端可能硬编码了某些 Redis 私有行为。建议升级到近一年的版本(go-redis v9+、redis-py v5+),它们都声明支持 Valkey。
  2. 模块命令:如果你在重度依赖 RedisBloom / RediSearch / RedisJSON,要确认 Valkey 侧对应的 valkey-bloom / valkey-search 模块是否已覆盖你的用法,命令名可能不同(如 BF.ADD vs 私有实现)。
  3. 哨兵/集群配置sentinel.confcluster 的槽迁移流程和 Redis 基本一致,但建议先在预发环境完整跑一遍故障切换演练。
  4. 监控对接:Prometheus exporter、Grafana 面板大多复用 Redis 的指标(因为 INFO 输出兼容),但 dashboard 名字和个别字段要核对。
  5. 灰度切流:双写或影子读,先读 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 公开基准,请在你自己的环境复测后再下定论。

推荐文章

Vue 中如何处理跨组件通信?
2024-11-17 15:59:54 +0800 CST
PHP 如何输出带微秒的时间
2024-11-18 01:58:41 +0800 CST
Vue3中的虚拟滚动有哪些改进?
2024-11-18 23:58:18 +0800 CST
Redis和Memcached有什么区别?
2024-11-18 17:57:13 +0800 CST
程序员茄子在线接单