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

前言

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

使用

准备SQL脚本

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

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是可以的。

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

  • 使用create table if not exitsinsert into on conflict do nothing,支持重复执行。

  • 20230113084913_init.up.sql

    BEGIN;
    CREATE TABLE IF NOT EXISTS users(
     xxx
    );
    CREATE TABLE IF NOT EXISTS users_1(
     xxx
    );
    COMMIT;

写代码

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错误,于是我就通过它加了个轮询。
  • 不同仓库的程序使用同一个数据库时,各自的SQL脚本放在自己的仓库,同时需要设置不同的db_version表。
    postgres.DefaultMigrationsTable = "schema_migrations_xxx"

    参考

  1. Golang migrate 做数据库变更管理
作者:Yuyy
博客:https://yuyy.info
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇