问题现象:在使用 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。
🛠 如何避免类似问题
- IDE 提示:调用方法时仔细查看参数提示,确认参数含义。
- 强制时间单位:设置过期时间务必传入
TimeUnit 参数。
- 代码规范:在团队规范中禁止使用三参数的
set 方法。
- 问题排查:遇到 Redis 字符串出现异常前缀时,优先检查是否误用了
offset 参数。
❤️ 结语
这个看似诡异的问题,根源在于 Spring Data Redis API 设计中一个隐蔽的陷阱。为了支持底层 Redis 命令的灵活性,它提供了多个重载方法,但参数语义差异巨大,稍不注意就会掉坑。
希望本文的排查思路和解决方案能帮助你避免类似问题。记住:set(key, value, 3600) ≠ 设置 3600 秒过期,而是从第 3600 字节开始写入! 正确的做法永远是使用带 TimeUnit 的四参数版本。