华为云DCS全形态原生Redis命令兼容性测试方案

1. 背景与目标

华为云 DCS(Distributed Cache Service)提供单机、主备、集群(Cluster)、读写分离、Proxy 集群等多种形态,不同形态在命令支持范围上存在差异:单机/主备基本等价于原生 Redis 单实例,集群形态受限于多 key 跨槽、事务、Lua 脚本等约束,Proxy 集群则在客户端与后端分片之间增加了一层代理,部分命令需要代理层转发或改写。

本文面向 DCS 全部形态,输出一套可复用的原生 Redis 命令字兼容性测试方案,包含测试范围、业界实践、经验教训、测试用例设计、执行流程、工作量评估与样例测试脚本,帮助团队在版本升级、形态扩容、内核补丁变更时快速完成回归验证。

测试目标:

  • 验证 DCS 各形态对原生 Redis 命令的功能一致性(返回值、错误码、边界行为)。
  • 识别形态差异导致的命令不可用/行为偏移,形成明确的兼容性矩阵。
  • 建立可自动化、可回归的测试基线,支撑版本发布质量门禁。
  • 量化测试工作量,为项目排期提供依据。

2. 测试范围与形态矩阵

2.1 被测形态

形态 说明 命令兼容性关注点
单机(Single) 单节点实例 与原生 Redis 单实例对齐,重点验证版本差异
主备(Master-Standby) 主从复制 + 哨兵/HA 复制相关命令、只读从节点行为、故障切换后命令可用性
集群(Cluster) 原生 Cluster 分片 跨槽多 key 命令、事务、Lua、SCAN 系列、MOVED/ASK 重定向
读写分离 主写从读 只读命令路由、写命令在从节点上的报错行为
Proxy 集群 代理层 + 后端分片 代理对命令的解析/改写、不支持命令的拦截提示、连接池行为

2.2 命令分类范围

按 Redis 官方命令分组覆盖,建议以 Redis 6.x/7.x 命令全集为基线:

  • String:SET/GET/SETNX/SETEX/PSETEX/GETSET/GETDEL/GETEX/APPEND/STRLEN/INCR/DECR/INCRBY/DECRBY/INCRBYFLOAT/SETRANGE/GETRANGE/MSET/MSETNX/MGET
  • Hash:HSET/HGET/HDEL/HGETALL/HMGET/HMSET/HINCRBY/HINCRBYFLOAT/HKEYS/HVALS/HLEN/HEXISTS/HRANDFIELD/HSCAN
  • List:LPUSH/RPUSH/LPOP/RPOP/LRANGE/LINDEX/LINSERT/LSET/LREM/LLEN/LTRIM/LPOS/LMOVE/BLPOP/BRPOP/BLMOVE/BRPOPLPUSH
  • Set:SADD/SREM/SMEMBERS/SISMEMBER/SMISMEMBER/SCARD/SINTER/SUNION/SDIFF/SINTERSTORE/SUNIONSTORE/SDIFFSTORE/SRANDMEMBER/SPOP/SSCAN/SMOVE
  • ZSet:ZADD/ZREM/ZSCORE/ZINCRBY/ZRANGE/ZRANGEBYSCORE/ZRANGEBYLEX/ZREVRANGE/ZRANK/ZREVRANK/ZCARD/ZCOUNT/ZUNIONSTORE/ZINTERSTORE/ZDIFFSTORE/ZSCAN/ZPOPMIN/ZPOPMAX/BZPOPMIN/BZPOPMAX/ZRANGESTORE
  • Bitmap / HyperLogLog / GEO:SETBIT/GETBIT/BITCOUNT/BITOP/BITPOS/BITFIELD、PFADD/PFCOUNT/PFMERGE、GEOADD/GEODIST/GEOPOS/GEOSEARCH/GEORADIUS
  • Stream:XADD/XRANGE/XREVRANGE/XLEN/XREAD/XGROUP/XACK/XPENDING/XCLAIM/XINFO/XTRIM/XDEL
  • Key 管理:DEL/UNLINK/EXISTS/EXPIRE/PEXPIRE/EXPIREAT/TTL/PTTL/PERSIST/TYPE/RENAME/RENAMENX/KEYS/SCAN/RANDOMKEY/DUMP/RESTORE/OBJECT/TOUCH
  • 事务与脚本:MULTI/EXEC/DISCARD/WATCH/UNWATCH、EVAL/EVALSHA/SCRIPT LOAD/SCRIPT EXISTS/SCRIPT FLUSH
  • 发布订阅:SUBSCRIBE/PUBLISH/PSUBSCRIBE/PUNSUBSCRIBE/PUBSUB
  • 服务端与连接:PING/ECHO/SELECT/AUTH/HELLO/CLIENT */COMMAND */INFO/CONFIG GET/CONFIG SET/DBSIZE/FLUSHDB/FLUSHALL/SLOWLOG
  • 集群相关:CLUSTER INFO/CLUSTER NODES/CLUSTER SLOTS/CLUSTER KEYSLOT/CLUSTER COUNTKEYSINSLOT/READONLY/READWRITE
  • 高危/受限命令:KEYS、FLUSHALL、FLUSHDB、CONFIG SET、DEBUG、SHUTDOWN、MONITOR、SORT、MIGRATE、SWAPDB

建议把命令按「必测(P0)/ 选测(P1)/ 受限确认(P2)」三级分层,P0 覆盖业务高频命令,P2 只做可用性确认与错误提示校验。

优先级分层清单(P0 / P1 / P2):

优先级 判定标准 用例深度 执行节奏
P0 必测 业务高频、核心链路依赖、跨形态行为差异大 每命令 5 条(正常 / 边界 / 异常参数 / 多 key / 类型错误) 每次提交与发布前必跑
P1 选测 中低频使用、功能明确但形态差异可控 每命令 3 条(正常 / 边界 / 异常) 夜间回归、版本发布前跑
P2 受限确认 高危、受限、云服务管控或形态本身不支持 每命令 1 条(可用性 + 错误提示校验) 版本发布前确认一次,结果入矩阵

P0 必测命令清单(约 80 个):

  • String:SET/GET/SETNX/SETEX/GETSET/GETDEL/GETEX/APPEND/STRLEN/INCR/DECR/INCRBY/DECRBY/INCRBYFLOAT/MSET/MGET
  • Hash:HSET/HGET/HDEL/HGETALL/HMGET/HINCRBY/HKEYS/HVALS/HLEN/HEXISTS/HSCAN
  • List:LPUSH/RPUSH/LPOP/RPOP/LRANGE/LINDEX/LREM/LLEN/LTRIM/BLPOP/BRPOP
  • Set:SADD/SREM/SMEMBERS/SISMEMBER/SCARD/SINTER/SUNION/SDIFF/SSCAN
  • ZSet:ZADD/ZREM/ZSCORE/ZINCRBY/ZRANGE/ZREVRANGE/ZRANK/ZCARD/ZCOUNT/ZSCAN/ZPOPMIN/ZPOPMAX
  • Key 管理:DEL/UNLINK/EXISTS/EXPIRE/TTL/PERSIST/TYPE/RENAME/SCAN
  • 事务与脚本:MULTI/EXEC/DISCARD/WATCH、EVAL/EVALSHA/SCRIPT LOAD
  • 服务端与连接:PING/ECHO/SELECT/AUTH/HELLO/CLIENT SETNAME/COMMAND INFO/INFO
  • 集群相关:CLUSTER INFO/CLUSTER NODES/CLUSTER SLOTS/CLUSTER KEYSLOT/READONLY/READWRITE

P1 选测命令清单(约 110 个):

  • String:PSETEX/SETRANGE/GETRANGE/MSETNX
  • Hash:HMSET/HINCRBYFLOAT/HRANDFIELD
  • List:LINSERT/LSET/LPOS/LMOVE/BLMOVE/BRPOPLPUSH
  • Set:SMISMEMBER/SINTERSTORE/SUNIONSTORE/SDIFFSTORE/SRANDMEMBER/SPOP/SMOVE
  • ZSet:ZRANGEBYSCORE/ZRANGEBYLEX/ZREVRANK/ZUNIONSTORE/ZINTERSTORE/ZDIFFSTORE/BZPOPMIN/BZPOPMAX/ZRANGESTORE
  • Bitmap / HyperLogLog / GEO:SETBIT/GETBIT/BITCOUNT/BITOP/BITPOS/BITFIELD、PFADD/PFCOUNT/PFMERGE、GEOADD/GEODIST/GEOPOS/GEOSEARCH
  • Stream:XADD/XRANGE/XREVRANGE/XLEN/XREAD/XGROUP/XACK/XPENDING/XCLAIM/XINFO/XTRIM/XDEL
  • Key 管理:PEXPIRE/EXPIREAT/PTTL/RENAMENX/RANDOMKEY/DUMP/RESTORE/OBJECT/TOUCH
  • 事务与脚本:UNWATCH/SCRIPT EXISTS/SCRIPT FLUSH
  • 发布订阅:SUBSCRIBE/PUBLISH/PSUBSCRIBE/PUNSUBSCRIBE/PUBSUB
  • 服务端与连接:CLIENT GETNAME/CLIENT KILL/CLIENT LIST/COMMAND COUNT/COMMAND DOCS/CONFIG GET/DBSIZE/SLOWLOG
  • 集群相关:CLUSTER COUNTKEYSINSLOT

P2 受限确认命令清单(约 50 个):

  • 高危命令:KEYS/FLUSHALL/FLUSHDB/CONFIG SET/DEBUG/SHUTDOWN/MONITOR/SORT/MIGRATE/SWAPDB
  • 受限/管控命令:SELECT(非 0 库)/RESET/FAILOVER/REPLICAOF/SLAVEOF/PSYNC/SAVE/BGSAVE/BGREWRITEAOF/LASTSAVE
  • 集群受限命令:CLUSTER SETSLOT/CLUSTER ADDSLOTS/CLUSTER DELSLOTS/CLUSTER FAILOVER/CLUSTER RESET/CLUSTER FORGET/CLUSTER MEET/CLUSTER REPLICATE
  • 模块与扩展命令:FUNCTION LOAD/FUNCTION LIST/FUNCTION DELETE/FCALL/MODULE LIST/MODULE LOAD
  • 其他低频命令:WAIT/WAITAOF/OBJECT ENCODING/OBJECT FREQ/OBJECT IDLETIME/LOLWUT/MEMORY USAGE/MEMORY DOCTOR/LATENCY RESET/LATENCY HISTORY

说明:以上清单以 Redis 7.x 常用命令为基线,合计约 240 个。实际执行时按实例版本裁剪:Redis 6 实例剔除 GETDEL/GETEX/SINTERCARD/ZRANGESTORE/FUNCTION 等 7.x 新增命令,并在矩阵中标注 N/A。P2 命令不追求功能全覆盖,只确认「是否可用 + 不可用时的错误提示是否清晰」,避免把云服务的有意管控误判为缺陷。

2.3 命令兼容性矩阵模板

兼容性矩阵是整套测试方案的核心交付物:行是命令(按分类归组),列是 5 种形态,单元格记录该命令在该形态下的实测结论。下表给出表头与示例填充,实际执行时按命令全集逐行展开。

命令分类 命令 / 用例 单机(Single) 主备(Master-Standby) 集群(Cluster) 读写分离 Proxy 集群
String MGET(同槽多 key) PASS PASS PASS PASS PASS
String MGET(跨槽多 key) PASS PASS FAIL(CROSSSLOT) PASS PASS_WITH_DIFF(代理层改写为多次单 key 查询)
Hash HSCAN(大 key 游标遍历) PASS PASS PASS_WITH_DIFF(仅遍历当前分片,需配合 CLUSTER SLOTS 全量遍历) PASS PASS
事务与脚本 MULTI / EXEC(多 key 同槽) PASS PASS PASS PASS PASS_WITH_DIFF(代理层事务语义受限,需确认原子性)
集群相关 CLUSTER SLOTS N/A(非集群形态无此命令) N/A PASS N/A PASS_WITH_DIFF(返回代理视角的拓扑,与后端分片映射不同)
高危/受限命令 KEYS PASS_WITH_DIFF(可用但受性能管控) PASS_WITH_DIFF FAIL(禁用或限流) PASS_WITH_DIFF FAIL(代理层拦截,返回明确错误)

单元格取值含义:PASS 表示与原生 Redis 行为一致;PASS_WITH_DIFF 表示可用但存在可接受的行为差异(需在备注列写明差异点,如错误信息前缀、返回顺序、拓扑视角不同);FAIL 表示命令不可用或行为不符合预期,必须提单跟踪;N/A 表示该形态本身不提供此命令,不计入失败。

矩阵的自动填充与 CI 门禁:

  1. 自动填充 :7.2 节的执行脚本按「形态 × 用例」遍历,每条用例产出 {form, case_id, result, detail} 记录;报告生成阶段以 case_id 关联用例元数据(命令分类、命令名、优先级),按「命令分类 → 命令 → 形态」透视成上表结构,detail 非空时自动写入备注列,无需人工填表。
  2. 基线比对 :首次全量执行的结果归档为 baseline.json(含环境指纹:实例版本、规格、分片数、代理版本、客户端版本)。后续每次执行与基线做 diff,输出三类变化:新增 FAIL、PASS → PASS_WITH_DIFF、N/A → PASS(能力增强)。
  3. CI 门禁规则 :将矩阵生成脚本接入流水线,在版本发布、内核补丁、形态扩容前后自动触发。门禁判定建议为------出现新增 FAIL 即阻断发布 ;PASS → PASS_WITH_DIFF 需人工确认并更新基线后方可放行;N/A → PASS 仅记录不阻断。
  4. 产物归档 :每次流水线运行输出 matrix.md(人读)与 matrix.json(机读)两份产物,随版本号归档,形成可追溯的兼容性演进历史。

说明:本节与 3.4 节「兼容性矩阵与门禁」为同一套机制的展开------3.4 节给出原则性结论,本节给出可落地的自动填充、基线比对与门禁判定细则;5.1 节流程图中的「生成兼容性矩阵报告 → 是否存在新增 FAIL → 归档基线并纳入 CI」即对应本节第 1~4 步。

3. 业界优秀实践

3.1 以官方命令规范为唯一事实来源

  • 以 Redis 官方文档的命令语法、返回值类型、错误信息、时间复杂度为断言基准,避免"凭经验判断"。
  • 使用 COMMAND / COMMAND INFO / COMMAND DOCS 输出作为命令元数据快照 ,与原生 Redis 逐条 diff,可快速发现命令缺失、参数个数(arity)、flags(如 write/readonly/fast)不一致。
  • 对每个命令维护一份 JSON 断言基线(输入 → 期望返回/期望错误前缀),测试脚本只做"执行 + 比对",便于跨形态复用。

3.2 分层测试策略

  1. 元数据层 :COMMAND COUNT、COMMAND INFO 全量比对,成本极低、收益最高,先跑这一层。
  2. 单命令功能层:每命令 3~5 个用例(正常、边界、异常参数),校验返回值与错误码。
  3. 组合/语义层:事务、Lua、Pipeline、多 key 跨槽、过期与淘汰、阻塞命令超时。
  4. 稳定性层:长时间压测下的命令成功率、主备切换/扩缩容期间的命令可用性。

3.3 使用成熟工具做"半自动"覆盖

  • redis-py / Jedis / go-redis 官方客户端:验证客户端 SDK 与 DCS 的兼容性(连接、重定向、Pipeline、集群拓扑刷新)。
  • redis-benchmark :快速验证命令吞吐与可用性,-t 指定命令集。
  • redis-cli --cluster / --pipe:验证集群拓扑与批量导入。
  • 自研命令矩阵脚本:以 YAML/JSON 描述用例,驱动多形态并行执行,输出兼容性矩阵报告。

3.4 兼容性矩阵与门禁

  • 输出「形态 × 命令 × 结果 」三维矩阵,结果分为:PASS / PASS_WITH_DIFF(行为差异但可接受)/ FAIL(不可用)/ N/A(形态不支持)。
  • 将矩阵纳入 CI,版本发布前自动比对,新增 FAIL 即阻断发布。
  • 矩阵模板、单元格取值定义与自动填充/门禁细则见 2.3 节,本节只保留原则性结论,避免与 2.3 节重复维护。

4. 经验与教训

4.1 常见坑点

  • 跨槽多 key 命令 :MGET、MSET、SUNIONSTORE、ZUNIONSTORE、RENAME 等在集群形态下若 key 不在同一 slot 会返回 CROSSSLOT。测试必须显式构造同槽(hash tag {tag})与跨槽两组用例,否则容易漏测。
  • 事务与 Lua 的集群限制 :MULTI/EXEC 中涉及多 key 需同槽;Lua 脚本中访问的 key 必须显式通过 KEYS[] 传入且同槽,否则报错。Proxy 集群可能对脚本做额外限制。
  • 阻塞命令 :BLPOP/BRPOP/BZPOPMIN 在 Proxy 形态下超时行为、连接占用可能与原生不同,需验证超时精度与连接释放。
  • SCAN 系列 :集群形态下 SCAN 只遍历当前节点,需用 SCAN + CLUSTER SLOTS 遍历全部分片;HSCAN/SSCAN/ZSCAN 在大 key 下的游标行为需单独验证。
  • SELECT 与多 DB :集群形态仅支持 DB 0,SELECT 1 应返回错误;Proxy 形态需确认是否透传。
  • CONFIG SET / CONFIG GET :云服务通常对参数白名单管控,需验证允许修改的参数生效 、禁止修改的参数返回明确错误,而不是静默失败。
  • KEYS / FLUSHALL 等高危命令:多数云服务会禁用或限流,需验证返回的错误信息是否清晰可读,避免业务侧"命令无响应"的误判。
  • 错误信息差异 :同一错误在不同形态下前缀可能不同(如 ERR / WRONGTYPE / MOVED / ASK),断言应匹配错误类型前缀而非完整字符串。
  • 版本差异 :Redis 6 与 7 在 GETDEL、GETEX、SINTERCARD、ZRANGESTORE、FUNCTION 等命令上差异明显,需按实例实际版本裁剪用例集。

4.2 流程教训

  • 不要只测"能跑通" :必须校验返回值类型与内容,例如 INCRBYFLOAT 返回字符串精度、ZADD 返回新增元素个数。
  • 不要忽略连接层 :HELLO、RESET、CLIENT SETNAME、CLIENT KILL、连接空闲超时、最大连接数,往往是线上问题高发区。
  • 不要一次性全量跑:先跑元数据层与 P0 命令,快速拿到结论;P1/P2 放到夜间回归。
  • 测试数据要可清理 :统一 key 前缀(如 dcs:test:{形态}:{命令}:),用例结束 UNLINK 清理,避免脏数据影响后续用例与内存水位。
  • 记录环境指纹:实例版本、规格、分片数、代理版本、客户端版本、网络时延,全部写入报告头部,否则结果不可复现。

5. 测试方案设计

5.1 整体流程

#mermaid-svg-KubnaUrEtmJCWgkC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KubnaUrEtmJCWgkC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KubnaUrEtmJCWgkC .error-icon{fill:#552222;}#mermaid-svg-KubnaUrEtmJCWgkC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KubnaUrEtmJCWgkC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KubnaUrEtmJCWgkC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KubnaUrEtmJCWgkC .marker.cross{stroke:#333333;}#mermaid-svg-KubnaUrEtmJCWgkC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KubnaUrEtmJCWgkC p{margin:0;}#mermaid-svg-KubnaUrEtmJCWgkC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KubnaUrEtmJCWgkC .cluster-label text{fill:#333;}#mermaid-svg-KubnaUrEtmJCWgkC .cluster-label span{color:#333;}#mermaid-svg-KubnaUrEtmJCWgkC .cluster-label span p{background-color:transparent;}#mermaid-svg-KubnaUrEtmJCWgkC .label text,#mermaid-svg-KubnaUrEtmJCWgkC span{fill:#333;color:#333;}#mermaid-svg-KubnaUrEtmJCWgkC .node rect,#mermaid-svg-KubnaUrEtmJCWgkC .node circle,#mermaid-svg-KubnaUrEtmJCWgkC .node ellipse,#mermaid-svg-KubnaUrEtmJCWgkC .node polygon,#mermaid-svg-KubnaUrEtmJCWgkC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KubnaUrEtmJCWgkC .rough-node .label text,#mermaid-svg-KubnaUrEtmJCWgkC .node .label text,#mermaid-svg-KubnaUrEtmJCWgkC .image-shape .label,#mermaid-svg-KubnaUrEtmJCWgkC .icon-shape .label{text-anchor:middle;}#mermaid-svg-KubnaUrEtmJCWgkC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KubnaUrEtmJCWgkC .rough-node .label,#mermaid-svg-KubnaUrEtmJCWgkC .node .label,#mermaid-svg-KubnaUrEtmJCWgkC .image-shape .label,#mermaid-svg-KubnaUrEtmJCWgkC .icon-shape .label{text-align:center;}#mermaid-svg-KubnaUrEtmJCWgkC .node.clickable{cursor:pointer;}#mermaid-svg-KubnaUrEtmJCWgkC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KubnaUrEtmJCWgkC .arrowheadPath{fill:#333333;}#mermaid-svg-KubnaUrEtmJCWgkC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KubnaUrEtmJCWgkC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KubnaUrEtmJCWgkC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KubnaUrEtmJCWgkC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KubnaUrEtmJCWgkC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KubnaUrEtmJCWgkC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KubnaUrEtmJCWgkC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KubnaUrEtmJCWgkC .cluster text{fill:#333;}#mermaid-svg-KubnaUrEtmJCWgkC .cluster span{color:#333;}#mermaid-svg-KubnaUrEtmJCWgkC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KubnaUrEtmJCWgkC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KubnaUrEtmJCWgkC rect.text{fill:none;stroke-width:0;}#mermaid-svg-KubnaUrEtmJCWgkC .icon-shape,#mermaid-svg-KubnaUrEtmJCWgkC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KubnaUrEtmJCWgkC .icon-shape p,#mermaid-svg-KubnaUrEtmJCWgkC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KubnaUrEtmJCWgkC .icon-shape .label rect,#mermaid-svg-KubnaUrEtmJCWgkC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KubnaUrEtmJCWgkC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KubnaUrEtmJCWgkC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KubnaUrEtmJCWgkC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
准备环境:各形态实例 + 客户端
采集环境指纹与 COMMAND 元数据
元数据层比对:COMMAND INFO diff
P0 命令功能用例执行
组合语义用例:事务/Lua/Pipeline/跨槽
稳定性与故障切换用例
生成兼容性矩阵报告
是否存在新增 FAIL
提单阻断发布
归档基线并纳入 CI

5.2 用例设计模板

每条用例统一字段,便于脚本化:

字段 说明
case_id 唯一编号,如 STR-SET-001
command 命令名
args 参数列表(支持占位符)
preconditions 前置数据准备
expect_type 期望返回类型(string/int/array/nil/error)
expect_value 期望值或正则
expect_error_prefix 期望错误前缀,如 WRONGTYPE
cleanup 清理动作
priority P0/P1/P2
forms 适用形态列表

5.3 断言原则

  • 类型优先:先断言返回类型,再断言值,避免"值恰好相等但类型错误"的漏判。
  • 错误前缀匹配 :WRONGTYPE、ERR wrong number of arguments、CROSSSLOT、MOVED、ASK、NOAUTH、NOPERM。
  • 幂等可重跑:用例执行前清理同名 key,保证可重复执行。
  • 超时保护:阻塞类命令设置客户端超时,避免用例挂死。

5.4 环境与工具准备

  • 每种形态至少 1 个实例,规格一致(如 4G 主备、8G 集群 3 分片),网络同 VPC。
  • 客户端:redis-cli(版本与实例大版本对齐)、redis-py、Jedis、go-redis 各一套。
  • 监控:实例 CPU/内存/连接数/QPS 曲线,用于识别命令执行对资源的异常影响。
  • 日志:开启慢日志,收集被拦截命令的返回信息。

6. 工作量评估

6.1 评估口径

以「命令数 × 用例数 × 形态数」为基本模型,叠加脚本开发、环境准备、报告与回归的固定投入。

  • 命令总数:约 240 个(Redis 7.x 常用命令,剔除已废弃),与 2.2 节 P0/P1/P2 清单口径一致。
  • 平均用例数:P0 命令 5 条、P1 命令 3 条、P2 命令 1 条,与 2.2 节优先级分层表一致。
  • 形态数:5 种,与 2.1 节被测形态一致。

6.2 工作量估算表

工作项 内容 人日(估算)
需求与范围梳理 命令清单、优先级分层、形态矩阵确认 3
环境准备 5 种形态实例开通、网络与客户端环境、监控接入 3
用例设计 240 命令用例编写与评审(含边界与异常) 10
脚本开发 用例驱动框架、断言库、多形态并行执行、报告生成 8
元数据层比对 COMMAND INFO 采集与 diff 工具 2
组合语义用例 事务、Lua、Pipeline、跨槽、阻塞命令 5
稳定性与故障切换 主备切换、扩缩容期间命令可用性 4
执行与缺陷跟踪 全量执行 2 轮 + 缺陷复现与回归 6
报告与基线归档 兼容性矩阵报告、CI 接入 3
合计 约 44 人日

若仅做 P0 命令(约 80 个)与 3 种主力形态,工作量可压缩至 约 18~22 人日 ;若需覆盖全部形态 + 全量命令 + 稳定性长稳,建议预留 50~60 人日。

6.3 排期建议

  • 第 1 周:范围梳理、环境准备、元数据层比对(快速暴露明显缺失)。
  • 第 2~3 周:P0 用例设计与脚本开发,边开发边执行。
  • 第 4 周:组合语义与稳定性用例,输出首版矩阵报告。
  • 第 5 周:缺陷回归、CI 接入、基线归档。

7. 样例测试脚本

7.1 用例描述文件(YAML)

yaml 复制代码
# cases/string.yaml
- case_id: STR-SET-001
  command: SET
  args: ["dcs:test:{form}:str:k1", "v1"]
  expect_type: string
  expect_value: "OK"
  priority: P0
  forms: [single, master_standby, cluster, read_write_split, proxy_cluster]

- case_id: STR-SET-002
  command: SET
  args: ["dcs:test:{form}:str:k1", "v1", "NX"]
  preconditions:
    - command: SET
      args: ["dcs:test:{form}:str:k1", "v0"]
  expect_type: nil
  priority: P0
  forms: [single, master_standby, cluster, read_write_split, proxy_cluster]

- case_id: STR-INCR-001
  command: INCR
  args: ["dcs:test:{form}:str:counter"]
  preconditions:
    - command: SET
      args: ["dcs:test:{form}:str:counter", "abc"]
  expect_type: error
  expect_error_prefix: "ERR value is not an integer"
  priority: P0
  forms: [single, master_standby, cluster, read_write_split, proxy_cluster]

- case_id: STR-MGET-001
  command: MGET
  args: ["dcs:test:{form}:str:k1", "dcs:test:{form}:str:k2"]
  expect_type: array
  expect_value: ["v1", null]
  priority: P0
  forms: [single, master_standby, cluster, read_write_split, proxy_cluster]

- case_id: STR-MGET-002
  command: MGET
  args: ["dcs:test:{form}:str:k1", "dcs:test:{form}:other:k2"]
  expect_type: error
  expect_error_prefix: "CROSSSLOT"
  priority: P0
  forms: [cluster, proxy_cluster]

7.2 Python 执行脚本(redis-py)

python 复制代码
import json
import re
import time
import yaml
import redis
from redis.cluster import RedisCluster

FORMS = {
    "single":          {"mode": "standalone", "host": "10.0.0.11", "port": 6379, "password": "***"},
    "master_standby":  {"mode": "standalone", "host": "10.0.0.12", "port": 6379, "password": "***"},
    "read_write_split":{"mode": "standalone", "host": "10.0.0.13", "port": 6379, "password": "***"},
    "cluster":         {"mode": "cluster",    "host": "10.0.0.14", "port": 6379, "password": "***"},
    "proxy_cluster":   {"mode": "standalone", "host": "10.0.0.15", "port": 6379, "password": "***"},
}

def build_client(cfg):
    if cfg["mode"] == "cluster":
        return RedisCluster(
            host=cfg["host"], port=cfg["port"], password=cfg["password"],
            decode_responses=True, socket_timeout=5,
        )
    return redis.Redis(
        host=cfg["host"], port=cfg["port"], password=cfg["password"],
        decode_responses=True, socket_timeout=5,
    )

def render(args, form):
    return [a.replace("{form}", form) if isinstance(a, str) else a for a in args]

def check_type(value, expect_type):
    mapping = {
        "string": str, "int": int, "array": list, "nil": type(None),
        "float": float, "dict": dict,
    }
    if expect_type == "error":
        return True
    return isinstance(value, mapping.get(expect_type, object))

def run_case(client, case, form):
    key_prefix = f"dcs:test:{form}:"
    try:
        for pre in case.get("preconditions", []):
            client.execute_command(pre["command"], *render(pre["args"], form))
        args = render(case["args"], form)
        result = client.execute_command(case["command"], *args)
        if case["expect_type"] == "error":
            return "FAIL", f"expected error {case.get('expect_error_prefix')}, got {result!r}"
        if not check_type(result, case["expect_type"]):
            return "FAIL", f"type mismatch: expect {case['expect_type']}, got {type(result).__name__}"
        if "expect_value" in case and result != case["expect_value"]:
            return "FAIL", f"value mismatch: expect {case['expect_value']!r}, got {result!r}"
        return "PASS", ""
    except redis.exceptions.ResponseError as e:
        msg = str(e)
        prefix = case.get("expect_error_prefix", "")
        if case["expect_type"] == "error" and prefix and msg.startswith(prefix):
            return "PASS", ""
        return "FAIL", f"unexpected error: {msg}"
    except Exception as e:
        return "FAIL", f"exception: {type(e).__name__}: {e}"
    finally:
        try:
            client.delete(f"{key_prefix}str:k1", f"{key_prefix}str:k2",
                          f"{key_prefix}str:counter", f"{key_prefix}other:k2")
        except Exception:
            pass

def main():
    with open("cases/string.yaml", "r", encoding="utf-8") as f:
        cases = yaml.safe_load(f)

    report = []
    for form, cfg in FORMS.items():
        client = build_client(cfg)
        for case in cases:
            if form not in case.get("forms", []):
                report.append({"form": form, "case_id": case["case_id"], "result": "N/A", "detail": "form not supported"})
                continue
            status, detail = run_case(client, case, form)
            report.append({"form": form, "case_id": case["case_id"], "result": status, "detail": detail})
        client.close()

    with open("report.json", "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)

    total = len(report)
    passed = sum(1 for r in report if r["result"] == "PASS")
    failed = sum(1 for r in report if r["result"] == "FAIL")
    print(f"total={total} pass={passed} fail={failed}")

if __name__ == "__main__":
    main()

7.3 元数据层比对脚本

python 复制代码
import json
import redis
from redis.cluster import RedisCluster

def fetch_command_meta(client):
    count = client.execute_command("COMMAND", "COUNT")
    names = client.execute_command("COMMAND", "LIST")
    meta = {}
    for name in names:
        info = client.execute_command("COMMAND", "INFO", name)
        if info:
            item = info[0]
            meta[name] = {
                "arity": item[1],
                "flags": sorted(item[2]),
                "first_key": item[3],
                "last_key": item[4],
                "step": item[5],
            }
    return count, meta

def diff_meta(baseline, target):
    missing = sorted(set(baseline) - set(target))
    extra = sorted(set(target) - set(baseline))
    changed = []
    for name in sorted(set(baseline) & set(target)):
        if baseline[name] != target[name]:
            changed.append({"command": name, "baseline": baseline[name], "target": target[name]})
    return {"missing": missing, "extra": extra, "changed": changed}

if __name__ == "__main__":
    native = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
    dcs = RedisCluster(host="10.0.0.14", port=6379, password="***", decode_responses=True)

    _, base_meta = fetch_command_meta(native)
    _, dcs_meta = fetch_command_meta(dcs)

    result = diff_meta(base_meta, dcs_meta)
    print(json.dumps(result, ensure_ascii=False, indent=2))

7.4 报告输出示例

json 复制代码
[
  {"form": "cluster", "case_id": "STR-MGET-002", "result": "PASS", "detail": ""},
  {"form": "proxy_cluster", "case_id": "STR-MGET-002", "result": "FAIL", "detail": "unexpected error: ERR command not supported"},
  {"form": "single", "case_id": "STR-SET-001", "result": "PASS", "detail": ""}
]

8. 结论与后续建议

  • 优先落地元数据层比对(见 3.2 节分层策略与 7.3 节脚本),投入小、见效快,可在半天内发现命令缺失与参数定义不一致。
  • 用例设计以官方命令规范为基线(见 3.1 节),断言坚持"类型优先 + 错误前缀匹配"(见 5.3 节),保证跨形态可复用。
  • 集群与 Proxy 形态是兼容性风险集中区,跨槽、事务、Lua、阻塞命令必须单独设计用例(见 4.1 节常见坑点)。
  • 建议将兼容性矩阵纳入 CI(见 2.3 节自动填充与门禁细则),形成版本发布前的自动门禁,长期看可显著降低回归成本。
  • 后续可扩展方向:命令级性能基线对比、大 key 场景下的命令行为、客户端 SDK 兼容性矩阵(Jedis/lettuce/go-redis/redis-py)。
相关推荐
xiaoqiMikko4 小时前
Redis 实战(三):持久化出错 —— fork 失败、磁盘写满、AOF 膨胀与损坏,逐个复现(6.2.4)
redis·后端
IT规划师4 小时前
面试中被嘲笑Token放在redis里,该如何应对?
redis·面试
我不会起名字3226 小时前
Redis 从 0 到 1(十九):JWT 还是 Redis Session?登录态存储的 Go 实现
redis·go·jwt·session·登录态
我不会起名字3226 小时前
Redis 从 0 到 1(十六):1 亿 UV 只占 12KB?计数去重三件套的 Go 实测
redis·go·uv·布隆过滤器·hyperloglog
码剑客8 小时前
Gin+GORM+Redis 项目开发常见性能坑汇总(Go后端避坑指南)
redis·后端·golang·gin
snow@li9 小时前
redis:RedisDesktopManager(RDM/RESP.app)Redis 数据可视化全景深度梳理
数据库·redis·缓存
海棠Flower未眠9 小时前
下单请求处理流程:商品维度分布式锁与 Redis 预扣库存
redis·分布式·junit
我不会起名字3229 小时前
Redis 从 0 到 1(二十一):P99 涨到 80ms,慢查询日志 5 步定位法 + Go 采集器
redis·go·慢查询·排查·slowlog