微服务数据库拆分原则

数据库拆分是微服务中的一个关键点,在进行拆分时需要遵循一些原则。

  • 每个微服务都拥有属于自己的数据库,且只允许当前服务调用
  • 微服务中,依赖数据(如主表依赖从表,用户与用户订单这种关系)应该通过服务进行调用。
  • 共享数据(如国家,地区),可能需要被许多微服务进行访问,将其拆分后虽然起到了解耦的作用,如果通过服务来进行访问对性能会有损耗。这种情况下就需要斟酌处理了,其中一种方式是直接对数据异构解耦。比如一个地区表,用户服务需要直接对其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

  1. <a name="ECIJs"></a>
  2. #### 创建持久化挂载目录

mkdir -p data/user-db

  1. <a name="AecVW"></a>
  2. #### 修改docker-compose.yaml

micro-user-db: image: mysql:${MYSQL_VERSION} ports:

  1. - ${USER_DB_PORT}:3306
  2. volumes:
  3. - ./data/user-db:/var/lib/mysql
  4. restart: always
  5. environment:
  6. TZ: ${TZ}
  7. MYSQL_USER: ${USER_DB_USER} # 设置用户名
  8. MYSQL_PASSWORD: ${USER_DB_PASSWORD} # 设置用户民吗
  9. MYSQL_DATABASE: ${USER_DB_DATABASE} # 初始数据库
  10. MYSQL_ROOT_PASSWORD: ${USER_DB_ROOT_PASSWORD} # root用户密码
  11. networks:
  12. - micro-network

  1. <a name="G7ewD"></a>
  2. #### 启动数据库
  3. `docker-compose up -d micro-user-db`<br />查看容器是否正常运行<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26186945/1651071062308-bc5d835a-95a7-4a49-a107-f08a3246afcb.png#clientId=u289b69b0-a033-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=655&id=uf704b3e0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=720&originWidth=1270&originalType=binary&ratio=1&rotation=0&showTitle=false&size=100156&status=done&style=none&taskId=uf1ce5588-8c12-4238-a91f-759fba7c1e0&title=&width=1154.5454295213563)<br />使用.env中配置的账号密码端口测试数据库链接<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26186945/1651071208073-e81de8b0-9529-4383-afaa-09414a4fd94a.png#clientId=u289b69b0-a033-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=545&id=u0f4d4b42&margin=%5Bobject%20Object%5D&name=image.png&originHeight=600&originWidth=900&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53949&status=done&style=none&taskId=u48f0f106-95da-4f7a-9198-5c61cf40a2b&title=&width=818.1818004482052)
  4. <a name="rOSfk"></a>
  5. ## 封装gorm
  6. 在web系统中,我们大部分时间都需要程序与数据库交互,实际开发中我们其实很多代码都是基于业务的CURD,使用数据库关系映射能大大提高我们的开发效率与安全性,学习到这个阶段的同学相信对[gorm](https://gorm.io/zh_CN)应该不会很陌生。gorm在新版本中为我们提供了读写分离,分表中间件,连接池,性能监控等高级特性,而这些特性能免去我们要安装许多侵入性的组件。
  7. <a name="MdqEG"></a>
  8. ### 封装通用代码
  9. 在多个微服务中,每个微服务都需要我们去初始化连接池,获取数据库链接等操作。而这些功能都是单一可复用的,因此我们需要封装一些通用代码给多个微服务功能,不做复制粘贴的程序员,是进步的基本要求。
  10. <a name="KAU41"></a>
  11. #### 初始化通用代码项目go mod

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

  1. <a name="IjiDV"></a>
  2. #### 封装通用数据结构转换方法

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

  1. ```
  2. package types
  3. import (
  4. "reflect"
  5. "strconv"
  6. )
  7. // Int64ToString INT64转字符串
  8. func Int64ToString(num int64) string {
  9. return strconv.FormatInt(num, 10)
  10. }
  11. // UInt64ToString UINT64转字符串
  12. func UInt64ToString(num uint64) string {
  13. return strconv.FormatUint(num, 10)
  14. }
  15. // StringToInt 字符串转INT
  16. func StringToInt(str string) (int, error) {
  17. num, err := strconv.Atoi(str)
  18. if err != nil {
  19. return 0, err
  20. }
  21. return num, nil
  22. }
  23. // Fill 通过反射将对象2的值填充给对象1
  24. func Fill(obj1 interface{}, obj2 interface{}) {
  25. //1.通过反射获取两个结构的字段
  26. v1 := reflect.ValueOf(obj1).Elem()
  27. v2 := reflect.ValueOf(obj2).Elem()
  28. //2.循环填充
  29. for i := 0; i < v1.NumField(); i++ {
  30. //2.1获取结构1字段详细信息
  31. fieldInfo1 := v1.Type().Field(i)
  32. field1Name := fieldInfo1.Name
  33. field1Type := fieldInfo1.Type
  34. //2.2 循环结构2的字段
  35. for i2 := 0; i2 < v2.NumField(); i2++ {
  36. //2.2.1获取解构2的详细信息
  37. fieldInfo2 := v2.Type().Field(i2)
  38. field2Name := fieldInfo2.Name
  39. field2Type := fieldInfo2.Type
  40. //2.2.2如果两个结构的字段名相等,而且值类型相等且有值,将结构2的值赋给结构1,
  41. if field1Name == field2Name && field1Type == field2Type {
  42. //2.2.2.1 判断是否有值
  43. //TODO 需增加更多值类型的判断
  44. if v2.FieldByName(fieldInfo2.Name).IsValid() {
  45. switch v2.FieldByName(fieldInfo2.Name).Type().String() {
  46. case "int":
  47. if v2.FieldByName(fieldInfo2.Name).Int() == 0 {
  48. continue
  49. }
  50. case "string":
  51. if v2.FieldByName(fieldInfo2.Name).String() == "" {
  52. continue
  53. }
  54. }
  55. }
  56. //2.2.2.1 设置值
  57. newValue := v2.FieldByName(field2Name)
  58. if newValue.IsValid(){
  59. v1.FieldByName(field1Name).Set(newValue)
  60. }
  61. }
  62. }
  63. }
  64. }

封装 config结构

  1. mkdir -p pkg/config
  2. touch pkg/config/config.go
  1. package config
  2. import (
  3. "github.com/869413421/micro-service/common/pkg/types"
  4. "os"
  5. "sync"
  6. "time"
  7. )
  8. var once sync.Once
  9. var config *Configuration
  10. type Configuration struct {
  11. Db *Db `json:"db"`
  12. }
  13. type Db struct {
  14. Address string `json:"address"`
  15. Database string `json:"database"`
  16. User string `json:"user"`
  17. Password string `json:"password"`
  18. Charset string `json:"charset"`
  19. MaxConnections int `json:"max_connections"`
  20. MaxIdeConnections int `json:"max_ide_connections"`
  21. ConnectionMaxLifeTime time.Duration `json:"connection_max_life_time"`
  22. }
  23. // LoadConfig 加载配置文件
  24. func LoadConfig() *Configuration {
  25. //1.适用sync.one,使配置只加载一次,后续不需要读取直接返回
  26. once.Do(func() {
  27. //1.1从环境变量中读取配置信息
  28. host := os.Getenv("DB_HOST")
  29. user := os.Getenv("DB_USER")
  30. database := os.Getenv("DB_DATABASE")
  31. password := os.Getenv("DB_PASSWORD")
  32. dbMaxConnections, _ := types.StringToInt(os.Getenv("DB_MAX_CONNECTIONS"))
  33. dbMaxIdeConnections, _ := types.StringToInt(os.Getenv("DB_MAX_IDE_CONNECTIONS"))
  34. dbConnectionMaxLifeTime, _ := types.StringToInt(os.Getenv("DB_CONNECTIONS_MAX_LIFE_TIME"))
  35. //1.2初始化配置结构体
  36. dbConfig := &Db{
  37. Address: host,
  38. Database: database,
  39. User: user,
  40. Password: password,
  41. Charset: "utf8",
  42. MaxConnections: dbMaxConnections,
  43. MaxIdeConnections: dbMaxIdeConnections,
  44. ConnectionMaxLifeTime: time.Duration(dbConnectionMaxLifeTime) * time.Minute,
  45. }
  46. config = &Configuration{Db: dbConfig}
  47. })
  48. return config
  49. }

封装方法,能使配置能够被规范化管理。上述代码中我们暂时通过简单地从系统环境变量中读取配置信息,使用sync.Once确保只会被初始化一次,后续调用中能减少我们对配置文件的加载,不再初始化直接返回配置信息。这里我们只是封装了数据库配置,但在我们系统中依然会有很多组件的配置信息需要读取,以及配置更改后如何热更新。这些我们在后续讲到配置中心的时候再深入了解

获取gorm

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

封装gorm,初始化化链接池

创建db目录

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

封装初始化连接池代码

  1. package db
  2. import (
  3. "fmt"
  4. "github.com/869413421/micro-service/common/pkg/config"
  5. "gorm.io/driver/mysql"
  6. "gorm.io/gorm"
  7. "strconv"
  8. "time"
  9. )
  10. type BaseModel struct {
  11. ID uint64 "gorm:column:id;primaryKey;autoIncrement;not null"
  12. CreatedAt time.Time `gorm:"column:created_at;index"`
  13. UpdatedAt time.Time `gorm:"column:updated_at;index"`
  14. }
  15. //GetStringID 主键转字符串
  16. func (model BaseModel) GetStringID() string {
  17. return strconv.Itoa(int(model.ID))
  18. }
  19. // CreatedAtDate 获取模型创建时间
  20. func (model BaseModel) CreatedAtDate() string {
  21. return model.CreatedAt.Format("2006-01-02 15:04:05")
  22. }
  23. // UpdatedAtDate 获取模型更新时间
  24. func (model BaseModel) UpdatedAtDate() string {
  25. return model.UpdatedAt.Format("2006-01-02 15:04:05")
  26. }
  27. var gormDb *gorm.DB
  28. var dbConfig *config.Db
  29. // connectDB 链接数据库
  30. func connectDB() (*gorm.DB, error) {
  31. // 1.获取配置
  32. serviceConfig := config.LoadConfig()
  33. dbConfig = serviceConfig.Db
  34. //2.链接数据库
  35. gormDb, err := gorm.Open(mysql.Open(fmt.Sprintf(
  36. "%s:%s@(%s)/%s?charset=%s&parseTime=True&loc=Local",
  37. dbConfig.User, dbConfig.Password, dbConfig.Address, dbConfig.Database, dbConfig.Charset,
  38. )), &gorm.Config{})
  39. if err != nil {
  40. return nil, err
  41. }
  42. //3.返回数据库链接
  43. return gormDb, nil
  44. }
  45. func setupDB() {
  46. //1.获取链接
  47. conn, err := connectDB()
  48. if err != nil {
  49. panic(err)
  50. }
  51. conn.Set("gorm:table_options", "ENGINE=InnoDB")
  52. conn.Set("gorm:table_options", "Charset=utf8")
  53. sqlDB, err := conn.DB()
  54. if err != nil {
  55. panic(fmt.Sprintf("connection to db error %v", err))
  56. }
  57. //2.设置最大连接数
  58. sqlDB.SetMaxOpenConns(dbConfig.MaxConnections)
  59. //3.设置最大空闲连接数
  60. sqlDB.SetMaxIdleConns(dbConfig.MaxIdeConnections)
  61. //4. 设置每个链接的过期时间
  62. sqlDB.SetConnMaxLifetime(dbConfig.ConnectionMaxLifeTime * time.Minute)
  63. //5.设置好连接池,重新赋值
  64. gormDb = conn
  65. }
  66. // GetDB 开放给外部获得db连接
  67. func GetDB() *gorm.DB {
  68. //1.如果db为空,初始化链接池
  69. if gormDb == nil {
  70. setupDB()
  71. }
  72. //2.返回db对象给外部使用
  73. return gormDb
  74. }

提交代码到github,供其他服务使用

:::warning 记得在项目下添加.gitignore :::

  1. git add .
  2. git commit -m "数据库连接池封装"
  3. git push

用户服务链接数据库

打开用户服务项目,引用通用代码包

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

:::warning 在我们测试如果我们修改了common的代码,需要我们将代码推送到github,然后引用包的项目更新才能看到效果,这样在开发阶段效率低下,可以修改go.mod 将common包替换成我们本地的路径,然后编译到可执行文件中,将可执行文件挂载在容器里,方法跟我们上一节中一样。但是切记,正式上线前需要讲挂载和替换去掉。 :::

  1. module github.com/869413421/micro-service/user
  2. go 1.13
  3. // This can be removed once etcd becomes go gettable, version 3.4 and 3.5 is not,
  4. // see https://github.com/etcd-io/etcd/issues/11154 and https://github.com/etcd-io/etcd/issues/11931.
  5. replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
  6. # 替换成本地common包,方便开发阶段调试
  7. replace github.com/869413421/micro-service/common => ../common
  8. require (
  9. github.com/869413421/micro-service/common v0.0.0-20220428152058-528eea77a565 // indirect
  10. github.com/golang/protobuf v1.5.2
  11. github.com/micro/go-micro/v2 v2.9.1
  12. google.golang.org/protobuf v1.28.0
  13. )

将数据库配置设置为环境变量

修改docker-compose.yaml

  1. ...
  2. micro-user-service:
  3. depends_on: # 启动依赖,需要等etcd集群启动后才启动当前容器
  4. - etcd1
  5. - etcd2
  6. - etcd3
  7. - micro-user-db
  8. build: ./user # dockerfile所在目录
  9. environment:
  10. TZ: ${TZ}
  11. MICRO_SERVER_ADDRESS: ":9091" # 服务端口
  12. MICRO_REGISTRY: "etcd" # 注册中心类型
  13. MICRO_REGISTRY_ADDRESS: "etcd1:2379,etcd2:2379,etcd3:2379" # 注册中心集群地址
  14. DB_HOST: "micro-user-db:3306"
  15. DB_DATABASE: ${USER_DB_DATABASE}
  16. DB_USER: ${USER_DB_USER}
  17. DB_PASSWORD: ${USER_DB_PASSWORD}
  18. DB_MAX_CONNECTIONS: ${USER_DB_MAX_CONNECTIONS}
  19. DB_MAX_IDE_CONNECTIONS: ${USER_DB_MAX_IDE_CONNECTIONS}
  20. DB_CONNECTIONS_MAX_LIFE_TIME: ${USER_DB_CONNECTIONS_MAX_LIFE_TIME}
  21. ports:
  22. - 9092:9091
  23. volumes:
  24. - ./user:/app
  25. networks:
  26. - micro-network
  27. ...

建立用户model

  1. mkdir -p pkg/model
  2. touch pkg/model/user.go
  1. package model
  2. import (
  3. db "github.com/869413421/micro-service/common/pkg/db"
  4. )
  5. // User 用户模型
  6. type User struct {
  7. db.BaseModel
  8. Name string `gorm:"column:name;type:varchar(255);not null;unique;default:''" valid:"name"`
  9. Email string `gorm:"column:email;type:varchar(255) not null;unique;default:''" valid:"email"`
  10. RealName string `gorm:"column:real_name;type:varchar(255);not null;default:''" valid:"realName"`
  11. Avatar string `gorm:"column:avatar;type:varchar(255);not null;default:''" valid:"avatar"`
  12. Status int `gorm:"column:status;type:tinyint(1);not null;default:0" `
  13. Password string `gorm:"column:password;type:varchar(255) not null;;default:''" valid:"password"`
  14. }

加入模型迁移

修改main.go

  1. package main
  2. import (
  3. "github.com/869413421/micro-service/common/pkg/db"
  4. "github.com/869413421/micro-service/user/handler"
  5. "github.com/869413421/micro-service/user/pkg/model"
  6. "github.com/869413421/micro-service/user/subscriber"
  7. "github.com/micro/go-micro/v2"
  8. log "github.com/micro/go-micro/v2/logger"
  9. proto "github.com/869413421/micro-service/user/proto/user"
  10. )
  11. func main() {
  12. //1.准备数据库连接,并且执行数据库迁移
  13. db := db.GetDB()
  14. db.AutoMigrate(&model.User{})
  15. // New Service
  16. service := micro.NewService(
  17. micro.Name("micro.service.user"),
  18. micro.Version("v1"),
  19. )
  20. // Initialise service
  21. service.Init()
  22. // Register Handler
  23. proto.RegisterUserHandler(service.Server(), new(handler.User))
  24. // Register Struct as Subscriber
  25. micro.RegisterSubscriber("micro.service.user", service.Server(), new(subscriber.User))
  26. // Run service
  27. if err := service.Run(); err != nil {
  28. log.Fatal(err)
  29. }
  30. }

编译可以执行代码

  1. make build

如果没有安装make命令,可手动执行

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

重新运行服务,执行模型迁移

重启用户服务容器

  1. docker-compose up -d micro-user-service

image.png

检查迁移是否执行成功

image.png
至此我们已经完成了gorm的封装,以及编写好用户服务交互的代码