🧊serialVersionUID 与 @Serial:加个字段,Redis 缓存全炸了
一句话总结
serialVersionUID是你亲手写死的「类版本号」;不写,JVM 就自己猜,类一改它就翻脸。
一、先看事故现场
有个 User,登录后丢进 Redis 缓存 7 天:
1 | public class User implements Serializable { |
下一个迭代,产品说要加手机号。你顺手加了个字段,发版:
1 | public class User implements Serializable { |
发版后告警炸了:
1 | java.io.InvalidClassException: com.xxx.User; |
线上所有还没过期的缓存,全部读不出来。 你只加了一行字段,什么逻辑都没动。
二、为什么会这样
序列化时,JVM 会把一个版本号(serialVersionUID)跟数据一起写进去。反序列化时拿出来比对,不一致就直接拒绝。
你没声明这个字段,JVM 就根据类的结构自己算一个:
1 | 旧类 User{name, age} → 算出 111111 → 写进了 Redis |
读的时候 111111 != 222222 → 炸。
⚠️ 更坑的是:这个自动计算不只看字段。方法签名、实现的接口、类修饰符都参与计算。所以哪怕你只是加了个 getter,ID 也可能变。
没写
serialVersionUID的类,你几乎不能碰。
三、正确做法
手动写死:
1 | public class User implements Serializable { |
现在加字段后 ID 还是 1L,跟 Redis 里一致 → 正常读出来,phone 自动为 null。平滑过渡,无感发版。
四、别以为它万能
serialVersionUID 只是让你跳过版本号检查这一关,后面还有字段匹配这一关。
✅ 能扛住的改动
| 改动 | 结果 |
|---|---|
| 加字段 | 老数据读出来,新字段为 null / 0 |
| 删字段 | 老数据里多的那个被丢掉 |
| 加/删/改方法 | 完全不影响 |
❌ 扛不住的改动
1 | // 老版本 |
Redis 里存的是数字,代码要的是字符串,对不上,UID 救不了。
同类型的雷:
- 类改名 / 换包路径
- 字段加上或去掉
transient、static - 父类变了
一张表记住
| 改动 | 加了 UID |
|---|---|
| 加 / 删字段、改方法 | ✅ 兼容 |
| 改字段类型、改类名包名 | ❌ 还是炸 |
规则:加字段随便加;要改字段类型或挪包,就别指望兼容老数据 —— 要么换个新字段名,要么先把缓存清掉。
五、@Serial 又是什么
JDK 14 引入的 java.io.Serial,说白了就是给序列化相关成员做拼写检查。
1 |
|
它运行时完全没用(@Retention(SOURCE)),加不加都不影响序列化行为。它只在编译期帮你确认「你这行写对了没」。
为什么需要它
序列化的那些「魔法方法」,Java 是按名字 + 签名反射调用的,不是接口实现,没有 @Override 那样的保护。写错一个字符就静默失效:
1 | // 想自定义反序列化,参数类型写错了 —— 编译通过,运行时这方法永远不会被调用 |
同理 serialVersionUID 写成 int、漏掉 static,加了 @Serial 都会被当场拦住。
能标注的 7 种成员
只能用在实现了 Serializable 的类里,签名必须完全匹配,否则编译报错:
| 成员 | 要求的签名 |
|---|---|
serialVersionUID |
private static final long |
serialPersistentFields |
private static final ObjectStreamField[] |
writeObject |
private void writeObject(ObjectOutputStream) throws IOException |
readObject |
private void readObject(ObjectInputStream) throws IOException, ClassNotFoundException |
readObjectNoData |
private void readObjectNoData() throws ObjectStreamException |
writeReplace |
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException |
readResolve |
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException |
注意:不能用在 record 和枚举上(这些类型的 serialVersionUID 本身就被忽略),也不能标在非 Serializable 类里。
六、实战建议
1️⃣ 必须加 UID 的:会真正跨 JVM / 跨版本传输的类
- Redis 缓存对象
- Dubbo / RPC 传的 DTO、VO
- Session 里的对象
2️⃣ 只是顺手 implements Serializable、从没真序列化过的 —— 加不加无所谓,但建议统一加上,成本为零,能避开发版事故。
3️⃣ 千万别随便改已有的 serialVersionUID —— 改了等于宣布「跟老数据不兼容」,线上 Redis 里的旧数据会全部读不出来。这是自己给自己造事故。
4️⃣ @Serial 按需加 —— 只有 serialVersionUID 的 DTO 加它收益不大(规范统一 + 防手误);真正值得加的是自定义了 writeObject / readObject / readResolve 的类。
口诀
UID 手写死,加删字段不用怕;
类型包名一动手,缓存照样全报废。@Serial不干活,只在编译帮你查。




