返回首页

使用 Go 和 YDB Serverless 替代 Spring Boot

开发者将应用从 Spring Boot 和 PostgreSQL 迁移到 Yandex Cloud 中的 Go 和 YDB Serverless。描述了手动 SQL、事务、DI 和免费层 RU 优化的挑战。

Go + YDB:适合 Yandex Cloud 免费层
Advertisement 728x90

从Spring Boot迁移到Go与YDB无服务器:打字训练器的转型之路

拥有Java和Spring Boot开发经验的开发者,将TypeStep打字训练工具迁移至Yandex云无服务器平台。放弃付费虚拟机和类似ECS的服务后,最终选择用Go构建后端,并以YDB替代PostgreSQL。结果:容器冷启动仅需毫秒级响应,每月100万请求单元(RU)免费额度内稳定运行。

Go编译为静态二进制文件,无需JVM或JIT预热。容器可瞬间启动——这对无服务器容器至关重要,因为每个请求都可能触发新实例。而使用GraalVM的Spring Boot需要额外配置,不符合预算限制。

架构去冗余化设计

前端采用Next.js静态生成,部署于对象存储;后端使用Go在无服务器容器中运行Docker镜像;数据库则选用YDB无服务器版本。请求直接由浏览器发起:静态资源从存储获取,API调用直达容器。无需API网关,显著降低延迟与成本。

Google AdInline article slot
// YDB列字段示例枚举

type StageType string

const (
    Lowercase   StageType = "lowercase"
    Uppercase   StageType = "uppercase"
    Punctuation StageType = "punctuation"
    Numbers     StageType = "numbers"
)

func (s *StageType) Scan(value interface{}) error {
    return ScanStringEnum(s, value, "StageType")
}

func (s *StageType) Value() (driver.Value, error) {
    return string(*s), nil
}

从JPA迁移后,手动设计表结构并编写YQL查询感觉像是倒退。每个字段都需要显式声明、类型参数和手动扫描。

仓库与YQL查询实现

仓库层使用YQL配合事务处理。创建记录的示例如下:

func (r *ItemRepository) CreateItem(ctx context.Context, item Item) (*Item, error) {
    var result *Item
    err := r.txManager.DoTx(ctx, func(ctx context.Context, tx table.TransactionActor) error {
        res, err := tx.Execute(ctx, `
            DECLARE $id AS Utf8;
            DECLARE $name AS Utf8;
            DECLARE $status AS Utf8;
            DECLARE $created_at AS Datetime;

            INSERT INTO items (id, name, status, created_at)
            VALUES ($id, $name, $status, $created_at)
            RETURNING id, name, status, created_at;
        `,
            table.NewQueryParameters(
                table.ValueParam("$id", types.UTF8Value(item.Id)),
                table.ValueParam("$name", types.UTF8Value(item.Name)),
                table.ValueParam("$status", types.UTF8Value(string(item.Status))),
                table.ValueParam("$created_at", types.DatetimeValueFromTime(item.CreatedAt)),
            ),
        )
        if err != nil {
            return fmt.Errorf("failed to execute CreateItem: %w", err)
        }
        defer res.Close()

        // ... 将结果扫描到 &result
        return res.Err()
    })
    return result, err
}

新增字段需同步更新所有操作:INSERT、UPDATE、SELECT。相比之下,JPA能自动映射实体变更。

Google AdInline article slot
  • 手动SQL优势:完全掌控查询逻辑,避免N+1问题。
  • 劣势:代码冗长,需手动处理类型映射与枚举。
  • 对比JPA:JPA适合基础表结构,但隐藏了底层执行细节。

分布式YDB中的事务管理

YDB不支持外键,但提供事务机制。未使用@Transactional注解时,必须手动管理事务。

TransactionManager通过上下文判断是否存在已有事务:

func (t *TransactionManager) DoTx(ctx context.Context, fn func(ctx context.Context, tx table.TransactionActor) error) error {
    if tx, ok := TxFromContext(ctx); ok {
        return fn(ctx, *tx)
    }
    return t.ydb.NativeDriver.Table().DoTx(ctx, func(ctx context.Context, tx table.TransactionActor) error {
        ctx = context.WithValue(ctx, txKey{}, &tx)
        return fn(ctx, tx)
    })
}

该模式模拟Spring的事务传播行为:要么开启新事务,要么复用现有事务。有效避免重复代码(如CreateCreateTx分离)。

Google AdInline article slot

服务与仓库注入该管理器,单个事务内执行多个操作也自然流畅。

手动依赖注入与构建流程

脱离Spring IoC后,依赖项在main.go中按顺序组装:数据库 → 仓库 → 服务 → 处理器。无反射、代理或运行时循环。

优势:

  • 依赖关系清晰可见。
  • 无隐式逻辑,减少意外错误。
  • 易于重构。

缺点:每个组件需手动编写样板代码。

基于上下文的错误处理

Go的错误是值类型,不自带堆栈信息。解决方案:使用唯一前缀包装fmt.Errorf

func (s *ItemService) ProcessItem(ctx context.Context, id string) error {
    item, err := s.repo.FindById(ctx, id)
    if err != nil {
        return fmt.Errorf("ProcessItem: failed to find item %s: %w", id, err)
    }
    // ...
}

日志输出示例:ProcessItem: failed to find item abc123: ItemRepository.FindById: query failed: connection refused。链路精准定位失败点。

优化YDB请求单元消耗

初始方案模仿PostgreSQL:字母统计分多行存储,每键插入/更新均循环执行。导致RU迅速耗尽。

优化措施:

  • 客户端聚合数据后再批量插入。
  • 使用批处理操作。
  • 重新评估索引与分区策略。

优化后,持续保持在免费额度范围内。

核心经验总结

  • Go在无服务器容器中实现冷启动低于1秒,远优于Spring Boot的数秒级响应。
  • YDB要求手动编写YQL及DECLARE参数,简化模型可显著节省RU。
  • TransactionManager实现类似@Transactional功能,避免代码重复。
  • 手动依赖注入虽不如Spring便捷,但逻辑更透明,需在main.go中保持纪律性。
  • 错误包装添加前缀,便于日志追踪与排查。

— Editorial Team

Advertisement 728x90

继续阅读