Valkey 深度实战:当 Redis 走向限制性许可,Linux 基金会如何用一个分支重写内存数据库游戏规则——架构、集群与平滑迁移完全指南(2026)
2024 年,Redis 宣布从 7.4 起改用 RSALv2 + SSPLv1 双许可证,社区一片哗然。一个月后,Linux 基金会带着 AWS、Google、Oracle 等一众大厂,从 Redis 最后一个 BSD 版本(7.2.4)分叉出了 Valkey。两年过去,Valkey 不仅活了下来,还在 2025 年底发布了 9.0——原子级槽位迁移、哈希字段过期、集群模式多编号库、可扩展到 2000 节点。本文从工程视角拆解 Valkey 的架构演进、与 Redis 的真实差异、集群原理、平滑迁移路径与生产部署实战,并给出可直接抄走的代码。
一、背景:一场许可证引发的内存数据库「分家」
1.1 事件经过
- 2024-03:Redis 官方宣布,自 7.4 起放弃 BSD,改用 RSALv2(Redis Source Available License)+ SSPLv1(Server Side Public License)。这两份许可证都不是 OSI 批准的开源许可证,核心限制是:你不能把 Redis 当作托管服务对外提供,去和 Redis 商业方(Redis Ltd.)竞争。
- 2024-04:社区从 Redis 7.2.4(最后一个 BSD 许可版本)分叉,成立 Valkey,交由 Linux 基金会托管。
- 2024-2025:AWS、Google Cloud、Oracle、Ericsson 等相继宣布支持 Valkey;多家云厂商把托管内存数据库默认指向 Valkey。
1.2 为什么这件事对工程师重要
许可证变化直接影响你公司的合规边界:
- 如果你自托管 Redis/Valkey 做缓存、会话、排行榜——无论是 7.2.4 还是新版本,普通使用都不受限。
- 如果你在做商业化 SaaS,且把 Redis 作为托管能力对外卖——新许可证下会有法律风险。
- Valkey 的 Valkey License(VAL) 对普通使用、修改、再分发(含商业化产品)宽松,仅限制「将其作为托管服务与维护方竞争」。对绝大多数业务团队,Valkey 等于「无许可证焦虑的 Redis」。
实务建议:新项目默认选 Valkey;存量 Redis 7.2.4 及以下可无缝评估迁移;Redis 7.4+ 若已受许可证约束,迁移到 Valkey 是低成本的「回家」之路。
二、核心概念:Valkey 到底是什么
一句话:Valkey 是 Redis 7.2.4 的代码后代,一个由社区与 Linux 基金会驱动的开源内存键值数据库,协议兼容 Redis 客户端。
它的能力面与 Redis 高度重叠:
- 内存为主、支持持久化(RDB / AOF);
- 丰富数据结构:String、Hash、List、Set、Sorted Set、Stream、Bitmap、HyperLogLog、GEO、JSON(需模块);
- 发布订阅、Lua 脚本、事务(MULTI/EXEC)、键过期;
- 主从复制、Sentinel 高可用、Cluster 分片。
差异不在「能不能用」,而在许可证、演进方向、内存效率与集群能力。
2.1 RESP 协议兼容:客户端几乎零改
Valkey 沿用 RESP(REdis Serialization Protocol)。这意味着绝大多数 Redis 客户端(redis-py、go-redis、lettuce、jedis、node-redis、ioredis)无需修改代码,只要把连接地址从 Redis 换成 Valkey 即可。这是迁移如此便宜的根本原因。
# redis-py 连 Valkey,代码完全不变
import redis
r = redis.Redis(host="valkey-prod", port=6379, decode_responses=True)
r.set("user:1001:name", "san", ex=3600)
print(r.get("user:1001:name")) # 'san'
三、架构分析:Valkey 的内核长什么样
3.1 单线程命令执行 + 多线程 IO
Valkey 的网络模型继承自 Redis 的经典设计:
- 网络 IO 与命令执行分离:从 Valkey 8 起,IO 多线程(
io-threads)成熟,读/写 socket 的解析与封装可并行; - 命令执行仍是单线程:每个命令在单个线程里原子执行,避免了锁与并发数据结构复杂度,也保证了 Lua 脚本、事务的原子性。
客户端请求
│
▼
[IO 线程池] 读 socket → 解析 RESP → 放入队列
│
▼
[主线程] 串行执行命令(单线程,原子)
│
▼
[IO 线程池] 执行结果 → 封装 RESP → 写 socket
开启 IO 多线程(适合大值、高并发场景):
# valkey.conf
io-threads 4 # 通常设为 CPU 核数的 1/2 ~ 3/4
io-threads-do-reads yes
3.2 数据结构的「编码」:内存效率的真相
Valkey 和 Redis 一样,对每种数据结构按数据规模自动选择底层编码以省内存。以 Hash / Sorted Set 为例:
- 小数据用 listpack(紧凑的连续内存编码,替代了旧的 ziplist);
- 大数据升级为 哈希表(dict)/ 跳表(skiplist)+ dict。
Valkey 在 8.x/9.x 重点优化了这些编码,使同样数据占用内存显著低于 Redis 新版本。这也是很多团队迁移后「实例直接变小」的原因。
Hash (小): listpack ── 省内存,O(N) 访问
Hash (大): dict ── O(1) 访问
ZSet (小): listpack
ZSet (大): skiplist + dict ── 范围查询 O(log N)
3.3 持久化:RDB 与 AOF
- RDB:某一时刻的内存快照,恢复快、文件小,但可能丢最后一次快照后的数据。
- AOF(Append Only File):记录每条写命令,可配置刷盘策略(
everysec是默认且较安全的选择)。
生产建议:
# valkey.conf —— 现代默认值
appendonly yes
appendfsync everysec
# AOF 重写阈值
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
经验:缓存场景可关 AOF 只留 RDB;状态型数据(会话、排行榜)务必开 AOF +
everysec。
四、集群:从槽位到原子迁移
4.1 16384 个哈希槽
Valkey Cluster 把键空间切成 16384 个槽(slot),用 CRC16(key) mod 16384 决定一个键属于哪个槽。集群中每个节点负责一部分槽。
key "user:1001" → CRC16 → slot 5471 → 节点 B
key "order:88" → CRC16 → slot 12003 → 节点 C
客户端(如 redis-py 的 RedisCluster)会缓存槽映射,直接连到正确节点,无需代理。
4.2 gossip 与故障转移
节点间通过 gossip 协议互相探活;当多数主节点认为某主节点失联,其从节点发起故障转移接管槽。整个过程对客户端基本透明(只会在切换瞬间有短暂错误,客户端重试即可)。
4.3 Valkey 9.0 的关键升级:原子级槽位迁移
Redis Cluster 的在线 resharding 早已支持不停机迁移槽,但迁移过程中目标槽可能短时不可用或需要处理「正在迁移」的中间态。Valkey 9.0 引入了原子级槽位迁移(atomic slot migration):一个槽的迁移在切换瞬间是原子的,大幅降低迁移期的抖动与边界错误。
9.0 还带来了:
- 哈希字段过期(Hash Field Expiration, HFE):可以对 Hash 里的单个字段设置 TTL,而不再是整个 key 过期——这是很多「带时效的字段」场景的刚需。
- 集群模式支持编号数据库(numbered DB):过去 Cluster 模式只能用
DB0,9.0 起支持多SELECT库。 - 扩展性:官方称可扩展到 2000 个节点、吞吐达 每秒 10 亿次请求量级。
# 给某个 hash 字段单独设过期(Valkey 9.0+)
HSET user:1001 name "san" score 99
HEXPIRE user:1001 3600 FIELDS 1 score # score 字段 1 小时后消失,name 还在
五、代码实战:从连得上到跑得稳
5.1 容器里跑起来
docker run -d --name valkey \
-p 6379:6379 \
valkey/valkey:9.0 \
valkey-server --appendonly yes
# 进 CLI 验证
docker exec -it valkey valkey-cli ping # PONG
5.2 Python 实战:一个带模式的缓存层
import redis
import json
import time
class ValkeyCache:
def __init__(self, url="redis://valkey-prod:6379/0"):
# redis-py 直接连 Valkey
self.r = redis.from_url(url, decode_responses=True)
def get_json(self, key: str):
raw = self.r.get(key)
return json.loads(raw) if raw else None
def set_json(self, key: str, obj, ttl: int = 300):
self.r.set(key, json.dumps(obj), ex=ttl)
def mget_users(self, ids):
# 批量读,减少 RTT
keys = [f"user:{i}" for i in ids]
raws = self.r.mget(keys)
return [json.loads(x) for x in raws if x]
def rate_limit(self, user_id: str, limit: int = 100, window: int = 60) -> bool:
# 滑动窗口计数(用 sorted set 做更精确,这里用 incr+expire 演示)
k = f"rl:{user_id}"
n = self.r.incr(k)
if n == 1:
self.r.expire(k, window)
return n <= limit
cache = ValkeyCache()
cache.set_json("user:1001", {"name": "san", "vip": True}, ttl=600)
print(cache.get_json("user:1001"))
5.3 Go 实战:用 pipeline 批量写入
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
// go-redis 连 Valkey,零改动
rdb := redis.NewClient(&redis.Options{Addr: "valkey-prod:6379"})
// Pipeline:一次性打包 1000 条写入,一次 RTT
pipe := rdb.Pipeline()
for i := 0; i < 1000; i++ {
pipe.Set(ctx, fmt.Sprintf("metric:%d", i), i*2, 0)
}
if _, err := pipe.Exec(ctx); err != nil {
panic(err)
}
fmt.Println("batch done")
}
5.4 Lua 脚本:原子地「读-改-写」
需要原子性又不希望多次往返时,用 Lua:
-- 原子转账:from 扣、to 加,余额不足回滚
-- KEYS[1]=from, KEYS[2]=to, ARGV[1]=amount
local from = KEYS[1]
local to = KEYS[2]
local amt = tonumber(ARGV[1])
local bal = tonumber(redis.call('GET', from))
if not bal or bal < amt then
return -1 -- 余额不足
end
redis.call('DECRBY', from, amt)
redis.call('INCRBY', to, amt)
return 1
script = r.register_script(lua_above)
res = script(keys=["acct:A", "acct:B"], args=[50])
print("ok" if res == 1 else "insufficient")
5.5 Stream:做一个轻量消息队列
Valkey Stream 自带消费者组,可做可靠的事件流:
# 生产者
r.xadd("events", {"type": "order", "id": "88"})
# 消费者组
r.xgroup_create("events", "cg1", id="0", mkstream=True)
# 消费
msgs = r.xreadgroup("cg1", "worker-1", {"events": ">"}, count=10)
for msg_id, fields in msgs:
process(fields)
r.xack("events", "cg1", msg_id) # 确认,避免重复消费
六、平滑迁移:从 Redis 到 Valkey
6.1 可行性评估
绝大多数应用迁移只需三步:
- 客户端:确认所用 Redis 客户端版本不过旧(近几年的都支持 RESP,无需改代码)。
- 命令清单:跑一遍业务用到的命令,对照 Valkey 支持表(核心命令 100% 兼容;少数 Redis 新版本才加、或企业云专有命令需注意)。
- 切换连接:把连接串里的 host 指向 Valkey。
6.2 零停机迁移方案:主从复制接力
最稳的方式是利用复制协议,让 Valkey 当 Redis 的从库,再切主:
# 1) 启动一个 Valkey,作为现有 Redis 的副本
valkey-server --port 6380 --replicaof redis-old 6379
# 2) 等复制追上(INFO replication 看 master_link_status:up, offset 接近)
valkey-cli -p 6380 INFO replication
# 3) 断开复制,Valkey 成为独立主库
valkey-cli -p 6380 REPLICAOF NO ONE
# 4) 把应用连接切到 6380(或做 VIP/ DNS 切换)
优点是全程 Redis 在线,最后一步才切流量,回滚也只需把连接指回原 Redis。
6.3 需要注意的命令差异
Valkey 出于精简与合规,去掉/调整了少量命令与模块生态:
- 一些 Redis 企业版/云专有命令在 Valkey 不存在;
- 个别 模块 API 行为有差异,依赖特定 Redis Module(如 RedisJSON、RediSearch 的商业版)时要确认 Valkey 侧对应实现(如
valkey-search、valkey-json社区模块)。 - 调试类命令(如某些
DEBUG子命令)可能受限。
# 迁移前,在预发环境 dump 出实际用到的命令做核对
valkey-cli --scan | head # 看 key 模式
# 配合 AOF / 慢日志,梳理业务命令集
6.4 用 valkey-benchmark 做对比
不要凭感觉判断性能,用自带基准:
# 对比同样硬件下 Valkey vs Redis
valkey-benchmark -h valkey-prod -p 6379 -t set,get -n 1000000 -c 50 -d 256
# 输出 SET/GET 的 QPS、延迟分布
提醒:基准要贴近真实负载(值大小
-d、并发-c、命令 mix-t)。脱离场景的 QPS 数字没有意义。
七、生产部署与性能优化
7.1 内存与淘汰策略
maxmemory 8gb
maxmemory-policy allkeys-lru # 缓存场景:满了就 LRU 淘汰
# 状态数据可选 volatile-lru / noeviction(满了写报错,保数据)
常用策略:
allkeys-lru:纯缓存,允许丢;volatile-lru:只淘汰带过期的键;noeviction:不淘汰,写满报错(适合不能丢的数据,但需监控)。
7.2 大 key 与热 key
- 大 key(如一个存了 10 万成员的 Set)会阻塞单线程、拖慢迁移。用
redis-cli --bigkeys巡检。 - 热 key(某个 key 被疯狂访问)会造成单节点瓶颈。解法:本地缓存 + 打散(给 key 加随机后缀分片)。
valkey-cli --bigkeys # 找出大 key
valkey-cli --hotkeys # 找出热 key(需开启 maxmemory + 相关配置)
7.3 多实例而非一个超大实例
内存数据库对单实例内存有现实上限(fork 做 RDB/AOF 重写时需要双倍内存余量)。经验法则:单实例控制在 10~25GB 以内,需要更大容量就上 Cluster 分片。
7.4 Sentinel vs Cluster 怎么选
- Sentinel:一主多从 + 哨兵做故障转移,解决「高可用」,但不解决容量(数据仍在单主)。
- Cluster:分片扩容量 + 自带故障转移,解决「高可用 + 大容量」。
# 最少 Sentinel 部署:3 个哨兵监控 1 主 2 从
valkey-sentinel /etc/sentinel.conf
# sentinel.conf 里指定监控的主
sentinel monitor mymaster valkey-master 6379 2
7.5 安全基线
# 禁止危险命令(生产必做)
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS "" # 或限制给运维账号
requirepass <强密码>
# 云上务必绑定内网 + 安全组,不要 0.0.0.0 暴露公网
bind 10.0.0.10
八、适用与不适用
Valkey 适合:
- 缓存(页面/DB 查询缓存、会话);
- 排行榜、计数、限流;
- 消息队列(Stream)、发布订阅;
- 需要亚毫秒延迟的共享状态(分布式锁、配置中心);
- 想避开 Redis 新许可证约束的团队。
Valkey 不适合:
- 需要复杂联表查询、事务跨多键强一致的关系型场景(那是 PostgreSQL/MySQL);
- 超大规模分析聚合(那是 DuckDB/ClickHouse);
- 需要 SQL 的湖仓(那是 DuckLake/DuckDB)。
一句话:它是内存里的「瑞士军刀」,不是数据库全家桶。
九、总结与展望
Valkey 的故事,本质上是一次「社区用代码投票」:当上游许可证触碰到生态红线,开源精神让整个行业用分叉找回了确定性。对工程师而言,最幸运的是——Valkey 在协议、客户端、数据模型上与 Redis 几乎完全一致,迁移成本低到「改个连接串」。
从工程价值看,Valkey 9.0 的原子槽位迁移、哈希字段过期、集群多编号库,都是冲着「生产痛点」去的;而更优的内存编码,又让它在同等硬件下更省。对于一个新项目,没有理由不从 Valkey 起步;对于一个被 Redis 新许可证束缚的老项目,迁移是一次性投资、长期免焦虑。
2026 年的内存数据库格局,已经从「Redis 一家独大」变成「Redis + Valkey 双轨」。作为一名务实的程序员,我的建议很直接:默认 Valkey,把许可证风险挡在门外,把性能留在手里。
参考与延伸:
- Valkey 官网:https://valkey.io
- Valkey GitHub:https://github.com/valkey-io/valkey
- Linux Foundation 公告与迁移指南
- RESP 协议规范(与 Redis 兼容)
- Valkey 9.0 Release Notes(原子槽位迁移 / HFE / 集群多编号库)