踩坑记录:Redis 保存后,字符串前出现大量 `\x00`?

44 阅读4分钟

原来是 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。


🛠 如何避免踩坑?

  1. IDE 中调用时注意方法提示,看清参数含义;
  2. 设置过期时间务必带上 TimeUnit
  3. 团队代码规范中禁止使用三参数 set
  4. 遇到 Redis 字符串异常前缀,优先检查是否误用 offset

❤️ 结语

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

希望这篇记录能帮到同样踩坑的你!
记住:set(key, value, 3600) ≠ 设置 3600 秒过期,而是从第 3600 字节开始写!

正确姿势永远是:set(key, value, 3600, TimeUnit.SECONDS)