跳到主要内容
版本:1.0.0

Schema 演进

Schema 演进允许数据结构随时间变化,同时保持与先前序列化数据的兼容性。Fory Go 通过兼容模式支持这一点。Xlang 模式默认使用兼容的 Schema 演进;native 模式默认使用 schema-consistent 载荷,并显式启用兼容模式。

兼容模式默认值

对于跨语言和默认 Go 载荷,请使用默认运行时:

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

对于需要 Schema 演进的仅 Go native-mode 载荷,请显式启用兼容模式:

f := fory.New(fory.WithXlang(false), fory.WithCompatible(true))

工作方式

Schema-Consistent Native 模式

  • 紧凑序列化,不写入元数据
  • 反序列化期间会检查 struct 哈希
  • 任何 schema 变更都会导致 ErrKindHashMismatch

使用兼容模式

  • 类型元数据会写入序列化数据
  • 支持添加、移除和重排字段
  • 启用向前和向后兼容

为稳定 Struct 禁用演进

如果 struct schema 稳定且不会变化,可以为该 struct 禁用演进,以避免兼容元数据开销。实现 ForyEvolving 接口并返回 false

type StableMessage struct {
ID int64
}

func (StableMessage) ForyEvolving() bool {
return false
}

支持的 Schema 变更

添加字段

可以添加新字段;反序列化旧数据时,这些字段会获得零值:

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

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

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

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

// 使用 V2 反序列化
f2 := fory.New(fory.WithXlang(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.WithXlang(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.WithXlang(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 // int32
}

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

重命名字段

// 不支持(会被视为移除 + 添加)
type V1 struct {
UserName string
}

type V2 struct {
Username string // 名称不同,不是重命名
}

这会被视为移除 UserName 并添加 Username,从而导致数据丢失。

最佳实践

1. 对持久化数据使用兼容模式

// 默认 xlang 载荷已经使用兼容模式。
f := fory.New(fory.WithXlang(true))

对于存储在数据库、文件或缓存中的仅 Go native-mode 数据,请启用兼容模式:

f := fory.New(fory.WithXlang(false), 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
}

Xlang Schema 演进

Schema 演进可跨语言工作:

Go(生产者)

type MessageV1 struct {
ID int64
Content string
}

f := fory.New(fory.WithXlang(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).build();
fory.register(Message.class, 1);
Message msg = fory.deserialize(data, Message.class);
// msg.author 将为 null

性能注意事项

兼容模式主要影响序列化尺寸:

方面Schema Consistent兼容模式
序列化尺寸更小更大(包含元数据,尤其是没有字段 ID 时)
速度类似(元数据只是 memcpy)
Schema 灵活性完整

说明:在兼容模式中使用字段 ID(fory:"id=N")可以减少元数据尺寸。

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

  • 持久化存储
  • 跨服务通信
  • 长期缓存

以下场景使用 native schema-consistent 模式:

  • 内存内操作
  • 同版本通信
  • 最小序列化尺寸

错误处理

哈希不匹配(Native Schema-Consistent 模式)

f := fory.New(fory.WithXlang(false)) // 兼容模式禁用

// 未启用兼容模式时 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.WithXlang(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.WithXlang(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 (zero value)\n", productV2.Description)
fmt.Printf("InStock: %v (zero value)\n", productV2.InStock)
}

相关主题