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协议错误到业务错误码的映射链路,以及契约测试中的断言节点:
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,便于契约测试失败时追溯完整调用链