缓存中的serialVersionUID序列化问题
Hi,我是阿昌,今天记录一个 Java 序列化容易被忽略的问题:serialVersionUID 不一致导致InvalidClassException。
这个问题通常出现在项目版本升级的过程中,尤其是缓存或者数据库里存了旧版本的序列化数据,代码一升级就开始报错,排查起来还挺绕的。
一、问题现象
线上某个接口突然开始报 java.io.InvalidClassException,异常堆栈大概长这样:
1 | java.io.InvalidClassException: com.example.User; |
翻译成人话就是:序列化的时候 UID 是 1,反序列化的时候 UID 变成了 2,两边对不上,JVM 就不干了。

二、问题分析
这个问题的根因很清楚:同一个类的 serialVersionUID 发生了变化。
Java 的序列化机制在反序列化时,会拿当前这个类的 serialVersionUID 跟序列化数据流里记录的 UID 做对比。如果两者不一致,JVM 就认为这是两个”不同版本”的类,直接抛出 InvalidClassException,拒绝反序列化。
那什么情况下 UID 会变呢?分两种情况:
1、没有显式声明 serialVersionUID
如果类实现了 Serializable 但没有显式声明 serialVersionUID,JVM 会在编译期根据类的结构自动计算一个 UID。
计算时会考虑这些因素:
- 类名
- 实现的接口
- 所有 public 和 protected 成员变量
- 所有 public 和 protected 方法
- 构造器
所以,只要你随便改点东西——比如加个字段、删个方法、甚至换一个编译环境——这个自动生成的 UID 就可能发生变化。
2、显式声明了 serialVersionUID 但被改动了
如果之前声明了 serialVersionUID = 1L,后面有人不小心改成了 2L,那也会导致同样的问题。
这在项目迭代中很容易发生,尤其是在多人协作、代码合并的时候。
回到业务场景:
缓存里存的对象是用旧版本(UID=1)序列化写入的,代码升级后,类结构调整导致 UID 变成了 2。这时候再去读缓存——反序列化——UID 对不上,直接报错。
三、代码案例
下面用两个简单的 Demo 展示问题是怎么产生的。
场景:序列化写入 → 代码改动 → 反序列化读取
Step 1:定义一个没有显式声明 UID 的类并写文件
1 | import java.io.*; |
运行后,user.obj 文件中记录的是 JVM 自动生成的 UID。
Step 2:改动类结构后尝试反序列化
1 | import java.io.*; |
执行第二步时就会抛出:
1 | java.io.InvalidClassException: com.example.User; |
因为加了一个 email 字段,类结构变了,JVM 重新计算的 UID 跟之前不同,反序列化失败。
正确做法:显式声明 serialVersionUID
1 | import java.io.*; |
加了 serialVersionUID = 1L 之后,只要这个值不变,即使后续加字段、删方法,JVM 也不会抛 InvalidClassException。
注意:删字段或者改字段类型还是要小心,即使不报错,旧数据里对应字段的值也会是默认值(null / 0)。
四、解决办法
如果线上已经出现这个问题,处理步骤就三步:
1、统一 UID
在类的定义中显式加上 serialVersionUID,值设置为跟当前线上最新的序列化数据保持一致。
如果旧数据的 UID 是 JVM 自动生成的,可以通过 serialver 命令查出来:
1 | serialver com.example.User |
然后用这个值去声明:
1 | private static final long serialVersionUID = -2134351137722478609L; |
2、清理旧缓存
把 Redis 缓存、数据库里存有旧序列化数据的记录清掉(或者做平滑迁移),避免旧版本的脏数据继续作怪。
3、以最新版本重新写入
缓存 / 数据清掉之后,后续的读写都会以当前最新的 UID 版本来序列化和反序列化,就不会再报错了。
五、总结
这个问题归根结底就一句话:
实现 Serializable 接口的类,一定要显式声明
serialVersionUID,并且不要随意改动它。
几点注意事项:
- 声明了就别改:
serialVersionUID = 1L从上线第一天就定好,后续迭代不要动这个值。 - 不要依赖 JVM 自动生成:自动生成的值受类结构影响,稍微改点东西就可能变,完全不可控。
- 字段增删要谨慎:加字段一般没问题,旧数据那个字段会是默认值;但删字段或改字段类型,即使不报异常逻辑也可能出错。
- 版本升级时注意数据兼容:如果缓存或数据库里存了序列化二进制数据,升级前要想好旧数据的兼容方案——要么保证 UID 不变,要么做数据迁移。
这个坑不大,但掉进去的时候排查起来挺费时间的,所以从一开始就养成好习惯,所有实现了 Serializable 的类都把 UID 写死就对了。