跳到主要内容
版本:0.16

Schema 演进

Schema 演进允许数据结构变化后仍然能与历史序列化数据兼容。Fory Go 通过兼容模式提供这一能力。

启用兼容模式

创建 Fory 实例时开启兼容模式:

f := fory.New(fory.WithCompatible(true))

工作机制

不启用兼容模式(默认)

  • 使用更紧凑的序列化格式,不写入额外元信息
  • 反序列化时会校验结构体 hash
  • 任何 Schema 变化都可能触发 ErrKindHashMismatch

启用兼容模式

  • 会将类型元信息写入序列化结果
  • 支持新增、删除、重排字段
  • 同时支持前向和后向兼容

支持的 Schema 变更

新增字段

可以新增字段。旧数据反序列化到新结构体时,新字段会得到零值:

// 版本 1
type UserV1 struct {
ID int64
Name string
}

// 版本 2(新增 Email)
type UserV2 struct {
ID int64
Name string
Email string // 新字段
}

f := fory.New(fory.WithCompatible(true))
f.RegisterStruct(UserV1{}, 1)

// 用 V1 序列化
userV1 := &UserV1{ID: 1, Name: "Alice"}
data, _ := f.Serialize(userV1)

// 用 V2 反序列化
f2 := fory.New(fory.WithCompatible(true))
f2.RegisterStruct(UserV2{}, 1)

var userV2 UserV2
f2.Deserialize(data, &userV2)
// userV2.Email == ""(零值)

删除字段

删除字段后,反序列化会自动跳过旧数据中对应的内容:

// 版本 1
type ConfigV1 struct {
Host string
Port int32
Timeout int64
Debug bool // 之后会被删除
}

// 版本 2(删除 Debug)
type ConfigV2 struct {
Host string
Port int32
Timeout int64
// Debug 字段已删除
}

f := fory.New(fory.WithCompatible(true))
f.RegisterStruct(ConfigV1{}, 1)

// 用 V1 序列化
config := &ConfigV1{Host: "localhost", Port: 8080, Timeout: 30, Debug: true}
data, _ := f.Serialize(config)

// 用 V2 反序列化
f2 := fory.New(fory.WithCompatible(true))
f2.RegisterStruct(ConfigV2{}, 1)

var configV2 ConfigV2
f2.Deserialize(data, &configV2)
// Debug 对应的数据会被跳过

字段重排

字段顺序可以在版本之间变化:

// 版本 1
type PersonV1 struct {
FirstName string
LastName string
Age int32
}

// 版本 2(字段顺序调整)
type PersonV2 struct {
Age int32 // 提前
LastName string
FirstName string // 后移
}

兼容模式会按字段名匹配,因此这类变化可自动处理。

不兼容的变更

即使开启兼容模式,以下变化依然不受支持。

字段类型变化

// 不支持
type V1 struct {
Value int32
}

type V2 struct {
Value string // 从 int32 改为 string,不兼容
}

字段重命名

// 不支持,会被视为“删除旧字段 + 新增新字段”
type V1 struct {
UserName string
}

type V2 struct {
Username string // 名称不同,不会被当作重命名
}

这会被解释为删除 UserName 并新增 Username,因此旧数据无法自动迁移到新字段。

最佳实践

1. 持久化数据默认开启兼容模式

// 用于数据库、文件、缓存等持久化场景
f := fory.New(fory.WithCompatible(true))

2. 为新增字段提供默认值

type ConfigV2 struct {
Host string
Port int32
Timeout int64
Retries int32 // 新字段
}

func NewConfigV2() *ConfigV2 {
return &ConfigV2{
Retries: 3, // 默认值
}
}

// 反序列化后补齐默认值
if config.Retries == 0 {
config.Retries = 3
}

跨语言 Schema 演进

Schema 演进同样适用于跨语言场景。

Go(生产者)

type MessageV1 struct {
ID int64
Content string
}

f := fory.New(fory.WithCompatible(true))
f.RegisterStruct(MessageV1{}, 1)
data, _ := f.Serialize(&MessageV1{ID: 1, Content: "Hello"})

Java(使用更新 Schema 的消费者)

public class Message {
long id;
String content;
String author; // Java 侧新增字段
}

Fory fory = Fory.builder()
.withXlang(true)
.withCompatibleMode(true)
.build();
fory.register(Message.class, 1);
Message msg = fory.deserialize(data, Message.class);
// msg.author 会是 null

性能考量

兼容模式主要影响序列化后的体积:

方面Schema 一致模式兼容模式
序列化大小更小更大(会携带元信息,未使用字段 ID 时尤其明显)
速度很快接近一致(元信息处理基本是 memcpy)
Schema 灵活性完整支持

注意:使用字段 ID(fory:"id=N")可以显著降低兼容模式下的元信息开销。

建议在以下场景使用兼容模式:

  • 持久化存储
  • 服务间通信
  • 生命周期较长的缓存

以下场景可以继续使用 Schema 一致模式:

  • 纯内存内操作
  • 同版本之间的通信
  • 追求最小序列化体积

错误处理

Hash 不匹配(Schema 一致模式)

f := fory.New() // 兼容模式关闭

// Schema 已变化,但仍用一致模式读取旧数据
err := f.Deserialize(oldData, &newStruct)
// 错误:ErrKindHashMismatch

未知字段

在兼容模式下,未知字段会被自动跳过。如果你想检测它们:

// 当前 Fory 会自动跳过未知字段
// 暂无显式 API 用于枚举这些字段

完整示例

package main

import (
"fmt"
"github.com/apache/fory/go/fory"
)

// V1:初始 Schema
type ProductV1 struct {
ID int64
Name string
Price float64
}

// V2:新增字段
type ProductV2 struct {
ID int64
Name string
Price float64
Description string // 新增
InStock bool // 新增
}

func main() {
// 用 V1 序列化
f1 := fory.New(fory.WithCompatible(true))
f1.RegisterStruct(ProductV1{}, 1)

product := &ProductV1{ID: 1, Name: "Widget", Price: 9.99}
data, _ := f1.Serialize(product)
fmt.Printf("V1 serialized: %d bytes\n", len(data))

// 用 V2 反序列化
f2 := fory.New(fory.WithCompatible(true))
f2.RegisterStruct(ProductV2{}, 1)

var productV2 ProductV2
if err := f2.Deserialize(data, &productV2); err != nil {
panic(err)
}

fmt.Printf("ID: %d\n", productV2.ID)
fmt.Printf("Name: %s\n", productV2.Name)
fmt.Printf("Price: %.2f\n", productV2.Price)
fmt.Printf("Description: %q(零值)\n", productV2.Description)
fmt.Printf("InStock: %v(零值)\n", productV2.InStock)
}

相关主题