数据库迁移对于 Golang 服务,为什么重要?

数据库迁移对于 golang 服务,为什么重要?

数据库迁移,为什么重要?

您是否曾经遇到过这样的情况:当您使用更新的数据库架构在生产环境中部署新的更新时,但之后出现错误并需要恢复内容......这就是迁移出现的情况。

数据库迁移有几个关键目的:

  1. 架构演变:随着应用程序的演变,它们的数据模型也会发生变化。迁移允许开发人员系统地更新数据库架构以反映这些更改,确保数据库结构与应用程序代码匹配。
  2. 版本控制:迁移提供了一种对数据库架构进行版本控制的方法,允许团队跟踪一段时间内的更改。此版本控制有助于理解数据库的演变并有助于开发人员之间的协作。
  3. 跨环境的一致性:迁移确保数据库架构在不同环境(开发、测试、生产)中保持一致。这降低了可能导致错误和集成问题的差异风险。
  4. 回滚功能:许多迁移工具都支持回滚更改,允许开发人员在迁移导致问题时恢复到数据库之前的状态。这增强了开发和部署过程中的稳定性。
  5. 自动部署:迁移可以作为部署过程的一部分实现自动化,确保将必要的架构更改应用于数据库,而无需手动干预。这简化了发布流程并减少了人为错误。

在golang项目中应用

要使用 gorm 和 mysql golang 服务创建全面的生产级设置,以便轻松迁移、更新和回滚,您需要包含迁移工具、处理数据库连接池并确保正确的结构定义。这是一个完整的示例来指导您完成整个过程:

项目结构

/golang-service
|-- main.go
|-- database
|   |-- migration.go
|-- models
|   |-- user.go
|-- config
|   |-- config.go
|-- migrations
|   |-- ...
|-- go.mod

1.数据库配置(config/config.go)

package config

import (
    "fmt"
    "log"
    "os"
    "time"

    "github.com/joho/godotenv"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var db *gorm.db

func connectdb() {
    err := godotenv.load()
    if err != nil {
        log.fatal("error loading .env file")
    }

    // charset=utf8mb4: sets the character set to utf8mb4, which supports all unicode characters, including emojis.
    // parsetime=true: tells the driver to automatically parse date and datetime values into go's time.time type.
    // loc=local: uses the local timezone of the server for time-related queries and storage.
    dsn := fmt.sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parsetime=true&loc=local",
        os.getenv("db_user"),
        os.getenv("db_pass"),
        os.getenv("db_host"),
        os.getenv("db_port"),
        os.getenv("db_name"),
    )

    db, err := gorm.open(mysql.open(dsn), &gorm.config{})
    if err != nil {
        panic("failed to connect database")
    }

    sqldb, err := db.db()
    if err != nil {
        panic("failed to configure database connection")
    }

    // set connection pool settings
    sqldb.setmaxidleconns(10)
    sqldb.setmaxopenconns(100)
    sqldb.setconnmaxlifetime(time.hour)

    // 1.sqldb.setmaxidleconns(10)
    // sets the maximum number of idle (unused but open) connections in the connection pool.
    // a value of 10 means up to 10 connections can remain idle, ready to be reused.

    // 2. sqldb.setmaxopenconns(100):
    // sets the maximum number of open (active or idle) connections that can be created to the database.
    // a value of 100 limits the total number of connections, helping to prevent overloading the database.

    // 3. sqldb.setconnmaxlifetime(time.hour):
    // sets the maximum amount of time a connection can be reused before it’s closed.
    // a value of time.hour means that each connection will be kept for up to 1 hour, after which it will be discarded and a new connection will be created if needed.

    db = db
}

2. 数据库迁移(database/migration.go)

package database

import (
    "golang-service/models"
    "golang-service/migrations"
    "gorm.io/gorm"
)

func migrate(db *gorm.db) {
    db.automigrate(&models.user{})
    // apply additional custom migrations if needed
}

3.模型(models/user.go)

package models

import "gorm.io/gorm"

type user struct {
    gorm.model
    name  string `json:"name"`
}

4.环境配置(.env)

db_user=root
db_pass=yourpassword
db_host=127.0.0.1
db_port=3306
db_name=yourdb

5. 主入口点(main.go)

package main

import (
    "golang-service/config"
    "golang-service/database"
    "golang-service/models"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func main() {
    config.connectdb()
    database.migrate(config.db)

    r := gin.default()
    r.post("/users", createuser)
    r.get("/users/:id", getuser)
    r.run(":8080")
}

func createuser(c *gin.context) {
    var user models.user
    if err := c.shouldbindjson(&user); err != nil {
        c.json(400, gin.h{"error": err.error()})
        return
    }

    if err := config.db.create(&user).error; err != nil {
        c.json(500, gin.h{"error": err.error()})
        return
    }

    c.json(201, user)
}

func getuser(c *gin.context) {
    id := c.param("id")
    var user models.user

    if err := config.db.first(&user, id).error; err != nil {
        c.json(404, gin.h{"error": "user not found"})
        return
    }

    c.json(200, user)
}

6、说明:

  • 数据库配置:管理连接池以获得生产级性能。
  • 迁移文件:(在迁移文件夹中)帮助对数据库架构进行版本控制。
  • gorm 模型:将数据库表映射到 go 结构。
  • 数据库迁移:(在数据库文件夹中)随着时间的推移更改表的自定义逻辑,以便轻松回滚。
  • 测试:您可以使用 httptest 和 testify 为此设置创建集成测试。

7. 创建第一个迁移

  1. 对于生产环境,我们可以使用像 golang-migrate 这样的迁移库来应用、回滚或重做迁移。

    安装golang-migrate

    go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
    
  2. 为用户表生成迁移文件

    migrate create -ext=sql -dir=./migrations -seq create_users_table
    

    运行命令后,我们将得到一对 .up.sql (用于更新架构)和 down.sql (用于稍后可能的回滚)。数字000001是自动生成的迁移索引。

    /golang-service
    |-- migrations
    |   |-- 000001_create_users_table.down.sql
    |   |-- 000001_create_users_table.up.sql
    

    添加相关sql命令到.up文件和.down文件。

    000001_create_users_table.up.sql

    create table users (
    id bigint auto_increment primary key,
    name varchar(255) not null,
    created_at datetime,
    updated_at datetime,
    deleted_at datetime);
    

    000001_create_users_table.down.sql

    drop table if exists users;
    

    运行向上迁移并使用以下命令将更改应用到数据库(-verbose 标志以查看更多日志详细信息):

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" -verbose up
    

    如果我们遇到迁移问题,我们可以使用以下命令查看当前的迁移版本及其状态:

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" version
    

    如果由于某些原因导致迁移失败,我们可以考虑使用带有脏迁移版本号的强制(谨慎使用)命令。如果版本是1(可以在migrations或schema_migrations表中检查),我们将运行:

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" force 1
    

8.改变计划

  1. 在某个时间点,我们可能想添加新功能,其中一些可能需要更改数据方案,例如我们想向用户表添加电子邮件字段。我们将按照以下方式进行。

    进行新的迁移,将电子邮件列添加到用户表

    migrate create -ext=sql -dir=./migrations -seq add_email_to_users
    

    现在我们有了一对新的 .up.sql 和 .down.sql

    /golang-service
    |-- migrations
    |   |-- 000001_create_users_table.down.sql
    |   |-- 000001_create_users_table.up.sql
    |   |-- 000002_add_email_to_users.down.sql
    |   |-- 000002_add_email_to_users.up.sql
    
  2. 将以下内容添加到*_add_email_to_users.*.sql文件

    000002_add_email_to_users.up.sql

    alter table `users` add column `email` varchar(255) unique;
    

    000002_add_email_to_users.down.sql

    alter table `users` drop column `email`;
    

    再次运行向上迁移命令以更新数据模式

    migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" -verbose up
    

    我们还需要更新 golang users 结构(添加 email 字段)以使其与新模式保持同步..

    type user struct {
     gorm.model
     name  string `json:"name"`
     email string json:"email" gorm:"uniqueindex"
    }
    

9. 回滚迁移:

如果由于某些原因我们在新更新的模式中出现错误,并且需要回滚,这种情况下我们将使用 down 命令:

migrate -path ./migrations -database "mysql://user:password@tcp(localhost:3306)/dbname" down 1

数字 1 表示我们要回滚 1 个迁移。

这里我们还需要手动更新 golang users 结构(删除 email 字段)以反映数据架构更改。

   type user struct {
    gorm.model
    name  string `json:"name"`
   }

10. 与 makefile 一起使用

为了简化迁移和回滚的过程,我们可以添加一个makefile。

/golang-service
|-- ...
|-- makefile

makefile 内容如下。

include .env

create_migration:
    migrate create -ext=sql -dir=./migrations -seq create_new_migration

migrate_up:
    migrate -path=./migrations -database "mysql://${db_user}:${db_pass}@tcp(${db_host}:${db_port})/${db_name}" -verbose up 1

migrate_down:
    migrate -path=./migrations -database "mysql://${db_user}:${db_pass}@tcp(${db_host}:${db_port})/${db_name}" -verbose down 1

.phony:  create_migration migrate_up migrate_down

现在我们只需在 cli 上运行 make migrate_up 或 make migrate_down 即可进行迁移和回滚。

11.注意事项:

  • 回滚期间数据丢失:回滚删除列或表的迁移可能会导致数据丢失,因此在运行回滚之前始终备份数据。
  • ci/cd 集成:将迁移过程集成到 ci/cd 管道中,以在部署期间自动执行架构更改。
  • 数据库备份:安排定期数据库备份,以防止迁移错误时丢失数据。

关于数据库备份

在回滚迁移或进行可能影响数据库的更改之前,以下是需要考虑的一些关键点。

  1. 架构更改:如果迁移涉及更改架构(例如,添加或删除列、更改数据类型),则回滚到之前的迁移可能会导致存储在这些更改的列或表中的任何数据丢失.
  2. 数据删除:如果迁移包含删除数据的命令(例如删除表或截断表),则回滚将执行相应的“向下”迁移,这可能会永久删除该数据。
  3. 事务处理:如果您的迁移工具支持事务,则回滚可能会更安全,因为更改是在事务中应用的。但是,如果您在事务之外手动运行 sql 命令,则存在丢失数据的风险。
  4. 数据完整性:如果您以取决于当前架构的方式修改了数据,则回滚可能会使您的数据库处于不一致的状态。

所以备份数据至关重要。这是一个简短的指南:

  1. 数据库转储:
    使用特定于数据库的工具创建数据库的完整备份。对于 mysql,您可以使用:

     mysqldump -u root -p dbname > backup_before_rollback.sql
    

    这将创建一个文件 (backup_before_rollback.sql),其中包含 dbname 数据库的所有数据和架构。

  2. 导出特定表:
    如果您只需要备份某些表,请在 mysqldump 命令中指定它们:

    mysqldump -u root -p golang_1 users > users_table_backup.sql
    
  3. 验证备份:
    确保备份文件已创建并检查其大小或打开它以确保它包含必要的数据。

  4. 安全地存储备份:
    将备份副本保存在安全位置,例如云存储或单独的服务器,以防止回滚过程中数据丢失。

云端备份

要在使用 golang 并部署在 aws eks 上时备份 mysql 数据,您可以按照以下步骤操作:

  1. 使用mysqldump进行数据库备份:
    使用 kubernetes cron 作业创建 mysql 数据库 mysqldump。

    mysqldump -h <mysql-host> -u <user> -p<password> <database_name> > backup.sql
    

    将其存储在持久卷或 s3 存储桶中。

  2. 使用 kubernetes cronjob 实现自动化:
    使用 kubernetes cronjob 自动化 mysqldump 过程。
    yaml 配置示例:yaml

    apiVersion: batch/v1
    kind: CronJob
    metadata:
    name: mysql-backup
    spec:
       schedule: "0 2 * * *" # Runs every day at 2 AM
       jobTemplate:
         spec:
           template:
             spec:
               containers:
                 - name: mysql-backup
                   image: mysql:5.7
                   args:
                     - /bin/sh
                     - -c
                     - "mysqldump -h <mysql-host> -u <user> -p<password> <database_name> | aws s3 cp - s3://<your-bucket-name>/backup-$(date +\\%F).sql"
                   env:
                     - name: AWS_ACCESS_KEY_ID
                       value: "<your-access-key>"
                     - name: AWS_SECRET_ACCESS_KEY
                       value: "<your-secret-key>"
               restartPolicy: OnFailure
    


    `

  3. 使用 aws rds 自动备份(如果使用 rds):
    如果您的 mysql 数据库位于 aws rds 上,您可以利用 rds 自动备份和快照。
    设置备份保留期并手动拍摄快照或使用 lambda 函数自动拍摄快照。

  4. 使用 velero 备份持久卷 (pv):
    使用 kubernetes 备份工具 velero 来备份保存 mysql 数据的持久卷。
    在您的 eks 集群上安装 velero 并将其配置为备份到 s3。

通过使用这些方法,您可以确保您的 mysql 数据得到定期备份和安全存储。

以上就是数据库迁移对于 Golang 服务,为什么重要?的详细内容,更多请关注其它相关文章!