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 门禁:
- 自动填充 :7.2 节的执行脚本按「形态 × 用例」遍历,每条用例产出
{form, case_id, result, detail}记录;报告生成阶段以case_id关联用例元数据(命令分类、命令名、优先级),按「命令分类 → 命令 → 形态」透视成上表结构,detail非空时自动写入备注列,无需人工填表。 - 基线比对 :首次全量执行的结果归档为
baseline.json(含环境指纹:实例版本、规格、分片数、代理版本、客户端版本)。后续每次执行与基线做 diff,输出三类变化:新增 FAIL、PASS → PASS_WITH_DIFF、N/A → PASS(能力增强)。 - CI 门禁规则 :将矩阵生成脚本接入流水线,在版本发布、内核补丁、形态扩容前后自动触发。门禁判定建议为------出现新增
FAIL即阻断发布 ;PASS → PASS_WITH_DIFF需人工确认并更新基线后方可放行;N/A → PASS仅记录不阻断。 - 产物归档 :每次流水线运行输出
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 分层测试策略
- 元数据层 :
COMMAND COUNT、COMMAND INFO全量比对,成本极低、收益最高,先跑这一层。 - 单命令功能层:每命令 3~5 个用例(正常、边界、异常参数),校验返回值与错误码。
- 组合/语义层:事务、Lua、Pipeline、多 key 跨槽、过期与淘汰、阻塞命令超时。
- 稳定性层:长时间压测下的命令成功率、主备切换/扩缩容期间的命令可用性。
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)。