Golang服务常用组件—GORM, Sqlx, MySQL, MongoDB
在分布式系统中考虑7x24h不间断服务时,相比无状态的应用服务,有状态的数据存储服务是一个非常重要又比较麻烦的部分。
在云原生中,无状态服务与有状态服务的区别有:
事件 | 无状态服务 | 有状态服务 |
---|---|---|
升级 | 直接部署新版本,删/减老版本实例 | 升级Schema; 新增表,双表操作,到下个版本才能完全迁移 |
POD异常 | 拉起新的POD实例 | 选举副本做Master,再拉起新的副本实例;在客户端中熔断,写操作切换到新的Master |
负载均衡 | Service Mesh或IPVS负载均衡,非侵入式 | 分片选择:SQL协议Proxy模式;客户端侵入式 |
数据库的升级/维护,选择GitOps比较合适,避免人工直接操作环境。
本文中,我们不演示如何实现高可用、高并发的数据库集群的Master选举、客户端熔断,只演示:
- 如何连接数据库单例或集群
- 如何完成增、删、改、查等基本操作
1. GORM
GORM是GoLang中最出色的ORM框架,支持MySQL、PostgreSQL、Sqlite、SQL Server,功能非常强大,也可以直接执行SQL并获取结果集。
其中的数据库自动迁移AutoMigrate()
需要特别注意,虽然创建Schema很便捷,但是在多应用服务实例与数据库集群中不适用。升级云原生中微服务与数据库,还是“先GitOps自动升级数据库,再部署新版本微服务”的方案比较保险。
// product_db.go
package main
import (
"log"
"os"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var db *gorm.DB
// Product Model
type Product struct {
gorm.Model
Code string
Name string
Price float64
}
// Book Model
type Book struct {
gorm.Model
ProductID uint
Product Product
AuthorID uint
ISBN string
Comment string
}
// Author Model
type Author struct {
gorm.Model
FirstName string
LastName string
PenName string
Birthday string
Books []Book
}
// UserBuy Model
type UserBuy struct {
gorm.Model
UserID uint
ProductID uint
}
// UserBook Query result
type UserBook struct {
ID string
Code string
Name string
Price float64
ISBN string
Comment string
}
func init() {
driverName := os.Getenv("SQL_DRIVER_NAME")
dataSourceName := os.Getenv("SQL_DATA_SOURCE_NAME")
var err error
db, err = gorm.Open(driverName, dataSourceName)
if err != nil {
log.Println("ERROR Gorm open DB failed")
} else {
log.Println("INFO Gorm open DB OK")
}
// Migrate the schema
db.AutoMigrate(&Product{}, &Book{}, &Author{}, &UserBuy{})
if isEmptyBooks() {
log.Println("INFO Gorm DB books is Empty")
createUserBooks()
}
}
func getUserBooks() (userBooks *[]UserBook, err error) {
userID := uint(10001)
var userBooksGot []UserBook
err = db.Raw(`SELECT C.id, C.code, C.name, C.price, B.isbn, B.comment
FROM (SELECT product_id FROM user_buys WHERE user_id = ?) AS A
INNER JOIN books B ON A.product_id = B.product_id
LEFT JOIN products C ON B.product_id = C.id`, userID).Scan(&userBooksGot).Error
if err == nil {
userBooks = &userBooksGot
}
return
}
func isEmptyBooks() bool {
var count int
err := db.Model(&Book{}).Count(&count).Error
return err == nil && count == 0
}
func createUserBooks() {
userID := uint(10001)
authors := []Author{
{
FirstName: "Ken",
LastName: "Bench",
PenName: "KB",
Birthday: "1953-2-11",
Books: []Book{
{
Product: Product{
Code: "P-10001",
Name: "Book Name 1",
Price: 46.33,
},
ISBN: "543-233-33",
Comment: "c",
},
{
Product: Product{
Code: "P-10002",
Name: "Book Name 2",
Price: 26.67,
},
ISBN: "543-233-34",
Comment: "abc",
},
},
},
{
FirstName: "Martin",
LastName: "Fowler",
PenName: "MF",
Birthday: "1973-12-13",
Books: []Book{
{
Product: Product{
Code: "P-10021",
Name: "Book Name 21",
Price: 76.33,
},
ISBN: "543-233-43",
Comment: "a",
},
{
Product: Product{
Code: "P-10022",
Name: "Book Name 22",
Price: 76.67,
},
ISBN: "543-233-44",
Comment: "123",
},
},
},
}
for i := range authors {
db.Create(&authors[i])
}
// UserBuys
var products []Product
db.Find(&products)
userBuys := make([]UserBuy, len(products))
for i, product := range products {
userBuys[i] = UserBuy{
UserID: userID,
ProductID: product.ID,
}
}
for i := range userBuys {
db.Create(&userBuys[i])
}
}
func giveUserFreeBooks(userInfo *UserInfo) error {
var books []Book
db.Where("isbn in (?)", []string{"543-233-33", "543-233-43"}).Find(&books)
log.Printf("INFO give %d books to user ID %d\n", len(books), userInfo.ID)
userBuys := make([]UserBuy, len(books))
for i, book := range books {
userBuys[i] = UserBuy{
UserID: uint(userInfo.ID),
ProductID: book.ProductID,
}
}
for i := range userBuys {
db.Create(&userBuys[i])
}
return nil
}
2. Sqlx
Sqlx是对GoLang标准database/sql
的扩展。其特点是:
- 把SQL执行的结果集转化成数据结构(Struct、Maps、Slices)。
- 支持问号(?)或命名的Prepared Statements,避免SQL注入的安全问题
连接MySQL与增/查代码:
// user_db.go
package main
import (
"log"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
const schema = `
CREATE TABLE user (
id int AUTO_INCREMENT PRIMARY KEY,
name text,
email text
);
CREATE TABLE place (
country text,
city text NULL,
telcode integer
);
ALTER TABLE user AUTO_INCREMENT = 10001;`
var db *sqlx.DB
func initDB() {
driverName := os.Getenv("SQL_DRIVER_NAME")
dataSourceName := os.Getenv("SQL_DATA_SOURCE_NAME")
if dataSourceName == "" {
log.Println("ERROR evn SQL_DRIVER_NAME and SQL_DATA_SOURCE_NAME required")
return
}
// MYSQL DSN format: username:password@protocol(address)/dbname?param=value
var err error
db, err = sqlx.Open(driverName, dataSourceName)
if err != nil {
log.Println("ERROR Fail to open to the USER DB, ", err)
}
err = db.Ping()
if err != nil {
log.Println("ERROR Fail to ping to the USER DB, ", err)
return
}
_, err = db.Exec(schema)
if err != nil {
log.Println("ERROR Fail to create schema to the USER DB, ", err)
return
}
log.Println("INFO Success to create schema to the USER DB")
}
func closeDB() {
if db != nil {
db.Close()
}
}
func createUserInfoToDB(userInfo *UserInfo) {
sql := "INSERT INTO user (name, email) VALUES (:name, :email)"
res, err := db.NamedExec(sql, userInfo)
if err != nil {
return
}
newID, err := res.LastInsertId()
if err != nil {
return
}
userInfo.ID = int(newID)
}
func getUserInfoFromDB(userID int) *UserInfo {
userInfo := UserInfo{}
err := db.Get(&userInfo, "SELECT id, name, email FROM user WHERE id = ?", userID)
if err != nil {
return nil
}
return &userInfo
}
3. MySQL
MySQL是线程模型,PostgreSQL是进程模型。MySQL比较适合处理高并发、大流量的互联网数据,节省资源。GoLang中已有标准database/sql
,为了支持MySQL,我们只需在项目中引入MySQL Driver:
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", "user:password@/dbname")
MySQL Cluster包含以下3中节点:
- 管理节点:用于给整个集群其他节点提供配置、管理、仲裁等功能。
- 数据节点:MySQL Cluster的核心,存储数据、日志,提供数据的各种管理服务。
- SQL节点(API):用于访问MySQL Cluster数据,提供对外应用服务。
4. MongoDB
MongoDB 是一个基于分布式文件存储的数据库。其存储关系是Database > Collection > Document -> Field。
Sharded Cluster配置时,以下3中节点:
- shard节点:存储分片数据。
- mongos节点:提供访问入口。
- config server节点:存储分片元信息与配置数据(自动同步给shard与mongos)。
在GoLang中使用MongoDB时,只需使用官方提供的mongo-go-driver。用法如下:
// mongo_demo.go
package main
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var mongoURI = "mongodb://my-root:my-root-pw@mongo:27017"
type numberValue struct {
Found bool
Name string
Value float64
}
func mongoDemo() numberValue {
client, err := mongo.NewClient(options.Client().ApplyURI(mongoURI))
if err != nil {
log.Println("Error Invalid mongo URI, error: ", err)
}
ctx, cancelConnect := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelConnect()
err = client.Connect(ctx)
if err != nil {
log.Println("Error Can't connect to mongo server, error: ", err)
}
ctx, cancelPing := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelPing()
err = client.Ping(ctx, nil)
if err != nil {
log.Println("Error Can't ping to mongo server, error: ", err)
} else {
log.Println("INFO ping to mongo server OK")
}
collection := client.Database("testing").Collection("numbers")
if OK, value := findPI(collection); OK {
return numberValue{true, "pi", value}
}
return insertPI(collection)
}
func findPI(collection *mongo.Collection) (bool, float64) {
var result struct {
Value float64
}
filter := bson.M{"name": "pi"}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := collection.FindOne(ctx, filter).Decode(&result)
if err != nil {
log.Println("INFO Can't find 'pi', error: ", err)
return false, 0
}
log.Println("INFO Found 'pi': ", result.Value)
return true, result.Value
}
func insertPI(collection *mongo.Collection) numberValue {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
value := 3.14159
res, err := collection.InsertOne(ctx, bson.M{"name": "pi", "value": value})
if err != nil {
log.Println("ERROR Insert 'pi', error: ", err)
return numberValue{false, "", 0}
}
id := res.InsertedID
log.Println("INFO Inserted 'pi' ID: ", id)
return numberValue{false, "pi", value}
}
5. 总结
在GORM和Sqlx之间做选择,需要根据团队的编程风格来确定。如果想干净的使用SQL,就选择Sqlx。如果偏重于ORM,就选择GORM。GORM存储关系数据时,比Sqlx要少写许多代码。不管使用哪个,在头脑中的概念一定要清楚:在整洁架构中,领域模型是核心,GORM和Sqlx只是外围存储部分。
MySQL与MongoDB之间,不需要做选择,它们面对的问题不同。MySQL属于关系数据库,在需要JOIN查询时,传统的SQL数据库时一个很好的选择。虽然New SQL也能干这个活,但是目前New SQL仍没有传统SQL数据库成熟。MongoDB是文档型数据库,它有良好的分布式能力。经常按文档修改就选MongoDB。同时,MongoDB不需传统SQL的Schema。
MySQL与MongoDB的Cluster部署时,它们的节点要求非常相似:Router(API) + Shard(Data) + Config(Mgr)。