3. 错误码设计原则:HTTP状态码与业务错误码的区别、Redis常见错误码与自定义规范

在契约测试项目中,错误码是客户端与Redis集群之间、以及服务内部模块之间通信的“通用语言”。设计不合理的错误码会导致断言失效、故障定位困难、数据同步链路中断。本章从三个层次展开:HTTP状态码与业务错误码的职责分离、Redis协议层常见错误码的语义与处理策略、自定义业务错误码的规范与断言模板。

3.1 HTTP状态码 vs 业务错误码:职责分离原则

HTTP状态码描述传输层的交互结果,业务错误码描述应用层的语义结果。两者必须解耦,不能混用。

维度 HTTP状态码 业务错误码
职责 表示请求是否被服务器接收、处理、完成 表示业务逻辑的具体失败原因
粒度 粗粒度(4xx/5xx) 细粒度(可精确到字段级别)
可枚举性 固定标准(RFC 7231) 项目自定义,可扩展
契约断言场景 断言“请求是否到达” 断言“业务结果是否符合预期”
典型误用 用200表示“操作成功”,用500表示“余额不足” 用-1表示所有错误,丢失语义
契约断言硬规则: 契约测试中,HTTP状态码只断言传输层健康状态(如200/503);业务错误码必须从响应体(JSON/Redis Bulk String)中提取并断言。两者不可互相替代。

3.2 Redis常见错误码:语义、触发条件与契约断言策略

Redis协议层错误码以-ERR、-MOVED、-ASK、-CLUSTERDOWN等前缀返回。在集群模式下,这些错误码直接决定数据同步与重定向逻辑。

错误码 语义 触发条件 契约断言策略
MOVED 槽位迁移完成,客户端需直连目标节点 请求的key所在槽位已永久迁移至其他节点 断言响应以-MOVED开头,并提取目标节点地址
ASK 槽位正在迁移,客户端需先发送ASKING命令 请求的key处于迁移中状态(源节点仍有数据) 断言响应以-ASK开头,并验证后续重试逻辑
CLUSTERDOWN 集群不可用(主节点宕机或槽位覆盖不足) 集群无法完成16384个槽位的完整覆盖 断言响应为-CLUSTERDOWN,触发集群健康检查契约
CROSSSLOT 跨槽命令涉及多个不位于同一节点的key MGET/MSET等命令的key散落在不同槽位 断言响应为-CROSSSLOT,提示客户端拆分请求
WRONGTYPE 操作类型与key存储的数据结构不匹配 对String类型key执行List操作 断言响应为-WRONGTYPE,验证数据结构一致性
BUSY Redis正在执行耗时脚本(如Lua) 脚本执行超时或死循环 断言响应为-BUSY,触发脚本终止契约
契约测试最佳实践: 在集群模式下,MOVED和ASK是正常重定向,不应视为错误。契约断言应区分“协议层重定向”与“应用层异常”,避免误报。

3.3 自定义错误码规范:分层、格式与断言模板

业务系统在Redis之上封装缓存服务时,需要定义一套自定义业务错误码,用于在契约测试中精确断言数据同步结果。

3.3.1 错误码分层结构

推荐采用三段式编码:模块(2位) + 场景(2位) + 序号(2位),例如 040201。

  • 模块码:00-系统通用,01-用户模块,02-订单模块,04-缓存同步模块
  • 场景码:01-连接,02-读写,03-迁移,04-一致性
  • 序号码:01-99,具体错误细分
错误码 模块 场景 含义 HTTP状态码映射
040201 缓存同步 连接 Redis连接超时 503
040202 缓存同步 连接 Redis认证失败 401
040301 缓存同步 读写 Key不存在 404
040302 缓存同步 读写 数据类型不匹配 400
040401 缓存同步 迁移 槽位迁移中,数据暂不可用 200(含业务错误码)
040501 缓存同步 一致性 主从数据版本不一致 409

3.3.2 错误码响应格式规范

契约测试要求所有错误响应遵循统一JSON结构,便于断言脚本解析:

{
  "code": "040302",
  "message": "数据类型不匹配:期望String,实际Hash",
  "detail": {
    "key": "user:1001",
    "expected_type": "string",
    "actual_type": "hash",
    "redis_error": "WRONGTYPE Operation against a key holding the wrong kind of value"
  },
  "timestamp": 1712567890123,
  "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
契约断言模板(Pact/Jest风格):
expect(responseBody.code).toBe("040302");
expect(responseBody.detail.redis_error).toMatch(/WRONGTYPE/);

3.4 错误码设计SVG:分层映射与断言流程

下图展示从Redis协议错误到业务错误码的映射链路,以及契约测试中的断言节点:

错误码分层映射与契约断言流程 错误码分层映射与契约断言流程 Redis协议错误 -MOVED 槽位迁移 -ASK 迁移中 -CLUSTERDOWN -WRONGTYPE / -CROSSSLOT 错误码映射层 协议错误 → 业务错误码 MOVED → 040401 (迁移中) WRONGTYPE → 040302 (类型不匹配) CLUSTERDOWN → 049901 (集群不可用) 业务错误码 040302 类型不匹配 040401 迁移中 040501 版本不一致 040202 认证失败 契约断言层:验证错误码映射是否正确 断言1:协议错误捕获 响应以 -MOVED / -ASK 开头 断言2:业务码提取 JSON body.code === 040302 断言3:detail验证 detail.redis_error 匹配 WRONGTYPE 说明:契约测试不直接断言Redis原始错误文本,而是断言经过映射层转换后的业务错误码与detail字段。 这保证了客户端与缓存服务之间的契约不因Redis版本升级而断裂。

3.5 自定义错误码断言实战:契约测试代码片段

以下示例展示在Pact-JVM中如何对Redis错误码进行契约断言:

@Pact(consumer = "OrderService", provider = "CacheProxy")
public RequestResponsePact createPact(PactDslWithProvider builder) {
    return builder
        .given("缓存中存在Hash类型key: order:1001")
        .uponReceiving("对Hash类型key执行String GET操作")
            .path("/cache/get")
            .method("POST")
            .body("{\"key\": \"order:1001\"}")
        .willRespondWith()
            .status(200)
            .headers(Map.of("Content-Type", "application/json"))
            .body(new PactDslJsonBody()
                .stringType("code", "040302")
                .stringType("message", "数据类型不匹配")
                .object("detail")
                    .stringType("key", "order:1001")
                    .stringType("expected_type", "string")
                    .stringType("actual_type", "hash")
                    .stringType("redis_error", "WRONGTYPE")
                .closeObject()
                .integerType("timestamp")
                .stringType("trace_id")
            )
        .toPact();
}

@Test
@PactTestFor(pactMethod = "createPact")
public void testWrongTypeErrorCode(MockServer mockServer) {
    // 发送请求并断言业务错误码
    HttpResponse response = HttpClient.post(mockServer.getUrl() + "/cache/get", 
        "{\"key\": \"order:1001\"}");
    
    JsonNode body = objectMapper.readTree(response.getContent());
    
    // 契约断言:业务错误码精确匹配
    assertThat(body.get("code").asText()).isEqualTo("040302");
    
    // 契约断言:detail中包含Redis原始错误
    assertThat(body.get("detail").get("redis_error").asText())
        .contains("WRONGTYPE");
    
    // 契约断言:HTTP状态码为200(传输层正常)
    assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
}
常见陷阱: 不要直接在契约中断言Redis原始错误文本(如-WRONGTYPE Operation against a key holding the wrong kind of value)的完整字符串。Redis版本升级可能改变错误描述措辞,导致契约断裂。应始终断言业务错误码和detail中的结构化字段。

3.6 错误码设计检查清单

  • 职责分离:HTTP状态码只表示传输层结果,业务错误码表示应用层语义
  • 可枚举性:所有业务错误码必须在契约文档中显式列出,不允许运行时动态生成
  • 可断言性:每个错误码对应唯一的JSON结构,契约测试可精确匹配
  • 版本兼容:错误码一旦发布,语义不可变更;新增错误码只能追加,不能修改已有码值
  • Redis协议映射:每个Redis协议错误(MOVED/ASK/CLUSTERDOWN/WRONGTYPE等)必须映射到唯一的业务错误码
  • trace_id贯穿:所有错误响应必须携带trace_id,便于契约测试失败时追溯完整调用链