找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1917

积分

0

好友

254

主题
发表于 2025-12-25 06:04:50 | 查看: 30| 回复: 0

问题现象:在使用 StringRedisTemplate 将 JSON 字符串保存到 Redis 后,读取时发现字符串开头多出了几十个甚至上千个 \x00(空字节),导致 JSON 解析失败。

🧩 问题背景

在开发一个游戏启动 Token 功能时,需要将用户账户信息序列化为 JSON,并通过 StringRedisTemplate 存入 Redis 并设置过期时间。示例代码如下:

stringRedisTemplate.opsForValue().set(token, JsonUtils.objectToJson(account), LAUNCH_TOKEN_EXPIRE_SECONDS);

其中 LAUNCH_TOKEN_EXPIRE_SECONDS = 3600(即 1 小时)。

但在 redis-cli 中查看该 key 时,内容却显示为:

127.0.0.1:6379> get launchd63fa468ecf74cb2982d49c0e4cd5fbe
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00{\"id\":2003415281603235842,...}"

字符串开头赫然出现了 30 个(实际应为 3600 个)\x00 字符,这直接导致前端或下游服务解析 JSON 失败。

🔍 排查过程

第一阶段:检查数据序列化

  • 检查 JsonUtils.objectToJson() 的输出,确认生成的 JSON 字符串干净,无 \x00 字符。
  • 确认使用的是 StringRedisTemplate,其默认采用 UTF-8 字符串序列化。

✅ 结论:数据本身没有问题

第二阶段:检查 Redis 客户端环境

  • 创建全新的 StringRedisTemplate 实例进行测试。
  • 检查 RedisConnectionFactory 类型,确认为 LettuceConnectionFactory,标准且未代理。
  • 排查项目是否引入了 Redisson、JetCache 等第三方框架(虽已引入但未启用)。

✅ 结论:客户端环境干净,无污染

第三阶段:怀疑 Redis 服务器或协议问题

  • 使用 xxd 工具查看键值的原始字节,确认 \x00 真实存在于存储中。
  • 一度怀疑是 APM 工具、字节码增强或连接池导致的问题。

但所有线索都指向一个矛盾点:写入的是合法字符串,为何 Redis 中会多出前导空字节?

💡 关键发现:重载方法的陷阱

突然意识到 StringRedisTemplate.opsForValue().set() 存在多个重载方法。查阅 Spring Data Redis 源码,发现两个关键的方法签名:

// 方法1:带偏移量(offset)
void set(K key, V value, long offset);

// 方法2:带过期时间(timeout + unit)
void set(K key, V value, long timeout, TimeUnit unit);

而实际编写的代码是:

stringRedisTemplate.opsForValue().set(token, accountJson, LAUNCH_TOKEN_EXPIRE_SECONDS);
//                                          ↑
//                               这里传入的是 3600(本意是过期秒数)

这实际上调用了第一个重载方法——将 3600 误解为了 offset(写入偏移量)!

🧪 行为验证:SETRANGE 命令的原理

Redis 的 SETRANGE key offset value 命令行为如下:

  • 如果 key 不存在,会自动创建。
  • 使用 \x00 填充 [0, offset) 区间。
  • 然后从 offset 位置开始写入 value

例如,执行:

SETRANGE mykey 5 "hello"

结果将是:

"\x00\x00\x00\x00\x00hello"

这完美解释了观察到的现象:3600 个 \x00 填充后,再写入 JSON 字符串

✅ 正确写法

要设置键的过期时间,必须使用四参数版本的方法,明确指定时间单位:

stringRedisTemplate.opsForValue()
    .set(token, accountJson, LAUNCH_TOKEN_EXPIRE_SECONDS, TimeUnit.SECONDS);

这样底层会执行类似 SETEX key 3600 value 的操作,不会添加任何填充字节

📌 经验总结

用法 方法签名 实际效果 是否推荐
set(k, v) set(K, V) 普通 SET
set(k, v, timeout, unit) set(K, V, long, TimeUnit) SET + EXPIRE 用于缓存场景
set(k, v, offset) set(K, V, long) SETRANGE(偏移写入) 极易误用

⚠️ 重要警告set(key, value, long) 这个三参数方法不是用于设置过期时间,而是设置写入偏移量。除非你明确需要 SETRANGE 功能(实际应用极少),否则绝对不要用它来设置 TTL。

🛠 如何避免类似问题

  1. IDE 提示:调用方法时仔细查看参数提示,确认参数含义。
  2. 强制时间单位:设置过期时间务必传入 TimeUnit 参数。
  3. 代码规范:在团队规范中禁止使用三参数的 set 方法。
  4. 问题排查:遇到 Redis 字符串出现异常前缀时,优先检查是否误用了 offset 参数。

❤️ 结语

这个看似诡异的问题,根源在于 Spring Data Redis API 设计中一个隐蔽的陷阱。为了支持底层 Redis 命令的灵活性,它提供了多个重载方法,但参数语义差异巨大,稍不注意就会掉坑。

希望本文的排查思路和解决方案能帮助你避免类似问题。记住:set(key, value, 3600) ≠ 设置 3600 秒过期,而是从第 3600 字节开始写入! 正确的做法永远是使用带 TimeUnit 的四参数版本。




上一篇:PPO算法原理解析:从动机到工程实现,助力RLHF大模型对齐
下一篇:Java后端SQL复用实战:基于MyBatis与工具类的三种高效方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 11:55 , Processed in 0.307350 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表