Google Cloud 已正式推出 Memorystore for Valkey 9.1。根据公开基准测试,它在保持微秒级延迟的同时,相比 Memorystore for Redis Cluster 可实现最高 3 倍 QPS。这个提升并非简单地增加线程,而是来自一次线程通信机制的重构:Valkey 用无锁多队列替代轮询列表,并根据 CPU 使用率和任务积压动态启停 I/O 线程。
对于承载实时推荐、会话状态、限流、AI 推理缓存或突发流量的系统,这次升级的价值不只在跑分。数据库级 ACL、拓扑感知的 CLUSTERSCAN,以及 HGETDEL、MSETEX 等新命令,也减少了应用层补丁代码和额外网络往返。
性能变化的核心:不再让主线程反复轮询
高吞吐内存数据库通常需要把网络读写交给后台 I/O 线程,从而让主执行线程集中处理命令。旧的线程模型会以轮询方式把客户端套接字静态分配给 I/O 线程,主线程还要不断检查待处理客户端列表,确认哪些工作已经完成。
这种设计有两个明显问题:
- 即使没有可收割的结果,主线程也可能消耗 CPU 遍历列表;
- 客户端与线程静态绑定,遇到连接热点或负载不均时,部分线程繁忙,另一些线程却可能空闲。
Valkey 9.1 将这套机制替换为三类互补的无锁队列:
- 主线程到 I/O 线程的 SPMC 队列:主线程作为单一生产者提交读写任务,多个工作线程按需取走任务。空闲线程可以主动接活,降低热点和线程饥饿风险。
- I/O 线程到主线程的 MPSC 队列:多个工作线程把完成的任务推回同一队列,主线程直接弹出结果,不再忙等和遍历列表。
- 每个 I/O 线程独立的 SPSC 队列:用于线程亲和的内存清理及高吞吐
epoll卸载,减少不必要的跨线程协调。
这类设计的重点不是“队列更多”,而是让任务流向更明确:谁生产、谁消费、哪些操作必须留在线程本地,都由专门的数据通道表达。由此可以减少缓存争用、跨线程唤醒和无效 CPU 消耗。
I/O 线程从固定配置变成按负载伸缩
Valkey 9.1 还引入了两阶段动态伸缩机制。
当主线程 CPU 使用率超过 30% 时,系统会启动第一个后台 I/O 线程,在请求队列形成严重瓶颈之前分担流量。完成“点火”后,系统继续观察 SPMC 队列的实时积压量,按需增加或减少活跃 I/O 工作线程。
这与始终开启全部线程相比更适合负载波动明显的业务:
- 低峰期可以停放空闲线程,避免无意义的调度和 CPU 消耗;
- 突发流量出现时,根据队列深度使用额外核心;
- 不再完全依赖静态阈值猜测线上并发模式。
不过,“最高 3 倍 QPS”不能直接等价为所有应用都会获得 3 倍吞吐。实际收益仍取决于命令结构、值大小、流水线深度、连接数、分片数、热点键、网络距离和节点规格。升级前应使用自己的请求分布进行压测,并同时观察 P50、P95、P99 延迟,而不是只看总 QPS。
新命令把常见业务逻辑下沉到服务端
原子读取并删除:HGETDEL
一次性令牌、临时验证码和短期状态经常需要“读取成功后立即删除”。如果应用先执行 HGET,再执行 HDEL,两个客户端可能读到同一个值;即使使用事务或 Lua 脚本,也会增加实现和维护成本。
Valkey 9.1 的 HGETDEL 可以在一次命令、一次网络往返中完成读取和删除:
# 将地址、端口和密码替换为自己的 Memorystore for Valkey 连接信息
export VALKEY_HOST="10.0.0.10"
export VALKEY_PORT="6379"
export VALKEY_PASSWORD="replace-me"
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
HSET user:1001 temp_token abcde
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
HGETDEL user:1001 FIELDS 1 temp_token
# 再次读取应返回 nil
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
HGET user:1001 temp_token
原子性解决的是并发竞争,但不等于完整的业务幂等性。如果令牌被取出后,下游处理失败,令牌不会自动恢复。支付、库存等关键流程仍需设计状态机或补偿机制。
多个键共享过期时间:MSETEX
需要同时创建多个临时状态时,过去通常要执行 MSET 后再逐个设置 TTL,或者通过 pipeline 减少网络往返。MSETEX 可以一次写入多个键,并为它们设置相同过期时间:
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
MSETEX 2 session:auth ok session:user_id 1001 EX 300
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
TTL session:auth
这里的 2 表示键值对数量,EX 300 表示 300 秒后过期。这适合会话初始化、临时工作流上下文和短期推理结果,但不应假设多个键会在完全相同的物理时刻被清除:过期回收的具体执行时间仍可能受实现和负载影响。
带条件的 HSETEX
HSETEX 新增 NX 和 XX 条件:NX 只在字段不存在时写入,XX 只在字段已经存在时更新。例如,可以在不覆盖现有限流配置的前提下初始化阈值:
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
HSETEX config:123 NX EX 3600 FIELDS 1 rate_limit 100
在把这类命令用于生产环境前,应确认客户端能够透传新命令。即使 SDK 尚未提供专用方法,很多客户端也允许通过 execute_command 一类接口发送原生命令。
CLUSTERSCAN:在拓扑变化中扫描整个集群
传统的集群扫描脚本通常先枚举节点,再分别执行 SCAN。当扫描期间发生槽迁移或节点故障转移时,脚本可能漏掉键、重复返回键,或者因为重定向处理不完整而失败。
CLUSTERSCAN 使用拓扑感知游标。游标包含当前槽、本地哈希表指纹和本地扫描位置,使客户端可以在集群拓扑变化时继续处理重定向。
单工作线程可以从游标 0 开始顺序扫描:
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
CLUSTERSCAN 0 MATCH 'user:*' COUNT 100
命令会返回新游标和一批键。调用方需要持续把新游标传给下一次请求,直到返回的游标重新变为 0。下面是一个可改造的 Bash 循环;它假设键名不包含换行,并使用 --raw 简化输出解析:
#!/usr/bin/env bash
set -euo pipefail
: "${VALKEY_HOST:?set VALKEY_HOST}"
: "${VALKEY_PORT:=6379}"
: "${VALKEY_PASSWORD:?set VALKEY_PASSWORD}"
cursor="0"
while true; do
mapfile -t result < <(
redis-cli --raw \
-h "$VALKEY_HOST" \
-p "$VALKEY_PORT" \
-a "$VALKEY_PASSWORD" \
CLUSTERSCAN "$cursor" MATCH 'user:*' COUNT 100
)
cursor="${result[0]}"
for ((i = 1; i < ${#result[@]}; i++)); do
printf '%s\n' "${result[$i]}"
done
[[ "$cursor" == "0" ]] && break
done
追求吞吐时,可以使用 SLOT 参数把 16,384 个槽分给多个工作线程:
# 两个独立工作线程的起始请求示例
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
CLUSTERSCAN 0 SLOT 0 MATCH 'user:*' COUNT 100
redis-cli -h "$VALKEY_HOST" -p "$VALKEY_PORT" -a "$VALKEY_PASSWORD" \
CLUSTERSCAN 0 SLOT 1000 MATCH 'user:*' COUNT 100
每个工作线程必须继续使用与自身游标匹配的槽,否则会返回错误。并行扫描还需要自行安排槽分区、重试、限速和结果去重。COUNT 是工作量提示,不应被当成严格的返回数量保证;生产任务也应避免让大规模扫描挤占在线请求资源。
数据库级 ACL:隔离不必完全依赖键前缀
Memorystore for Valkey 已支持集中式 ACL 管理,可将一项策略关联到多个集群,并结合版本化策略和审计日志管理权限。Valkey 9.1 进一步允许按数字数据库限制访问范围,例如:
production user: @all ~* db=0
staging user: @all ~* db=1
development user:@all ~* db=2
这可以防止测试服务误读生产数据库,也能减少纯靠 prod:、staging: 键前缀隔离所带来的应用约束。不过,数据库级隔离不应被视为所有安全边界的替代品。高风险租户仍应结合独立实例或集群、IAM、网络隔离、最小命令权限、密钥轮换和审计告警。
迁移时不要把切换端点当成全部工作
从自建 Redis OSS 或 Valkey 迁移到托管版时,可以采用四步流程:
- 按所需分片数、节点规格和集群数据库配置创建目标实例;
- 建立从源数据库到 Memorystore 的持续在线复制;
- 监控复制指标,确认数据集对齐并评估复制延迟;
- 将应用连接端点切换到目标实例。
实际落地时,建议再补充一份切换清单:
- 盘点应用使用的命令、Lua 脚本、模块和客户端版本;
- 验证超时、连接池、TLS、认证及集群重定向配置;
- 用真实键和值大小重放读写流量,比较尾延迟;
- 在切换前降低 DNS TTL,或准备可动态更新的服务发现配置;
- 定义写入冻结窗口、回滚条件和旧集群保留时间;
- 切换后监控错误率、连接数、CPU、内存、队列积压、命中率和 P99 延迟。
Valkey 9.1 还建立在此前版本提供的原生 JSON、Bloom Filter 以及更多节点规格之上,可覆盖轻量微服务、CPU 密集型负载和大内存集群。不过,节点越大并不一定越经济:热点键、单命令复杂度或客户端串行调用造成的瓶颈,通常不能只靠扩容解决。
是否值得升级:用业务流量回答
如果系统受限于网络 I/O、高连接并发或突发流量,Valkey 9.1 的无锁队列和动态 I/O 线程更可能带来明显收益;如果瓶颈来自大键、慢命令、跨区域访问或糟糕的数据模型,版本升级只能解决其中一部分问题。
建议先选择一个可回滚的非核心集群,重放代表性流量,并对比吞吐、CPU 成本和尾延迟。确认新命令、ACL 与客户端兼容后,再逐步迁移会话、推荐缓存、限流状态等工作负载。把“最高 3 倍”视为值得验证的上限,而不是容量规划中的固定倍数,才能把这次升级真正转化为稳定的生产收益。