go-micro v2運開實踐-框架篇(4)部署使用者資料庫,封裝gorm

huangyanming發表於2022-04-29

微服務資料庫拆分原則

資料庫拆分是微服務中的一個關鍵點,在進行拆分時需要遵循一些原則。

  • 每個微服務都擁有屬於自己的資料庫,且只允許當前服務呼叫
  • 微服務中,依賴資料(如主表依賴從表,使用者與使用者訂單這種關係)應該透過服務進行呼叫。
  • 共享資料(如國家,地區),可能需要被許多微服務進行訪問,將其拆分後雖然起到了解耦的作用,如果透過服務來進行訪問對效能會有損耗。這種情況下就需要斟酌處理了,其中一種方式是直接對資料異構解耦。比如一個地區表,使用者服務需要直接對其join進行訪問,訂單服務也需要對其join進行訪問。這時候我們在兩個服務的資料庫中都建立一個地區表,再透過binlog或者mq的方式讓這兩個表的資料進行同步。推薦一下chanl,阿里開源的一種binlog同步方案,支援多種語言客戶端。

docker-compose安裝使用者資料庫

修改.env

...

#資料庫版本
MYSQL_VERSION=latest
#使用者資料庫使用者名稱
USER_DB_USER="micro_user"
#使用者資料庫密碼
USER_DB_PASSWORD="micro_user"
#使用者資料庫初始db
USER_DB_DATABASE="micro_user"
#使用者資料庫root密碼
USER_DB_ROOT_PASSWORD="root"
#使用者資料庫對映埠
USER_DB_PORT=33061
#使用者資料庫最大連結數
USER_DB_MAX_CONNECTIONS=200
#使用者資料庫最大空閒連結數
USER_DB_MAX_IDE_CONNECTIONS=50
#使用者資料庫空閒連結最大存活時間,分
USER_DB_CONNECTIONS_MAX_LIFE_TIME=5

...

建立持久化掛載目錄

mkdir -p data/user-db

修改docker-compose.yaml

...

  micro-user-db:
    image: mysql:${MYSQL_VERSION}
    ports:
      - ${USER_DB_PORT}:3306
    volumes:
      - ./data/user-db:/var/lib/mysql
    restart: always
    environment:
      TZ: ${TZ}
      MYSQL_USER: ${USER_DB_USER} # 設定使用者名稱
      MYSQL_PASSWORD: ${USER_DB_PASSWORD} # 設定使用者民嗎
      MYSQL_DATABASE: ${USER_DB_DATABASE} # 初始資料庫
      MYSQL_ROOT_PASSWORD: ${USER_DB_ROOT_PASSWORD} # root使用者密碼
    networks:
      - micro-network

 ...

啟動資料庫

docker-compose up -d micro-user-db

檢視容器是否正常執行

使用.env中配置的賬號密碼埠測試資料庫連結

封裝gorm

在web系統中,我們大部分時間都需要程式與資料庫互動,實際開發中我們其實很多程式碼都是基於業務的CURD,使用資料庫關係對映能大大提高我們的開發效率與安全性,學習到這個階段的同學相信對gorm應該不會很陌生。gorm在新版本中為我們提供了讀寫分離,分表中介軟體,連線池,效能監控等高階特性,而這些特效能免去我們要安裝許多侵入性的元件。

封裝通用程式碼

在多個微服務中,每個微服務都需要我們去初始化連線池,獲取資料庫連結等操作。而這些功能都是單一可複用的,因此我們需要封裝一些通用程式碼給多個微服務功能,不做複製貼上的程式設計師,是進步的基本要求。

初始化通用程式碼專案go mod

mkdir common
cd common
go mod init github.com/869413421/micro-service/common

封裝通用資料結構轉換方法

mkdir -p pkg/types
touch pkg/types/converter.go
package types

import (
    "reflect"
    "strconv"
)

// Int64ToString INT64轉字串
func Int64ToString(num int64) string {
    return strconv.FormatInt(num, 10)
}

// UInt64ToString UINT64轉字串
func UInt64ToString(num uint64) string {
    return strconv.FormatUint(num, 10)
}

// StringToInt 字串轉INT
func StringToInt(str string) (int, error) {
    num, err := strconv.Atoi(str)
    if err != nil {
        return 0, err
    }
    return num, nil
}

// Fill 透過反射將物件2的值填充給物件1
func Fill(obj1 interface{}, obj2 interface{}) {
    //1.透過反射獲取兩個結構的欄位
    v1 := reflect.ValueOf(obj1).Elem()
    v2 := reflect.ValueOf(obj2).Elem()

    //2.迴圈填充
    for i := 0; i < v1.NumField(); i++ {
        //2.1獲取結構1欄位詳細資訊
        fieldInfo1 := v1.Type().Field(i)
        field1Name := fieldInfo1.Name
        field1Type := fieldInfo1.Type

        //2.2 迴圈結構2的欄位
        for i2 := 0; i2 < v2.NumField(); i2++ {
            //2.2.1獲取解構2的詳細資訊
            fieldInfo2 := v2.Type().Field(i2)
            field2Name := fieldInfo2.Name
            field2Type := fieldInfo2.Type

            //2.2.2如果兩個結構的欄位名相等,而且值型別相等且有值,將結構2的值賦給結構1,
            if field1Name == field2Name && field1Type == field2Type {

                //2.2.2.1 判斷是否有值
                //TODO 需增加更多值型別的判斷
                if v2.FieldByName(fieldInfo2.Name).IsValid() {
                    switch v2.FieldByName(fieldInfo2.Name).Type().String() {
                    case "int":
                        if v2.FieldByName(fieldInfo2.Name).Int() == 0 {
                            continue
                        }
                    case "string":
                        if v2.FieldByName(fieldInfo2.Name).String() == "" {
                            continue
                        }
                    }
                }

                //2.2.2.1 設定值
                newValue := v2.FieldByName(field2Name)
                if newValue.IsValid(){
                    v1.FieldByName(field1Name).Set(newValue)
                }
            }
        }
    }
}

封裝 config結構

mkdir -p pkg/config
touch pkg/config/config.go
package config

import (
    "github.com/869413421/micro-service/common/pkg/types"
    "os"
    "sync"
    "time"
)

var once sync.Once
var config *Configuration

type Configuration struct {
    Db *Db `json:"db"`
}

type Db struct {
    Address               string        `json:"address"`
    Database              string        `json:"database"`
    User                  string        `json:"user"`
    Password              string        `json:"password"`
    Charset               string        `json:"charset"`
    MaxConnections        int           `json:"max_connections"`
    MaxIdeConnections     int           `json:"max_ide_connections"`
    ConnectionMaxLifeTime time.Duration `json:"connection_max_life_time"`
}

// LoadConfig 載入配置檔案
func LoadConfig() *Configuration {
    //1.適用sync.one,使配置只載入一次,後續不需要讀取直接返回
    once.Do(func() {
        //1.1從環境變數中讀取配置資訊
        host := os.Getenv("DB_HOST")
        user := os.Getenv("DB_USER")
        database := os.Getenv("DB_DATABASE")
        password := os.Getenv("DB_PASSWORD")
        dbMaxConnections, _ := types.StringToInt(os.Getenv("DB_MAX_CONNECTIONS"))
        dbMaxIdeConnections, _ := types.StringToInt(os.Getenv("DB_MAX_IDE_CONNECTIONS"))
        dbConnectionMaxLifeTime, _ := types.StringToInt(os.Getenv("DB_CONNECTIONS_MAX_LIFE_TIME"))

        //1.2初始化配置結構體
        dbConfig := &Db{
            Address:               host,
            Database:              database,
            User:                  user,
            Password:              password,
            Charset:               "utf8",
            MaxConnections:        dbMaxConnections,
            MaxIdeConnections:     dbMaxIdeConnections,
            ConnectionMaxLifeTime: time.Duration(dbConnectionMaxLifeTime) * time.Minute,
        }

        config = &Configuration{Db: dbConfig}
    })

    return config
}

封裝方法,能使配置能夠被規範化管理。上述程式碼中我們暫時透過簡單地從系統環境變數中讀取配置資訊,使用sync.Once確保只會被初始化一次,後續呼叫中能減少我們對配置檔案的載入,不再初始化直接返回配置資訊。這裡我們只是封裝了資料庫配置,但在我們系統中依然會有很多元件的配置資訊需要讀取,以及配置更改後如何熱更新。這些我們在後續講到配置中心的時候再深入瞭解

獲取gorm

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

封裝gorm,初始化化連結池

建立db目錄

mkdir -p pkg/db
touch pkg/db/db.go

封裝初始化連線池程式碼

package db

import (
    "fmt"
    "github.com/869413421/micro-service/common/pkg/config"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "strconv"
    "time"
)

type BaseModel struct {
    ID        uint64    "gorm:column:id;primaryKey;autoIncrement;not null"
    CreatedAt time.Time `gorm:"column:created_at;index"`
    UpdatedAt time.Time `gorm:"column:updated_at;index"`
}

//GetStringID 主鍵轉字串
func (model BaseModel) GetStringID() string {
    return strconv.Itoa(int(model.ID))
}

// CreatedAtDate 獲取模型建立時間
func (model BaseModel) CreatedAtDate() string {
    return model.CreatedAt.Format("2006-01-02 15:04:05")
}

// UpdatedAtDate 獲取模型更新時間
func (model BaseModel) UpdatedAtDate() string {
    return model.UpdatedAt.Format("2006-01-02 15:04:05")
}

var gormDb *gorm.DB
var dbConfig *config.Db

// connectDB 連結資料庫
func connectDB() (*gorm.DB, error) {
    // 1.獲取配置
    serviceConfig := config.LoadConfig()
    dbConfig = serviceConfig.Db

    //2.連結資料庫
    gormDb, err := gorm.Open(mysql.Open(fmt.Sprintf(
        "%s:%s@(%s)/%s?charset=%s&parseTime=True&loc=Local",
        dbConfig.User, dbConfig.Password, dbConfig.Address, dbConfig.Database, dbConfig.Charset,
    )), &gorm.Config{})

    if err != nil {
        return nil, err
    }

    //3.返回資料庫連結
    return gormDb, nil
}

func setupDB() {
    //1.獲取連結
    conn, err := connectDB()
    if err != nil {
        panic(err)
    }
    conn.Set("gorm:table_options", "ENGINE=InnoDB")
    conn.Set("gorm:table_options", "Charset=utf8")
    sqlDB, err := conn.DB()
    if err != nil {
        panic(fmt.Sprintf("connection to db error %v", err))
    }

    //2.設定最大連線數
    sqlDB.SetMaxOpenConns(dbConfig.MaxConnections)

    //3.設定最大空閒連線數
    sqlDB.SetMaxIdleConns(dbConfig.MaxIdeConnections)

    //4. 設定每個連結的過期時間
    sqlDB.SetConnMaxLifetime(dbConfig.ConnectionMaxLifeTime * time.Minute)

    //5.設定好連線池,重新賦值
    gormDb = conn
}

// GetDB 開放給外部獲得db連線
func GetDB() *gorm.DB {
    //1.如果db為空,初始化連結池
    if gormDb == nil {
        setupDB()
    }

    //2.返回db物件給外部使用
    return gormDb
}

提交程式碼到github,供其他服務使用

記得在專案下新增.gitignore

git add .
git commit -m "資料庫連線池封裝"
git push

使用者服務連結資料庫

開啟使用者服務專案,引用通用程式碼包

go get -u github.com/869413421/micro-service/common

在我們測試如果我們修改了common的程式碼,需要我們將程式碼推送到github,然後引用包的專案更新才能看到效果,這樣在開發階段效率低下,可以修改go.mod 將common包替換成我們本地的路徑,然後編譯到可執行檔案中,將可執行檔案掛載在容器裡,方法跟我們上一節中一樣。但是切記,正式上線前需要講掛載和替換去掉。

module github.com/869413421/micro-service/user

go 1.13

// This can be removed once etcd becomes go gettable, version 3.4 and 3.5 is not,
// see https://github.com/etcd-io/etcd/issues/11154 and https://github.com/etcd-io/etcd/issues/11931.
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

# 替換成本地common包,方便開發階段除錯
replace github.com/869413421/micro-service/common => ../common

require (
    github.com/869413421/micro-service/common v0.0.0-20220428152058-528eea77a565 // indirect
    github.com/golang/protobuf v1.5.2
    github.com/micro/go-micro/v2 v2.9.1
    google.golang.org/protobuf v1.28.0
)

將資料庫配置設定為環境變數

修改docker-compose.yaml

...

  micro-user-service:
    depends_on: # 啟動依賴,需要等etcd叢集啟動後才啟動當前容器
      - etcd1
      - etcd2
      - etcd3
      - micro-user-db
    build: ./user # dockerfile所在目錄
    environment:
      TZ: ${TZ}
      MICRO_SERVER_ADDRESS: ":9091" # 服務埠
      MICRO_REGISTRY: "etcd" # 註冊中心型別
      MICRO_REGISTRY_ADDRESS: "etcd1:2379,etcd2:2379,etcd3:2379" # 註冊中心叢集地址
      DB_HOST: "micro-user-db:3306"
      DB_DATABASE: ${USER_DB_DATABASE}
      DB_USER: ${USER_DB_USER}
      DB_PASSWORD: ${USER_DB_PASSWORD}
      DB_MAX_CONNECTIONS: ${USER_DB_MAX_CONNECTIONS}
      DB_MAX_IDE_CONNECTIONS: ${USER_DB_MAX_IDE_CONNECTIONS}
      DB_CONNECTIONS_MAX_LIFE_TIME: ${USER_DB_CONNECTIONS_MAX_LIFE_TIME}
    ports:
      - 9092:9091
    volumes:
      - ./user:/app
    networks:
      - micro-network

...

建立使用者model

mkdir -p pkg/model
touch pkg/model/user.go
package model

import (
    db "github.com/869413421/micro-service/common/pkg/db"
)

// User 使用者模型
type User struct {
    db.BaseModel
    Name     string `gorm:"column:name;type:varchar(255);not null;unique;default:''" valid:"name"`
    Email    string `gorm:"column:email;type:varchar(255) not null;unique;default:''" valid:"email"`
    RealName string `gorm:"column:real_name;type:varchar(255);not null;default:''" valid:"realName"`
    Avatar   string `gorm:"column:avatar;type:varchar(255);not null;default:''" valid:"avatar"`
    Status   int    `gorm:"column:status;type:tinyint(1);not null;default:0" `
    Password string `gorm:"column:password;type:varchar(255) not null;;default:''" valid:"password"`
}

加入模型遷移

修改main.go

package main

import (
    "github.com/869413421/micro-service/common/pkg/db"
    "github.com/869413421/micro-service/user/handler"
    "github.com/869413421/micro-service/user/pkg/model"
    "github.com/869413421/micro-service/user/subscriber"
    "github.com/micro/go-micro/v2"
    log "github.com/micro/go-micro/v2/logger"

    proto "github.com/869413421/micro-service/user/proto/user"
)

func main() {

    //1.準備資料庫連線,並且執行資料庫遷移
    db := db.GetDB()
    db.AutoMigrate(&model.User{})

    // New Service
    service := micro.NewService(
        micro.Name("micro.service.user"),
        micro.Version("v1"),
    )

    // Initialise service
    service.Init()

    // Register Handler
    proto.RegisterUserHandler(service.Server(), new(handler.User))

    // Register Struct as Subscriber
    micro.RegisterSubscriber("micro.service.user", service.Server(), new(subscriber.User))

    // Run service
    if err := service.Run(); err != nil {
        log.Fatal(err)
    }
}

編譯可以執行程式碼

make build

如果沒有安裝make命令,可手動執行

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' -i -o micro-user-service ./main.go

重新執行服務,執行模型遷移

重啟使用者服務容器

docker-compose up -d micro-user-service

檢查遷移是否執行成功

至此我們已經完成了gorm的封裝,以及編寫好使用者服務互動的程式碼

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章