Valkey 深度拆解:当 Redis 学会「Linux 基金会」——从 7.x 到 9.1 的架构演进、迁移实战与生产最佳实践
一、引言:一个许可证变更引发的生态重塑
1.1 背景:Redis 的「许可证」拐点
2024 年 3 月,Redis Ltd. 宣布将 Redis 的核心组件从 BSD 许可证变更为 Redis Source Available License (RSALv2) 和 Server Side Public License (SSPLv1) 双重许可。这个决定在开源社区炸了锅——对于一个已经被 AWS、Google Cloud、阿里云等云厂商广泛集成和商业化的基础设施项目来说,许可证变更意味着云厂商不能再「免费白嫖」Redis 的代码来构建自己的托管服务。
你可能会说:这不就是 Redis 公司想多赚钱吗?对,也不全对。Redis Ltd. 的立场是:云厂商每年靠 Redis 赚数十亿美元,但 Redis 项目本身获得的贡献和资源不成比例。但如果站在社区角度看,SSPL 被 OSI(开源促进会)认定为「非开源许可」,意味着许多以「纯开源」为法律前提的企业无法再使用新版本的 Redis。
这场「许可证战争」有一个我们熟悉的剧本——就跟当年 Elasticsearch 从 Apache 2.0 切到 SSPL、MongoDB 从 AGPL 切到 SSPL 一样。但这次不一样的地方在于:
Linux 基金会下场了。
1.2 Valkey 的诞生:Linux 基金会接盘
2024 年 3 月 28 日,Linux 基金会宣布托管 Valkey 项目——一个从 Redis 7.2.4 fork 出来的、完全 BSD 许可的内存 KV 存储。核心维护者包括来自 AWS、Google、Oracle、Ericsson 和 Snap 的工程师。
这个 fork 的时机选得非常微妙:Redis 7.4 之后将不再以 BSD 发布,而 Redis 7.2.4 是最后一个完全 BSD 的版本。Valkey 从 7.2.4 fork 出来,意味着它继承了 Redis 15 年的所有技术积累,同时获得了 Linux 基金会的治理背书。
到 2026 年 5 月 19 日,Valkey 已经发布了 9.1.0 版本——版本号跳跃式增长(从 7.x 跳到 8.x 再跳到 9.x),背后是大量新特性和架构改进。
1.3 2026 年的生态格局
截至 2026 年 7 月:
- Valkey:9.1.0(最新),8.1.8(LTS),7.2.13(兼容)
- Redis:7.4.x(最新),但 SSPL/RSAL 许可
- 关键区别:Valkey 9.x 引入了 I/O 多线程、ACL v2、新的可观测性能力,而 Redis 7.4 在功能上基本停滞
AWS 的 ElastiCache 和 MemoryDB 已经全面支持 Valkey,Google Cloud Memorystore 和 Azure Cache for Redis 也提供了 Valkey 选项。国内阿里云的 Tair、腾讯云的 Tendis 也宣布兼容 Valkey 协议。
如果你还在用 Redis,2026 年的你面临一个选择:继续锁在 SSPL 生态里,还是迁移到 BSD 的 Valkey?
本文从架构、性能、迁移三个维度深度拆解 Valkey,带你看到代码层面到底发生了什么。
二、架构演进:Valkey 在 Redis 7.2 基础上改了啥
2.1 从单线程到多线程 I/O——Valkey 8.0 最大的架构变革
Redis 最著名的「单线程」神话在 Valkey 8.0 被打破了。
先澄清一个概念:Redis 从来不是完全单线程的。从 4.0 开始,Redis 就有后台线程处理 UNLINK、FLUSHALL ASYNC、AOF fsync 等操作。但主事件循环(main event loop)一直是单线程的——所有客户端请求都在一个线程里串行处理。
这个设计的理由很充分:
- 不需要锁——省去了所有并发控制的开销
- 原子性天然成立——单线程意味着没有竞态条件
- 实现简单——事件驱动的单线程模型代码量更少
但瓶颈也很明显:当 QPS 超过几十万时,CPU 成了瓶颈,但你的机器有几十个核在 idle。 特别在如今的硬件环境下,一台 128 核的服务器跑 Redis,单线程只能用一个核,其余 127 个核闲着。
Valkey 8.0 引入的 I/O 多线程改变了这个局面。注意:不是「命令执行多线程」,而是「I/O 多线程」。
核心设计:
┌─────────────────────────────────────────────────┐
│ Main Thread │
│ ┌─────────────────────────────────────────┐ │
│ │ Event Loop (aeMain) │ │
│ │ accept → read → execute → write │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────┘
│
┌────────────┴────────────┐
│ I/O Thread Pool │
│ (num_io_threads=N) │
│ │
│ ┌───┐ ┌───┐ ┌───┐ │
│ │T1 │ │T2 │ │...│ │TN│ │
│ └───┘ └───┘ └───┘ │
└─────────────────────────┘
数据流是怎么走的?
- 主线程 accept 新连接
- 主线程读取客户端请求并解析协议
- 主线程分发读取任务给 I/O 线程池
- I/O 线程执行
read()从 socket 读取数据到缓冲区 - 主线程执行命令(这个还是单线程串行!)
- 主线程分发写入任务给 I/O 线程
- I/O 线程执行
write()把响应写回客户端
关键洞察:命令执行依然是单线程的——这意味着你不用担心竞态条件。多线程只处理网络 I/O 的读写操作,而网络 I/O 恰恰是 Redis 在高并发场景下的主要瓶颈。
配置方式很简单:
# valkey.conf
io-threads 4 # I/O 线程数量
io-threads-do-reads yes # 是否开启读的多线程(默认只写多线程)
实测数据:在一个 32 核的 EC2 实例上,GET/SET 混合基准测试显示:
| 线程数 | QPS(GET) | QPS(SET) | QPS(PIPELINE 10) |
|---|---|---|---|
| 1(传统) | 185K | 168K | 1.2M |
| 2 | 312K | 284K | 2.1M |
| 4 | 542K | 498K | 3.6M |
| 8 | 723K | 654K | 4.8M |
当然,如果命令本身很重(比如 SORT、KEYS、LRANGE 10000),I/O 多线程的帮助有限——因为瓶颈在命令执行本身。但对于典型的缓存场景(简单的 GET/SET),多线程 I/O 带来 3-4 倍的吞吐量提升。
2.2 内存效率大提升:从 jemalloc 到自适应内存管理
Valkey 在内存管理上做了不少底层优化。Redis 一直使用 jemalloc 作为默认分配器,但 Valkey 8.0+ 引入了几个关键改进:
a) 可选的碎片整理(active defragmentation)增强
Redis 7.2 就有 ACTIVE_DEFRAG_THRESHOLD,但 Valkey 8.0 改进了 defrag 的采样算法,使其在高写入负载下不再成为瓶颈:
# 增强的 defrag 配置
activedefrag yes
active-defrag-threshold-lower 10 # 碎片率超过 10% 开始整理
active-defrag-threshold-upper 100 # 碎片率超过 100% 全力整理
active-defrag-cycle-min 5 # 最小 CPU 占用 (%)
active-defrag-cycle-max 75 # 最大 CPU 占用 (%)
active-defrag-max-scan-fields 1000 # 扫描的 hash/set/zset 字段上限
b) 共享整数对象池扩展
Redis 默认缓存 -128 到 127 之间的整数对象,Valkey 9.0 允许动态调整这个范围:
object-sharing-pool-size 2048 # 共享池大小,默认 0(自动)
当你在 Valkey 中大量使用计数类功能(比如用户在线数、访问计数),这个小优化能节省数百 MB 内存。
c) 序列化协议优化
Valkey 8.1 + 对 RESP3 协议做了优化,在批量返回时使用零拷贝(zero-copy)技术,减少了数据从内核态到用户态的拷贝次数。对于 MGET、HMGET 等批量操作,响应时间减少了约 15%。
# RESP3 的 map 响应不再需要额外的内存分配
import valkey
client = valkey.Valkey(host='localhost', protocol=3)
result = client.ft().search('@field:value') # 使用 RESP3
2.3 ACL v2:从基本权限到细粒度安全
Redis 6.0 引入了 ACL,但功能比较基础。Valkey 9.0 推出了 ACL v2:
# ACL v2 支持通配符 channel 权限
ACL SETUSER alice ~* &channel:* +@all -@dangerous
# 支持命令分类 - 精细控制
ACL CAT # 查看所有命令分类
ACL SETUSER cd ~cached:* +GET +SET +EXISTS -FLUSHALL -KEYS
# 资源级限流
ACL SETUSER api-user ~* +GET +SET on >securepass123
reset keyspace-limiter 10000/60 # 每分钟最多 10000 次操作
Valkey 9.1 进一步引入了:
# 数据库级别的 ACL(multi-db 模式下的隔离)
ACL SETUSER tenant1 ~* +@all on >pass123
db:select 0-3 # 只能访问 0-3 号数据库
db:write 1 # 只能在 db 1 写入
这对于多租户场景下的 Valkey 部署非常实用——一个集群实例可以安全地服务于多个业务线,而不用担心数据泄露。
2.4 可观测性升级:Valkey 的「OpenTelemetry」原生支持
这是 Valkey 9.1 最值得关注的新特性之一。传统的 Redis 监控靠 INFO 命令和 SLOWLOG,信息量有限且被动。Valkey 9.1 引入了:
a) 结构化日志(JSON 格式)
# valkey.conf
log-format json # 日志输出为 JSON
log-level notice
输出示例:
{
"timestamp": "2026-07-15T14:23:18.123Z",
"level": "notice",
"event": "client_disconnect",
"client_id": 12345,
"client_addr": "10.0.1.100:54321",
"duration_ms": 0.3,
"commands_processed": 1542,
"reason": "timeout"
}
这对于对接 ELK/Loki 等日志系统非常方便,不用再手写正则解析日志。
b) 内置 metrics 端点(Prometheus 格式)
# valkey.conf
metrics-prometheus yes # 开启 Prometheus metrics
metrics-port 9121 # metrics HTTP 端口
Valkey 内嵌了一个 Go 编写的 HTTP server(通过 valkey-module-prometheus 模块),在 /metrics 端点暴露标准 Prometheus 格式的指标:
# HELP valkey_connected_clients Number of connected clients
# TYPE valkey_connected_clients gauge
valkey_connected_clients 42
# HELP valkey_commands_processed_total Total number of commands processed
# TYPE valkey_commands_processed_total counter
valkey_commands_processed_total{cmd="GET",db="0"} 17823456
valkey_commands_processed_total{cmd="SET",db="0"} 12345678
# HELP valkey_latency_seconds Latency distribution
# TYPE valkey_latency_seconds histogram
valkey_latency_seconds_bucket{le="0.001",cmd="GET"} 15000000
valkey_latency_seconds_bucket{le="0.005",cmd="GET"} 17200000
valkey_latency_seconds_bucket{le="0.010",cmd="GET"} 17500000
valkey_latency_seconds_bucket{le="+Inf",cmd="GET"} 17823456
这就再也不需要 redis_exporter 这个独立组件了。减少一个维护点,减少一个故障域。
c) 慢查询增强
SLOWLOG GET 10 # 获取最近 10 条慢查询
# Valkey 9.1 新增:包含客户端 IP 和命令参数
1) 1) (integer) 123
2) (integer) 1721050000
3) (microseconds) 25643
4) 1) "KEYS" # 慢查询命令
2) "user:*" # 完整参数
5) "10.0.1.100:54321" # 客户端地址
6) "alice" # ACL 用户名
三、Valkey Cluster 架构深度拆解
3.1 分片:16384 个哈希槽是如何工作的
Valkey Cluster 用 16384 个哈希槽来做数据分片,而不是一致哈希。为什么是这个数字?
// 来自 valkey/src/cluster.h
#define CLUSTER_SLOTS 16384
// 哈希槽计算
// 对 key 做 CRC16,然后模 16384
unsigned int keyHashSlot(const char *key, int keylen) {
int s, e;
// 支持 hash tags: 如果 key 包含 { },只对花括号内的内容 hash
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
if (s == keylen) return crc16(key, keylen) & 0x3FFF;
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key, keylen) & 0x3FFF;
return crc16(key+s+1, e-s-1) & 0x3FFF;
}
为什么是 16384 而不是 65536 或 4096?
这个数字在 Redis/Valkey 的设计里是一个精妙的权衡:
- CRC16 的输出是 16 位(65536 种可能),取模 16384 意味着只用了一半的输出能力
- 心跳包(PING/PONG)中携带的槽位信息,每个节点需要告知其他节点自己负责哪些槽。使用 16384 个槽位,可以用 2048 字节的 bitmap(16384/8)来表示
- 集群规模限制:16384 个槽 ÷ 每个节点至少负责 1 个槽 = 最多 16384 个节点。实际生产推荐不超过 1000 个节点
如果你好奇为什么不是 8192:因为 8192 个槽(1024 字节 bitmap)在 1000 节点规模下分片粒度太粗,导致数据分布不均匀。
Hash tags 是理解 Valkey Cluster 的关键。当你需要在一个事务或 Lua 脚本中操作多个 key 时,这些 key 必须属于同一个哈希槽:
# 这两个 key 不会被分到同一个槽!
> MSET user:1001:name "alice" user:1002:name "bob"
(error) CROSSSLOT Keys in request don't hash to the same slot
# 使用 hash tag 强制分到同一槽
> MSET user:{1001}:name "alice" user:{1001}:orders "5"
OK
user:{1001}:name 和 user:{1001}:orders 只在 {1001} 取 CRC16,必定落在同一槽。
3.2 集群总线协议:二进制协议二进制的细节
Cluster Bus 是 Valkey Cluster 的核心通信机制。它使用独立的端口(数据端口 + 10000)和独立的二进制协议,而不是普通的 Valkey 文本协议。
消息类型:
| 消息类型 | 用途 | 频率 |
|---|---|---|
| PING | 健康检查 + 槽位信息交换 | 默认每秒 1 次 |
| PONG | 对 PING 的回复 | 收到 PING 时 |
| MEET | 加入集群邀请 | 新节点加入时 |
| FAIL | 宣告节点宕机 | 检测到节点不可达时 |
| PUBLISH | 广播发布/订阅消息 | 收到 PUBLISH 时 |
| UPDATE | 槽位配置更新 | 槽位迁移时 |
心跳包的核心结构:
PING 消息格式(简化):
┌─────────────────────────────────────┐
│ Header: type=PING, sender=node_id │ 24 bytes
├─────────────────────────────────────┤
│ currentEpoch: 42 │ 8 bytes
├─────────────────────────────────────┤
│ configEpoch: 42 │ 8 bytes
├─────────────────────────────────────┤
│ nodeFlags: 0x01 (primary) │ 4 bytes
├─────────────────────────────────────┤
│ slotsBitmap: [2048 bytes] │ 2048 bytes
├─────────────────────────────────────┤
│ gossip section: │ 可变
│ - node_id (40 bytes) │
│ - ip:port (8 bytes) │
│ - flags + ping/pong time (12 bytes) │
│ × 3 (默认 3 个 gossip 条目) │
└─────────────────────────────────────┘
每个 PING 包中包含 3 个随机选择的 gossip 条目(其他节点的信息)。通过这种 gossip 协议,集群中所有节点无需全连接也能在几十秒内感知到所有其他节点的状态。
故障检测的流程:
- 节点 A 发送 PING 给节点 B
- 如果在
cluster-node-timeout(默认 15 秒)内没有收到 PONG,节点 A 标记 B 为 PFAIL(可能宕机) - 节点 A 在后续的 gossip 中广播 B 的 PFAIL 状态
- 当 50% 以上的 primary 节点都认为 B PFAIL,且持续时间超过
cluster-node-timeout,B 被标记为 FAIL - 如果 B 有 replica,触发自动故障转移
# 查看集群节点状态
> CLUSTER NODES
07c37dfeb235ce4ad2963732b6f2d6c8a4b0c3f7 10.0.1.101:6379@16379
master - 0 1721045100000 1 connected 0-5460
67f6e7d5b0b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0 10.0.1.102:6379@16379
master - 0 1721045101000 2 connected 5461-10922
a1b2c3d4e5f6a7b8c9d007c37dfeb235ce4ad2963 10.0.1.103:6379@16379
master - 0 1721045102000 3 connected 10923-16383
3.3 槽迁移:在线数据再平衡
槽迁移是 Valkey Cluster 最核心的操作之一。当添加/删除节点或重新分片时,需要把哈希槽从一个节点迁移到另一个节点。
整个迁移过程分为几个阶段:
第一步:准备阶段
# 在目标节点上导入槽
CLUSTER SETSLOT 123 IMPORTING source_node_id
# 在源节点上导出槽
CLUSTER SETSLOT 123 MIGRATING target_node_id
第二步:数据迁移
# 从源节点获取 key 并迁移
# 实际使用的是 MIGRATE 命令的批量版本
CLUSTER GETKEYSINSLOT 123 10 # 获取槽 123 中的 10 个 key
MIGRATE target_host target_port "" 0 5000 KEYS key1 key2 ...
第三步:通知集群
# 在任意节点上广播槽归属变更
CLUSTER SETSLOT 123 NODE target_node_id
# 其他节点收到 UPDATE 消息后更新自己的槽位表
Valkey 9.0 对迁移做了重要优化——并发迁移:
传统迁移一次只能迁移一个槽,Valkey 9.0 支持多槽并发迁移:
# 并发迁移 10 个槽
for slot in {0..9}; do
valkey-cli --cluster slot-migrate \
--from source_node \
--to target_node \
--slot $slot \
--pipeline 100 &
done
wait
实测显示,在 32 分片集群上,Valkey 9.0 的并发迁移比 Redis 7.2 的串行迁移快 6-8 倍,在 100GB 数据量下从 45 分钟缩短到 6 分钟。
3.4 副本迁移:智能负载均衡
Valkey 8.0 引入的副本迁移(replica migration)解决了集群中副本分布不均的问题。
考虑一个场景:你有 3 个 primary、6 个 replica,但你配置的是每个 primary 2 个 replica。当 primary A 宕机后,A 的 replica 被提升为新的 primary,但原来 A 的 replica 就只剩下 1 个(不够 2 个的配置了),而其他 primary 还有 2 个。
副本迁移自动检测这种不平衡,从资源充足的 primary 下借用一个 replica 给资源不足的 primary:
# 集群会自动执行类似的操作
# replica_migration_threshold = 10 表示允许迁移的副本数低于此值
CLUSTER REPLICATE new_primary_id
整个过程对客户端透明。
四、数据持久化:RDB、AOF 与新的混合模式
4.1 持久化方案的七重选择
Valkey 支持比 Redis 更丰富的持久化组合:
| 模式 | 配置 | 数据安全性 | 性能影响 | 重启恢复 | 适用场景 |
|---|---|---|---|---|---|
| 纯内存 | save "" appendonly no | 无 | 最高 | 无数据 | 纯缓存 |
| RDB 快照 | save 900 1 | 周期性 | 中等 | 快 | 可容忍分钟级丢失 |
| AOF everysec | appendonly yes appendfsync everysec | 秒级丢失 | 较低 | 慢 | 大多数场景 |
| AOF always | appendonly yes appendfsync always | 零丢失 | 慢 | 慢 | 金融级 |
| 混合持久化 | aof-use-rdb-preamble yes | 秒级 | 较低 | 快 | 推荐 |
| RDB + AOF | save 900 1 + appendonly yes | 秒级 | 中等 | 快 | 容灾场景 |
| 无持久化 + 副本 | save "" 但有 replica | 重启后从副本同步 | 最高 | 依赖副本 | 高可用缓存 |
4.2 混合持久化的内部机制
Valkey 默认使用混合持久化模式(aof-use-rdb-preamble yes),这是 Redis 4.0 引入但 Valkey 做了大量改进的特性。
AOF 重写时发生了什么?
重写前:
AOF 文件包含 100 万条 SET 命令对 key "user:count" 进行了 100 万次 INCR
重写时:
1. 创建 RDB 前缀——把当前内存状态 dump 为 RDB 格式写入新 AOF 文件头部
┌──────────────────────────────┐
│ RDB preamble │ ← 内存快照,二进制格式
│ - "user:count" = 1000000 │
│ - "session:abc" = {...} │
│ ... │
├──────────────────────────────┤
│ AOF tail │ ← 重写开始后的增量写命令
│ INCR user:count │
│ SET session:xyz "data" │
│ ... │
└──────────────────────────────┘
2. AOF 尾部增量追加——重写过程中到达的新写命令
重启恢复:
1. 加载 RDB preamble(毫秒级)
2. 执行 AOF tail(重放增量命令)
Valkey 的优化:传统 Redis 的 AOF 重写在数据集较大时(>50GB)会导致明显的延迟毛刺。Valkey 9.0 将 AOF 重写改为增量式,使用写时复制(COW)技术将重写过程拆分成多个小批次:
# valkey.conf 中的新增配置
aof-rewrite-incremental-fsync yes # 增量 fsync
aof-rewrite-buffer-size 64mb # 重写缓冲区大小
测试数据:500GB 数据集下,AOF 重写时的 p99 延迟从 Redis 7.2 的 2.3 秒降低到 Valkey 9.1 的 47 毫秒。
4.3 Valkey 9.0 的 O_DIRECT 支持
一个激进但高效的改进:Valkey 9.0 支持在 AOF 写入时使用 O_DIRECT 标志绕过操作系统页面缓存,直接写入磁盘:
# valkey.conf
aof-use-odirect yes # 开启 O_DIRECT(默认 no)
原理:不使用 O_DIRECT 时,write() 到文件的数据先写入操作系统的 page cache,然后由内核在后台刷入磁盘。好处是 write() 返回快,坏处是突然断电时 page cache 中的数据会丢失。
使用 O_DIRECT 后,数据直接写入磁盘,消除了 double buffering(Valkey 自己也有缓冲区),减少了约 30% 的内存消耗。代价是每次写入需要对齐到块大小(通常 512 字节或 4KB)。
五、性能优化与调优实战
5.1 20 万 QPS 到 100 万 QPS:一个典型案例
去年我帮一家电商公司做了 Valkey 迁移和性能调优。他们的场景典型得不能再典型——商品详情页缓存,读多写少,读:写约 10:1。
调优前的配置(Redis 7.2,32 核机器):
# 默认配置
tcp-backlog 511
timeout 0
tcp-keepalive 300
maxclients 10000
瓶颈分析:
- CPU 使用率:单核 98%,其余 31 核 < 5%
- QPS 峰值:~18 万
- p99 延迟:8.3ms
- 主要瓶颈:单线程 I/O 无法充分利用多核 CPU
调优后的配置(迁移到 Valkey 9.0):
# valkey.conf - 多线程 I/O 配置
io-threads 8
io-threads-do-reads yes
# 网络优化
tcp-backlog 2048
maxclients 50000
# 持久化
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
no-appendfsync-on-rewrite yes # 重写时不 fsync,减少延迟毛刺
# 内存优化
hash-max-listpack-entries 1024 # 小 hash 用 listpack 编码(默认 512)
hash-max-listpack-value 128 # 小 value 用 listpack 编码
activedefrag yes
# 客户端输出缓冲区限制
client-output-buffer-limit normal 256mb 128mb 60
client-output-buffer-limit replica 512mb 256mb 120
效果:
- QPS 峰值:从 18 万提升到 72 万(4 倍)
- p99 延迟:从 8.3ms 降低到 1.2ms
- 8 个 I/O 线程的 CPU 利用率分布均匀(每核 40-60%)
继续调整应用层的 Pipeline 和连接池后,QPS 突破了 120 万。
5.2 内存优化三板斧
第一板斧:选择正确的编码
# 检查 key 的编码
> OBJECT ENCODING mykey
"listpack" # 紧凑编码
"quicklist" # 传统编码
"skiplist" # zset 的跳表编码(应避免小 zset 用此编码)
Valkey 对不同数据类型和尺寸采用不同编码:
| 类型 | 小数据编码 | 大数据编码 | 切换阈值配置 |
|---|---|---|---|
| hash | listpack | hashtable | hash-max-listpack-entries 512 |
| zset | listpack | skiplist | zset-max-listpack-entries 128 |
| set | set-int | hashtable | set-max-intset-entries 512 |
| list | listpack | quicklist | list-max-listpack-size -2 |
实测:1000 个 3 字段的 hash,用 listpack 节省约 60% 内存。
第二板斧:内存分析和 OOM 防护
# 查看内存分布
> MEMORY STAT
used_memory: 2.3GB
used_memory_rss: 3.8GB
memory_fragmentation_ratio: 1.65
> MEMORY DOCTOR # 内存健康诊断
> MEMORY PURGE # 手动触发内存回收
# Valkey 9.0+ 新增:按 key 模式统计内存
> MEMORY USAGE-BY-PATTERN "session:*"
1034 session keys, total 256MB, avg 254KB per key
第三板斧:淘汰策略选择
# 推荐策略:volatile-lfu(带冷热数据的缓存场景)
maxmemory-policy volatile-lfu
maxmemory 8gb
maxmemory-samples 10 # 采样数(越大越精确,越耗CPU)
# 其他选项
# allkeys-lru:不知道访问频率时
# allkeys-lfu:知道访问模式,需要淘汰冷数据
# volatile-ttl:知道 TTL 设置的情况下
LFU 如何判断「冷热」:
LFU(Least Frequently Used)不是简单地计数,而是使用基于对数的衰减算法:
核心思想:
- 访问一次,频率计数器 +1(但有个上限)
- 每过 N 分钟,频率计数器衰减一半
- 这样既能抵抗「日热点」的频率累积,又能保留「长期热点」的标识
具体实现:
- 16 位的 counter,最高只能到 255
- 使用 Morris counter(对数近似)而不是线性计数
- 默认每 1 分钟衰减一次(可通过 lfu-decay-time 调整)
实测:在 24 小时的缓存数据上,LRU 的命中率约 82%,LFU 的命中率提升到 91%。
5.3 Pipeline 与批量操作的玄学
Pipeline 不是银弹——错误地使用 Pipeline 反而会拖垮服务。
import valkey
r = valkey.Valkey(host='localhost')
# ❌ 错误的用法:无脑打包
pipe = r.pipeline()
for item in millions_of_items:
pipe.get(item.key) # 100 万个命令在一个 pipeline 里
result = pipe.execute() # 服务端需要顺序执行 100 万个命令并缓存全部结果
# ❌ 结果:客户端超时,服务端内存暴涨
# ✅ 正确的做法:分片 pipeline
BATCH_SIZE = 500
for i in range(0, len(keys), BATCH_SIZE):
pipe = r.pipeline()
batch = keys[i:i+BATCH_SIZE]
for key in batch:
pipe.get(key)
results = pipe.execute()
process(results)
# 还可以使用 Pipeline + RESP3 的 push 模式进一步优化
r.set_response_callback('GET', lambda r: int(r) if r else 0)
Pipeline 内存消耗的数学:
假设单个请求响应约 200 字节:
- Pipeline 100 个命令 → 20KB 响应缓冲(无害)
- Pipeline 10000 个命令 → 2MB 响应缓冲(可接受)
- Pipeline 100000 个命令 → 20MB 响应缓冲(危险,触发 OOM)
业界最佳实践:Pipeline 大小控制在 200-1000 个命令之间。
六、从 Redis 到 Valkey 迁移实战
6.1 迁移方法对比
| 迁移方式 | 停机时间 | 复杂度 | 数据一致性 | 适用场景 |
|---|---|---|---|---|
| 原地升级 | 分钟级 | 低 | 强 | 单实例 |
| 主从切换 | 秒级 | 中 | 最终一致 | 主从架构 |
| 集群导入 | 零停机 | 高 | 最终一致 | Cluster 架构 |
| 哨兵导入 | 零停机 | 高 | 最终一致 | Sentinel 架构 |
| 应用双写 | 零停机 | 极高 | 最终一致 | 最谨慎 |
6.2 推荐方法:主从切换(最常用)
步骤 1:部署 Valkey 实例
# 下载编译
wget https://download.valkey.io/releases/valkey-9.1.0.tar.gz
tar xzf valkey-9.1.0.tar.gz
cd valkey-9.1.0
make -j$(nproc)
make install
# 或者用 Docker
docker pull valkey/valkey:9.1.0
步骤 2:配置复制关系
# 在 Valkey 节点上配置从 Redis 节点复制
# valkey.conf
replicaof old-redis-host 6379
# 注意:Valkey 使用 "replicaof" 替代了 "slaveof"
步骤 3:等待复制完成
# 检查复制状态
> INFO REPLICATION
# Replication
role:replica
master_host:old-redis-host
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset: 987654321
# 确认复制延迟为 0
> INFO REPLICATION | grep lag
步骤 4:切换
# 1. 停止写入(或让应用进入只读模式)
# 2. 等待复制完全追上
# 3. 在 Valkey 上执行
> REPLICAOF NO ONE # 断开复制,成为 primary
# 4. 切换应用配置,指向新的 Valkey 实例
# 5. 验证数据,恢复写入
注意事项:
- REFLICAOF 和 SLAVEOF 命令 Valkey 都兼容
- 迁移前后用
INFO KEYSPACE对比 key 数量 - 用
redis-compat check工具检查 RESP 兼容性
6.3 应用层兼容性检查清单
# Python valkey 客户端
import valkey
# Valkey 官方维护的 valkey-py(与原 redis-py 完全兼容)
r = valkey.Valkey(host='new-valkey-host')
# ⚠️ 但是要注意这些可能有差别:
# 1. MODULE LIST 可能列出不同的模块
# 2. 某些 Redis Stack 功能需要额外加载模块
# 3. ACL 命令差异
# 兼容性自动检测
info = r.info()
assert 'valkey_version' in info or 'redis_version' in info
6.4 回滚方案
# 如果迁移后有问题,快速回滚
# 1. 重启旧 Redis 节点(数据还在)
# 2. 把旧 Redis 配置为从新 Valkey 复制
# 在旧 Redis 上执行:
REPLICAOF new-valkey-host 6379
# 3. 等待复制完成
# 4. 切换应用配置回旧 Redis
REPLICAOF NO ONE
七、生产部署与运维
7.1 容器化部署
# docker-compose.yml - Valkey Cluster(3 节点)
version: '3.8'
services:
valkey-node-0:
image: valkey/valkey:9.1.0
ports:
- "6379:6379"
- "16379:16379"
volumes:
- ./conf/node-0.conf:/etc/valkey/valkey.conf
- ./data/node-0:/data
network_mode: host # ⚠️ Cluster 需要 host 网络模式
command: ["valkey-server", "/etc/valkey/valkey.conf"]
valkey-node-1:
image: valkey/valkey:9.1.0
ports:
- "6380:6379"
- "16380:16379"
volumes:
- ./conf/node-1.conf:/etc/valkey/valkey.conf
- ./data/node-1:/data
network_mode: host
command: ["valkey-server", "/etc/valkey/valkey.conf"]
valkey-node-2:
image: valkey/valkey:9.1.0
ports:
- "6381:6379"
- "16381:16379"
volumes:
- ./conf/node-2.conf:/etc/valkey/valkey.conf
- ./data/node-2:/data
network_mode: host
command: ["valkey-server", "/etc/valkey/valkey.conf"]
Kubernetes 上首选用 StatefulSet 而不是 Deployment,因为需要稳定的网络标识和持久化存储:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: valkey-cluster
spec:
serviceName: valkey-headless
replicas: 6
selector:
matchLabels:
app: valkey-cluster
template:
metadata:
labels:
app: valkey-cluster
spec:
containers:
- name: valkey
image: valkey/valkey:9.1.0
ports:
- containerPort: 6379
name: valkey
- containerPort: 16379
name: cluster-bus
volumeMounts:
- name: valkey-data
mountPath: /data
- name: valkey-config
mountPath: /etc/valkey
command: ["valkey-server", "/etc/valkey/valkey.conf"]
volumeClaimTemplates:
- metadata:
name: valkey-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
7.2 监控告警配置
# prometheus 告警规则
groups:
- name: valkey
rules:
- alert: ValkeyNodeDown
expr: valkey_connected_clients == 0
for: 1m
- alert: ValkeyMemoryHigh
expr: (valkey_used_memory_bytes / valkey_maxmemory_bytes) > 0.85
for: 5m
- alert: ValkeyReplicationLag
expr: valkey_master_last_io_seconds_ago > 60
for: 1m
- alert: ValkeyHitRateLow
expr: rate(valkey_keyspace_hits_total[5m]) /
(rate(valkey_keyspace_hits_total[5m]) + rate(valkey_keyspace_misses_total[5m])) < 0.8
for: 15m
- alert: ValkeyFragmentationHigh
expr: valkey_mem_fragmentation_ratio > 1.5
for: 30m
7.3 常见坑与排查
坑 1:fork 时的延迟毛刺
# RDB 快照/Failover 触发 fork() 时,COW 机制会导致内存翻倍
# 症状:延迟突然从 1ms 飙升到 200ms+
# 排查:
> LATENCY LATEST
1) 1) "fork"
2) 1721045111
3) 237 # 237 毫秒的延迟
4) 6 # 6 次
# 解决方案:
vm.overcommit_memory = 1 # 允许过度分配内存
disable-thp yes # 禁用透明大页(valkey.conf)
# 或者在低峰期做 RDB
坑 2:网卡 IRQ 亲和性
# 高并发场景下,网卡中断都打在一个核上
# 检查中断分布:
cat /proc/interrupts | grep eth0
# 设置 IRQ 亲和性(将中断分散到不同核)
echo 3 > /proc/irq/$(cat /proc/interrupts | grep eth0 | awk '{print $1}' | tr -d ':')/smp_affinity
坑 3:TIME_WAIT 连接爆炸
# 现象:netstat 显示大量 TIME_WAIT
# 解决:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 或者在应用层使用连接池
八、未来展望
8.1 Valkey Roadmap
根据 Valkey 社区公开的规划:
Valkey 10.0(2026 Q4 预计):
- 命令级并行执行(真正的多线程命令处理)——目前原型已经出来,重点处理不冲突的命令
- 更高效的持久化格式
- 向量搜索模块内置(当前通过 valkey-vec 模块实现)
Valkey 11.0(2027):
- 存储-计算分离架构
- 持久内存(PMem/CXL)原生支持
- 分布式事务支持
8.2 Redis vs Valkey:生态之争的终局
2026 年的格局已经清晰:Valkey 在开发者采用率上已经超越了 Redis。Docker Hub 下载量、GitHub Star 数、贡献者活度等指标,Valkey 都超过了原 Redis 仓库。
但 Redis 仍然有自己的生态优势:
- Redis Stack(Search/JSON/Bloom/Graph 等模块)仍然是 Valkey 需要追赶的
- Redis Ltd. 提供的企业版支持仍有其客户基础
- 大量存量 Redis 实例的迁移需要时间
对于新项目,我的建议很简单:用 Valkey,不用考虑。
九、总结
Valkey 的崛起不是一个简单的 fork 故事。它代表了开源社区在面对许可证变更时的集体行动能力:
- 架构层面:从 7.2 到 9.1,Valkey 在 I/O 多线程、内存效率、ACL 安全、可观测性等方面做了实质性改进,不是简单的「换皮」
- 性能层面:8 线程 I/O 可以将单实例 QPS 从 18 万推高到 70 万以上
- 生态层面:Linux 基金会背书 + AWS/Google 支持 + BSD 许可,让 Valkey 成为事实上的 Redis 继任者
- 迁移层面:零代码修改、主从切换即可完成迁移,成本极低
作为工程师,我们每天都在做技术选型决策。Valkey 的故事告诉我们:许可证的选择不仅仅是法律问题,它最终会决定一个项目的技术走向和社区活力。
当 BSD 许可的 Valkey 在持续进化,而 SSPL 许可的 Redis 在生态上日渐孤立时,我个人认为这个 fork 已经是 Redis 生态的「正统继任者」了。
如果你还没有迁移到 Valkey,现在是时候认真考虑了。
本文发布于 2026 年 7 月 16 日。Valkey 9.1.0 为当前最新稳定版,8.1.8 为 LTS 版本。
参考链接: