原来是 Spring Data Redis 的 set() 方法用错了!
问题现象:使用
StringRedisTemplate保存 JSON 字符串到 Redis,结果读取时发现字符串开头多了几十个甚至上千个\x00(空字节),导致解析失败。
🧩 问题背景
在开发一个游戏启动 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。 - 检查
AESUtils.encrypt():返回 Base64 字符串,安全。 - 确认使用的是
StringRedisTemplate(默认 UTF-8 字符串序列化)。
✅ 结论:数据本身没问题。
第二阶段:怀疑 Redis 客户端被污染
- 创建全新的
StringRedisTemplate(redisConnectionFactory)测试; - 打印
RedisConnectionFactory类型:LettuceConnectionFactory,标准未代理; - 检查是否引入了 Redisson、JetCache 等框架(虽引入但未启用)。
✅ 结论:客户端环境干净。
第三阶段:怀疑 Redis 服务器或协议层问题
- 用
xxd查看原始字节,确认\x00真实存在; - 怀疑 APM 工具、字节码增强、连接池等问题……
但所有线索都指向一个矛盾:明明写入的是合法字符串,为何 Redis 里多了前导空字节?
💡 灵光一闪:重载方法的陷阱!
突然意识到:StringRedisTemplate.opsForValue().set() 有多个重载方法!
查阅源码发现两个关键签名:
// 方法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。
❤️ 结语
这个看似“诡异”的问题,其实源于一个极其隐蔽的 API 设计陷阱。Spring Data Redis 为了支持底层 Redis 命令的灵活性,提供了多个重载方法,但参数语义差异巨大,稍不注意就会掉坑。
希望这篇记录能帮到同样踩坑的你!
记住:set(key, value, 3600) ≠ 设置 3600 秒过期,而是从第 3600 字节开始写!
正确姿势永远是:
set(key, value, 3600, TimeUnit.SECONDS)✅