必须双写+渐进迁移+校验,否则字段丢失或类型错乱;String→Hash不能一次脚本转换,因JSON嵌套、null语义、数字精度等无法直接映射,且应用仍读旧String key导致不一致。

不能直接“升级”,必须双写+渐进迁移+校验,否则字段丢失或类型错乱是必然的。
为什么 String → Hash 不能靠一次脚本转换
String 存的是完整 JSON 字符串,比如 {"price":99.9,"stock":100,"status":"on_sale"};而 Hash 要求每个 field 是独立字符串,且 field 名必须显式指定。如果用 HSET user:1001 "price" "99.9" "stock" "100" 这种方式硬转,会丢失原始 JSON 中的嵌套结构、null 值语义、数字类型精度(比如 99.9 可能被 toString() 成 "99.90000000000001"),更严重的是:Java 应用里若仍走 StringRedisTemplate.opsForValue().get("user:1001"),它读到的还是旧 JSON,根本不会感知 Hash 已存在。
常见错误现象:
- 迁移脚本把 JSON 解析后用
HSET写入,但没删原 String key → 应用读写不一致 - 用
JSON.parse()后直接遍历 Object.keys() 写 Hash,遇到null字段变成字符串"null",反序列化失败 - 字段名含特殊字符(如
sku.name)未做转义,导致 Hash field 不合法或查询不到
双写阶段:让新老逻辑共存
在业务代码中加一层适配层,所有对 user:1001 的读写都走统一入口,根据开关决定是否启用 Hash。
关键操作:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 写操作:先执行
StringRedisTemplate.opsForValue().set("user:1001", json),再解析 JSON 并调用StringRedisTemplate.opsForHash().putAll("user:1001", map)—— 注意 map 的 value 必须全为String类型,Integer/Boolean 等需显式.toString() - 读操作:优先尝试
opsForHash().entries("user:1001"),若返回空(说明 Hash 还没写入),则 fallback 到opsForValue().get("user:1001")并自动补全 Hash - 开关配置用
@Value("${redis.hash.enabled:false}")控制,上线后通过配置中心动态打开
HIMPORT 批量导入适合初始化,但不适用于运行时迁移
Redis 8.10 的 HIMPORT 命令确实快,但它要求输入是紧凑二进制或特定文本格式(如 TSV),且不支持条件判断、字段过滤或异常重试。线上迁移时,你无法保证每条 JSON 都合法,也无法处理部分失败后回滚。
适用场景仅限:
- 离线数据迁移(如从 MySQL 导出后一次性灌入)
- 灰度环境预热缓存,且 schema 完全固定、无 null / NaN / 循环引用
- 配合
redis-cli --pipe使用,但要注意 network buffer 和 timeout 设置
别在生产流量路径里调 HIMPORT —— 它不是原子命令,失败时可能只写入部分字段,导致 Hash 数据残缺。
校验和兜底最容易被忽略
迁移完成后,不能只看 key 数量是否一致。要写一个校验脚本,逐条比对:
- String key 是否还存在(应全部消失)
- Hash 的 field 数量是否等于 JSON 的 top-level key 数量(排除
__metadata等内部字段) - 对每个 field,用
HGET user:1001 price和 JSON 解析后的json.price做字符串等值比较(注意浮点数用Math.abs(a-b) ) - 特别检查
null字段:Hash 里不应存在该 field,而不是存了字符串"null"
最危险的是“看似成功”的静默错误:比如某个服务用了 FastJson 的 SerializeConfig.disableCircularReferenceDetect(true),导出 JSON 时把循环引用转成 "$ref":"$",迁移到 Hash 后这个字段就彻底丢弃了,但日志里没有任何报错。

















