前言
数据库变更管理是软件发布必不可少的环节,理想状态是只需发布一个镜像,就能更新应用和数据库。我们项目使用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的大小关系,这里用时间表示,你也可以用
001
,002
。 - 后面部分是描述信息,仅仅是给程序员看。
一次数据库变更包含一个升级脚本和一个回退脚本,考虑到我们没有数据库变更回退的需求,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
错误,于是我就通过它加了个轮询。
参考
- Golang migrate 做数据库变更管理
Post Views: 5