微服務資料庫拆分原則
資料庫拆分是微服務中的一個關鍵點,在進行拆分時需要遵循一些原則。
- 每個微服務都擁有屬於自己的資料庫,且
只允許當前服務呼叫
。 - 微服務中,
依賴資料(如主表依賴從表,使用者與使用者訂單這種關係)
應該透過服務
進行呼叫。 共享資料(如國家,地區)
,可能需要被許多微服務進行訪問,將其拆分後雖然起到了解耦的作用,如果透過服務
來進行訪問對效能會有損耗。這種情況下就需要斟酌處理了,其中一種方式是直接對資料異構解耦。比如一個地區表
,使用者服務需要直接對其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 協議》,轉載必須註明作者和本文連結