前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang 项目启动时维护数据库变更

Golang 项目启动时维护数据库变更

作者头像
Yuyy
发布2023-02-01 17:05:32
9200
发布2023-02-01 17:05:32
举报

前言

数据库变更管理是软件发布必不可少的环节,理想状态是只需发布一个镜像,就能更新应用和数据库。我们项目使用gorm来操作数据库,gorm是具有数据库迁移功能的,但是没有SQL脚本直观。另外我们的应用是同库多服务的微服务,还有些服务存在多个实例的情况,这就需要考虑数据竞争问题了。经过调研,最终选择了Github 10k star 的golang-migrate

使用

准备SQL脚本

将初始化脚本、升级脚本放在项目里的init/postgres/sql目录下。

代码语言:javascript
复制
init
└── postgres
    ├── init.go
    └── sql
        ├── 20230113084913_init.down.sql
        ├── 20230113084913_init.up.sql
        ├── 20230114084930_1.1.0.down.sql
        └── 20230114084930_1.1.0.up.sql

脚本命名

  • 前面部分是一个整数,体现version的大小关系,这里用时间表示,你也可以用001002
  • 后面部分是描述信息,仅仅是给程序员看。

一次数据库变更包含一个升级脚本和一个回退脚本,考虑到我们没有数据库变更回退的需求,down.sql内容为空。

MySQL和Oracle不支持DDL回滚,但PG是可以的。

整个脚本用事务包裹,保证原子性。

使用if not exits,支持重复执行。

20230113084913_init.up.sql

代码语言:javascript
复制
BEGIN;
CREATE TABLE IF NOT EXISTS users(
 xxx
);
CREATE TABLE IF NOT EXISTS users_1(
 xxx
);
COMMIT;

写代码

代码语言:javascript
复制
package postgres

import (
    "context"
    "embed"
    "strings"
    "time"

    "github.com/golang-migrate/migrate/v4"
    _ "github.com/golang-migrate/migrate/v4/database/postgres"
    "github.com/golang-migrate/migrate/v4/source/iofs"

    "github.com/pkg/errors"
)

var (
    // `go embed` 仅能嵌入当前目录及其子目录,无法嵌入上层目录。同时也不支持软链接。
    //go:embed sql/*.sql
    fs embed.FS
    // 由于 go:embed 可以配置多个目录,这里还需要指定下
    initSqlPath = "sql"
)

// InitDb 用于项目启动时初始化数据库,使用参考 xxx
func InitDb(databaseUrl string, timeout time.Duration) (err error) {
    sourceInstance, err := iofs.New(fs, initSqlPath)
    if err != nil {
        return errors.Wrapf(err, "could not open initSqlPath: %s", initSqlPath)
    }

    url := dsn2Url(databaseUrl)
    m, err := migrate.NewWithSourceInstance("iofs", sourceInstance, url)
    if err != nil {
        return errors.Wrap(err, "could not init db migrate")
    }

    // 超时控制
    timeoutCtx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(timeout))
    defer cancelFunc()

    for {
        done := make(chan struct{})
        go func() {
            err = m.Up()
            done <- struct{}{}
        }()

        select {
        case <-timeoutCtx.Done():
            return errors.New("init db timeout")
        case <-done:
            if err != nil {
                if err == migrate.ErrNoChange {
                    _, _ = m.Close()
                    return nil
                }
                if err == migrate.ErrLocked {
                    time.Sleep(1 * time.Second)
                    continue
                }
                return errors.Wrap(err, "init db failed")
            }
            _, _ = m.Close()
            return nil
        }
    }
}

func dsn2Url(databaseUrl string) string {
    arr := strings.Split(databaseUrl, " ")
    params := make(map[string]string, len(arr))
    for _, kv := range arr {
        pair := strings.Split(kv, "=")
        if len(pair) == 2 {
            params[pair[0]] = pair[1]
        }
    }

    url := "postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode=disable&TimeZone=Asia/Shanghai"
    for k, v := range params {
        url = strings.Replace(url, "{"+k+"}", v, -1)
    }
    return url
}
  • databaseUrl:gorm.io/driver/postgres里的dsn"host=xxxx port=xxx user=postgres password=xxx dbname=xx sslmode=disable TimeZone=Asia/Shanghai"
  • 数据竞争问题:最开始我通过数据库唯一索引去实现一个分布式锁,写完后调试代码时发现,golang-migrate具备锁功能,它是通过pg的咨询锁实现的数据库级别的锁。如果获取锁失败会返回migrate.ErrLocked错误,于是我就通过它加了个轮询。

参考

  1. Golang migrate 做数据库变更管理

Post Views: 5

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-1-17 1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 使用
    • 准备SQL脚本
      • 写代码
      • 参考
      相关产品与服务
      数据库
      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档