Redis 数据结构与契约基础:String、Hash、List、Set、Sorted Set 的数据契约定义

在 Redis 中,每一种数据结构都对应着一组特定的操作语义与存储约束。契约校验的核心任务之一,就是为这些数据结构定义明确的“输入-输出-状态”规则。本章将逐一剖析五种基础数据结构的契约定义要点,包括类型约束、键命名规范、值边界条件以及常见反模式。

5.1 数据结构契约总览

下表对比了五种数据结构在契约定义中的关键差异,帮助你在设计校验规则时快速定位约束维度。

数据结构 值类型 最大元素数 有序性 唯一性 典型契约关注点
String 字节串 / 整数 / 浮点数 512 MB — — 长度、编码、数值范围
Hash field-value 对 2³² - 1 个 field — field 唯一 field 数量、字段名规范、值类型
List 有序字符串序列 2³² - 1 个元素 插入顺序 不强制 长度、阻塞超时、索引范围
Set 无序字符串集合 2³² - 1 个成员 — 成员唯一 成员去重、交并差操作
Sorted Set member-score 对 2³² - 1 个成员 score 排序 member 唯一 score 精度、排名、范围查询
契约设计提示: 在定义数据契约时,不仅要约束“值”本身,还要约束“键”的命名模式。例如,Hash 的 field 名应遵循小驼峰或下划线风格,List 的键名应体现队列语义(如 queue:order:new)。

5.2 String 契约:最基础也最容易忽略边界

String 是 Redis 中最简单的数据结构,但契约校验的陷阱往往出现在“简单”之中。以下是一个典型的 String 契约定义示例:

// 契约:用户会话 Token
// 键模式:session:{userId}
// 值约束:
//   - 最大长度:2048 字节
//   - 编码:UTF-8
//   - 过期时间:3600 秒
//   - 不允许空字符串

function validateSessionToken(key, value) {
  if (typeof value !== 'string' || value.length === 0) {
    return { valid: false, reason: 'Token 不能为空' };
  }
  if (Buffer.byteLength(value, 'utf8') > 2048) {
    return { valid: false, reason: 'Token 超过 2048 字节' };
  }
  return { valid: true };
}
常见反模式: 将 JSON 序列化后直接存入 String,却未在契约中声明 JSON 结构。这会导致下游消费者无法预判字段存在性,建议改用 Hash 或明确约定 JSON Schema。

5.3 Hash 契约:字段级约束与动态扩展

Hash 适合存储对象,但契约需要覆盖“字段白名单”和“字段值类型”。以下是一个用户画像的 Hash 契约:

// 契约:用户画像
// 键模式:profile:{userId}
// 字段白名单:["nickname", "avatar", "level", "vip_expire"]
// 字段约束:
//   - nickname: string, 1-32 字符
//   - avatar: string, 空或 URL 格式
//   - level: integer, 1-100
//   - vip_expire: string, ISO 8601 日期或空

function validateProfileHash(key, fields) {
  const allowed = new Set(['nickname', 'avatar', 'level', 'vip_expire']);
  for (const [field, value] of Object.entries(fields)) {
    if (!allowed.has(field)) {
      return { valid: false, reason: `非法字段: ${field}` };
    }
    // 类型与范围校验...
  }
  return { valid: true };
}
契约要点: Hash 的 field 数量应设置上限(如 1000 个),防止恶意客户端通过大量 field 造成内存膨胀。同时,建议在契约中声明“未知字段是否允许通过”,默认应拒绝。

5.4 List 契约:队列语义与阻塞边界

List 常用于消息队列或时间线场景。契约需要明确“左进右出”还是“右进左出”,以及阻塞操作的超时范围。

// 契约:订单处理队列
// 键模式:queue:order:{status}
// 操作约束:
//   - 只允许 LPUSH + BRPOP(左进右出)
//   - 单元素最大 10 KB
//   - 队列最大长度 100,000
//   - BRPOP 超时范围:1-30 秒

function validateListOperation(key, operation, element) {
  if (operation === 'LPUSH' && Buffer.byteLength(element) > 10240) {
    return { valid: false, reason: '元素超过 10 KB' };
  }
  // 长度校验需通过 LLEN 或预估
  return { valid: true };
}
性能契约: 对于长 List,避免使用 LINDEX 随机访问中间元素(O(N))。契约应推荐使用 LRANGE 配合游标或改用 Sorted Set。

5.5 Set 契约:唯一性与集合操作安全

Set 的核心契约是成员唯一性,但跨键的集合操作(SUNION、SINTER、SDIFF)需要额外的结果集大小约束。

// 契约:用户标签集合
// 键模式:tags:{userId}
// 成员约束:
//   - 每个标签 1-50 字符
//   - 最多 500 个标签
//   - 禁止包含空格或特殊字符

function validateSetMember(member) {
  if (member.length < 1 || member.length > 50) {
    return { valid: false, reason: '标签长度不合法' };
  }
  if (/[\s,;]/.test(member)) {
    return { valid: false, reason: '标签包含非法字符' };
  }
  return { valid: true };
}
集合操作风险: SINTER 或 SUNION 的结果集可能极大(百万级),契约应限制目标键的容量,或要求使用 SINTERSTORE 写入临时键并设置过期时间。

5.6 Sorted Set 契约:Score 精度与排名一致性

Sorted Set 的契约校验最复杂,因为 score 的精度直接影响排序结果。Redis 使用双精度浮点数(64-bit),但业务中常需要整数或定点数。

// 契约:商品热榜
// 键模式:hot:products:{category}
// 成员约束:
//   - member: 商品 ID,纯数字字符串
//   - score: 整数,范围 0 - 10^9
//   - 不允许 score 为 NaN 或 Infinity
//   - 最多 1000 个成员

function validateSortedSetMember(member, score) {
  if (!/^\d+$/.test(member)) {
    return { valid: false, reason: '商品 ID 必须为数字' };
  }
  if (!Number.isFinite(score) || score < 0 || score > 1e9) {
    return { valid: false, reason: 'Score 超出合法范围' };
  }
  if (score !== Math.floor(score)) {
    return { valid: false, reason: 'Score 必须为整数' };
  }
  return { valid: true };
}
精度陷阱: 当 score 为小数时,ZRANGEBYSCORE 的区间边界可能因浮点误差导致漏数据。契约应强制要求 score 为整数,或使用 ZRANGE 配合 ZRANK 实现分页。

5.7 数据结构契约的通用校验流程

无论哪种数据结构,契约校验都应遵循以下流程。下图展示了从键名解析到值校验的完整链路。

数据结构契约校验通用流程 1. 键名模式匹配 2. 数据结构类型识别 3. 值边界与类型校验 4. 操作语义约束 5. 返回结果验证 说明 键名正则匹配,提取参数 如 user:{id} → id 必须为整数 拒绝未知键模式 说明 值长度、编码、数值范围 Hash field 白名单 Sorted Set score 精度
流程落地建议: 将校验流程封装为中间件或 Lua 脚本,在 Redis 执行命令前拦截。对于批量操作(如 Pipeline),建议抽样校验而非全量扫描,以平衡性能与安全性。

5.8 契约定义的反模式清单

以下是在实际项目中常见的契约定义错误,应作为校验规则的必检项:

  • 忽略键名长度: 键名过长(超过 1024 字符)会浪费内存,契约应限制最大键名长度。
  • 混合数据结构: 同一键在不同时间被当作 List 和 Set 使用,导致类型冲突。契约应锁定键的数据结构类型。
  • 无过期时间: 对于临时数据(验证码、会话),契约必须强制设置 TTL,防止内存泄漏。
  • Score 使用浮点数: 在 Sorted Set 中使用浮点数作为 score,导致排序不稳定。契约应要求整数或定点数。
  • Hash field 无上限: 允许无限 field 增长,最终导致大 Key 问题。契约应设置 field 数量硬上限。
紧急避坑: 在契约校验中,对于 DEL、EXPIRE 等管理命令,建议只允许白名单内的键模式执行,防止误删核心数据。