摘要:本節(jié)將學(xué)習(xí)來(lái)統(tǒng)一管理和部署微服務(wù),引入第三個(gè)微服務(wù)并進(jìn)行存儲(chǔ)數(shù)據(jù)。到目前為止,要想啟動(dòng)微服務(wù)的容器,均在其中的同時(shí)設(shè)置其環(huán)境變量,服務(wù)多了以后管理起來(lái)十分麻煩。
譯文鏈接:wuYin/blog
原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權(quán)。
本文完整代碼:GitHub
在上節(jié)中,我們使用 go-micro 重新實(shí)現(xiàn)了微服務(wù)并進(jìn)行了 Docker 化,但是每個(gè)微服務(wù)都要多帶帶維護(hù)自己的 Makefile 未免過(guò)于繁瑣。本節(jié)將學(xué)習(xí) docker-compose 來(lái)統(tǒng)一管理和部署微服務(wù),引入第三個(gè)微服務(wù) user-service 并進(jìn)行存儲(chǔ)數(shù)據(jù)。
MongoDB 與 Postgres 微服務(wù)的數(shù)據(jù)存儲(chǔ)到目前為止,consignment-cli 要托運(yùn)的貨物數(shù)據(jù)直接存儲(chǔ)在 consignment-service 管理的內(nèi)存中,當(dāng)服務(wù)重啟時(shí)這些數(shù)據(jù)將會(huì)丟失。為了便于管理和搜索貨物信息,需將其存儲(chǔ)到數(shù)據(jù)庫(kù)中。
可以為每個(gè)獨(dú)立運(yùn)行的微服務(wù)提供獨(dú)立的數(shù)據(jù)庫(kù),不過(guò)因?yàn)楣芾矸爆嵣儆腥诉@么做。如何為不同的微服務(wù)選擇合適的數(shù)據(jù)庫(kù),可參考:How to choose a database for your microservices
選擇關(guān)系型數(shù)據(jù)庫(kù)與 NoSQL如果對(duì)存儲(chǔ)數(shù)據(jù)的可靠性、一致性要求不那么高,那 NoSQL 將是很好的選擇,因?yàn)樗艽鎯?chǔ)的數(shù)據(jù)格式十分靈活,比如常常將數(shù)據(jù)存為 JSON 進(jìn)行處理,在本節(jié)中選用性能和生態(tài)俱佳的MongoDB
如果要存儲(chǔ)的數(shù)據(jù)本身就比較完整,數(shù)據(jù)之間關(guān)系也有較強(qiáng)關(guān)聯(lián)性的話,可以選用關(guān)系型數(shù)據(jù)庫(kù)。事先捋一下要存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu),根據(jù)業(yè)務(wù)看一下是讀更多還是寫更多?高頻查詢的復(fù)不復(fù)雜?… 鑒于本文的較小的數(shù)據(jù)量與操作,作者選用了 Postgres,讀者可自行更換為 MySQL 等。
更多參考:如何選擇NoSQL數(shù)據(jù)庫(kù)、梳理關(guān)系型數(shù)據(jù)庫(kù)和NoSQL的使用情景
docker-compose 引入原因上節(jié)把微服務(wù) Docker 化后,使其運(yùn)行在輕量級(jí)、只包含服務(wù)必需依賴的容器中。到目前為止,要想啟動(dòng)微服務(wù)的容器,均在其 Makefile 中 docker run 的同時(shí)設(shè)置其環(huán)境變量,服務(wù)多了以后管理起來(lái)十分麻煩。
基本使用docker-compose 工具能直接用一個(gè) docker-compose.yaml 來(lái)編排管理多個(gè)容器,同時(shí)設(shè)置各容器的 metadata 和 run-time 環(huán)境(環(huán)境變量),文件的 service 配置項(xiàng)來(lái)像先前 docker run 命令一樣來(lái)啟動(dòng)容器。舉個(gè)例子:
docker 命令管理容器
$ docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 -e MICRO_REGISTRY=mdns vessel-service
等效于 docker-compose 來(lái)管理
version: "3.1" vessel-service: build: ./vessel-service ports: - 50052:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns"
想加減和配置微服務(wù),直接修改 docker-compose.yaml,是十分方便的。
更多參考:使用 docker-compose 編排容器
編排當(dāng)前項(xiàng)目的容器針對(duì)當(dāng)前項(xiàng)目,使用 docker-compose 管理 3 個(gè)容器,在項(xiàng)目根目錄下新建文件:
# docker-compose.yaml # 同樣遵循嚴(yán)格的縮進(jìn) version: "3.1" # services 定義容器列表 services: consignment-cli: build: ./consignment-cli environment: MICRO_REGISTRY: "mdns" consignment-service: build: ./consignment-service ports: - 50051:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns" DB_HOST: "datastore:27017" vessel-service: build: ./vessel-service ports: - 50052:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns"
首先,我們指定了要使用的 docker-compose 的版本是 3.1,然后使用 services 來(lái)列出了三個(gè)待管理的容器。
每個(gè)微服務(wù)都定義了自己容器的名字, build 指定目錄下的 Dockerfile 將會(huì)用來(lái)編譯鏡像,也可以直接使用 image 選項(xiàng)直接指向已編譯好的鏡像(后邊會(huì)用到);其他選項(xiàng)則指定了容器的端口映射規(guī)則、環(huán)境變量等。
可使用 docker-compose build 來(lái)編譯生成三個(gè)對(duì)應(yīng)的鏡像;使用 docker-compose run 來(lái)運(yùn)行指定的容器, docker-compose up -d 可在后臺(tái)運(yùn)行;使用 docker stop $(docker ps -aq ) 來(lái)停止所有正在運(yùn)行的容器。
運(yùn)行效果使用 docker-compose 的運(yùn)行效果如下:
Protobuf 與數(shù)據(jù)庫(kù)操作 復(fù)用及其局限性到目前為止,我們的兩個(gè) protobuf 協(xié)議文件,定義了微服務(wù)客戶端與服務(wù)端數(shù)據(jù)請(qǐng)求、響應(yīng)的數(shù)據(jù)結(jié)構(gòu)。由于 protobuf 的規(guī)范性,也可將其生成的 struct 作為數(shù)據(jù)庫(kù)表 Model 進(jìn)行數(shù)據(jù)操作。這種復(fù)用有其局限性,比如 protobuf 中數(shù)據(jù)類型必須與數(shù)據(jù)庫(kù)表字段嚴(yán)格一致,二者是高耦合的。很多人并不贊將 protobuf 數(shù)據(jù)結(jié)構(gòu)作為數(shù)據(jù)庫(kù)中的表結(jié)構(gòu):Do you use Protobufs in place of structs ?
中間層邏輯轉(zhuǎn)換一般來(lái)說(shuō),在表結(jié)構(gòu)變化后與 protobuf 不一致,需要在二者之間做一層邏輯轉(zhuǎn)換,處理差異字段:
func (service *Service) (ctx context.Context, req *proto.User, res *proto.Response) error { entity := &models.User{ Name: req.Name. Email: req.Email, Password: req.Password, } err := service.repo.Create(entity) // 無(wú)中間轉(zhuǎn)換層 // err := service.repo.Create(req) ... }
這樣隔離數(shù)據(jù)庫(kù)實(shí)體 models 和 proto.* 結(jié)構(gòu)體,似乎很方便。但當(dāng) .proto 中定義 message 各種嵌套時(shí),models 也要對(duì)應(yīng)嵌套,比較麻煩。
上邊隔不隔離由讀者自行決定,就我個(gè)人而言,中間用 models 做轉(zhuǎn)換是不太有必要的,protobuf 已足夠規(guī)范,直接使用即可。
consignment-service 重構(gòu)回頭看第一個(gè)微服務(wù) consignment-service,會(huì)發(fā)現(xiàn)服務(wù)端實(shí)現(xiàn)、接口實(shí)現(xiàn)等都往 main.go 里邊塞,功能跑通了,現(xiàn)在要拆分代碼,使項(xiàng)目結(jié)構(gòu)更加清晰,更易維護(hù)。
MVC 代碼結(jié)構(gòu)對(duì)于熟悉 MVC 開(kāi)發(fā)模式的同學(xué)來(lái)說(shuō),可能會(huì)把代碼按功能拆分到不同目錄中,比如:
main.go models/ user.go handlers/ auth.go user.go services/ auth.go微服務(wù)代碼結(jié)構(gòu)
不過(guò)這種組織方式并不是 Golang 的 style,因?yàn)槲⒎?wù)是切割出來(lái)獨(dú)立的,要做到簡(jiǎn)潔明了。對(duì)于大型 Golang 項(xiàng)目,應(yīng)該如下組織:
main.go users/ services/ auth.go handlers/ auth.go user.go users/ user.go containers/ services/ manage.go models/ container.go
這種組織方式叫類別(domain)驅(qū)動(dòng),而不是 MVC 的功能驅(qū)動(dòng)。
consignment-service 的重構(gòu)由于微服務(wù)的簡(jiǎn)潔性,我們會(huì)把該服務(wù)相關(guān)的代碼全放到一個(gè)文件夾下,同時(shí)為每個(gè)文件起一個(gè)合適的名字。
在 consignmet-service/ 下創(chuàng)建三個(gè)文件:handler.go、datastore.go 和 repository.go
consignmet-service/ ├── Dockerfile ├── Makefile ├── datastore.go # 創(chuàng)建與 MongoDB 的主會(huì)話 ├── handler.go # 實(shí)現(xiàn)微服務(wù)的服務(wù)端,處理業(yè)務(wù)邏輯 ├── main.go # 注冊(cè)并啟動(dòng)服務(wù) ├── proto └── repository.go # 實(shí)現(xiàn)數(shù)據(jù)庫(kù)的基本 CURD 操作負(fù)責(zé)連接 MongoDB 的 datastore.go
package main import "gopkg.in/mgo.v2" // 創(chuàng)建與 MongoDB 交互的主回話 func CreateSession(host string) (*mgo.Session, error) { s, err := mgo.Dial(host) if err != nil { return nil, err } s.SetMode(mgo.Monotonic, true) return s, nil }
連接 MongoDB 的代碼夠精簡(jiǎn),傳參是數(shù)據(jù)庫(kù)地址,返回?cái)?shù)據(jù)庫(kù)會(huì)話以及可能發(fā)生的錯(cuò)誤,在微服務(wù)啟動(dòng)的時(shí)候就會(huì)去連接數(shù)據(jù)庫(kù)。
負(fù)責(zé)與 MongoDB 交互的 repository.go現(xiàn)在讓我們來(lái)將 main.go 與數(shù)據(jù)庫(kù)交互的代碼拆解出來(lái),可以參考注釋加以理解:
package main import (...) const ( DB_NAME = "shippy" CON_COLLECTION = "consignments" ) type Repository interface { Create(*pb.Consignment) error GetAll() ([]*pb.Consignment, error) Close() } type ConsignmentRepository struct { session *mgo.Session } // 接口實(shí)現(xiàn) func (repo *ConsignmentRepository) Create(c *pb.Consignment) error { return repo.collection().Insert(c) } // 獲取全部數(shù)據(jù) func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) { var cons []*pb.Consignment // Find() 一般用來(lái)執(zhí)行查詢,如果想執(zhí)行 select * 則直接傳入 nil 即可 // 通過(guò) .All() 將查詢結(jié)果綁定到 cons 變量上 // 對(duì)應(yīng)的 .One() 則只取第一行記錄 err := repo.collection().Find(nil).All(&cons) return cons, err } // 關(guān)閉連接 func (repo *ConsignmentRepository) Close() { // Close() 會(huì)在每次查詢結(jié)束的時(shí)候關(guān)閉會(huì)話 // Mgo 會(huì)在啟動(dòng)的時(shí)候生成一個(gè) "主" 會(huì)話 // 你可以使用 Copy() 直接從主會(huì)話復(fù)制出新會(huì)話來(lái)執(zhí)行,即每個(gè)查詢都會(huì)有自己的數(shù)據(jù)庫(kù)會(huì)話 // 同時(shí)每個(gè)會(huì)話都有自己連接到數(shù)據(jù)庫(kù)的 socket 及錯(cuò)誤處理,這么做既安全又高效 // 如果只使用一個(gè)連接到數(shù)據(jù)庫(kù)的主 socket 來(lái)執(zhí)行查詢,那很多請(qǐng)求處理都會(huì)阻塞 // Mgo 因此能在不使用鎖的情況下完美處理并發(fā)請(qǐng)求 // 不過(guò)弊端就是,每次查詢結(jié)束之后,必須確保數(shù)據(jù)庫(kù)會(huì)話要手動(dòng) Close // 否則將建立過(guò)多無(wú)用的連接,白白浪費(fèi)數(shù)據(jù)庫(kù)資源 repo.session.Close() } // 返回所有貨物信息 func (repo *ConsignmentRepository) collection() *mgo.Collection { return repo.session.DB(DB_NAME).C(CON_COLLECTION) }拆分后的 main.go
package main import (...) const ( DEFAULT_HOST = "localhost:27017" ) func main() { // 獲取容器設(shè)置的數(shù)據(jù)庫(kù)地址環(huán)境變量的值 dbHost := os.Getenv("DB_HOST") if dbHost == ""{ dbHost = DEFAULT_HOST } session, err := CreateSession(dbHost) // 創(chuàng)建于 MongoDB 的主會(huì)話,需在退出 main() 時(shí)候手動(dòng)釋放連接 defer session.Close() if err != nil { log.Fatalf("create session error: %v ", err) } server := micro.NewService( // 必須和 consignment.proto 中的 package 一致 micro.Name("go.micro.srv.consignment"), micro.Version("latest"), ) // 解析命令行參數(shù) server.Init() // 作為 vessel-service 的客戶端 vClient := vesselPb.NewVesselServiceClient("go.micro.srv.vessel", server.Client()) // 將 server 作為微服務(wù)的服務(wù)端 pb.RegisterShippingServiceHandler(server.Server(), &handler{session, vClient}) if err := server.Run(); err != nil { log.Fatalf("failed to serve: %v", err) } }實(shí)現(xiàn)服務(wù)端的 handler.go
將 main.go 中實(shí)現(xiàn)微服務(wù)服務(wù)端 interface 的代碼多帶帶拆解到 handler.go,實(shí)現(xiàn)業(yè)務(wù)邏輯的處理。
package main import (...) // 微服務(wù)服務(wù)端 struct handler 必須實(shí)現(xiàn) protobuf 中定義的 rpc 方法 // 實(shí)現(xiàn)方法的傳參等可參考生成的 consignment.pb.go type handler struct { session *mgo.Session vesselClient vesselPb.VesselServiceClient } // 從主會(huì)話中 Clone() 出新會(huì)話處理查詢 func (h *handler)GetRepo()Repository { return &ConsignmentRepository{h.session.Clone()} } func (h *handler)CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error { defer h.GetRepo().Close() // 檢查是否有適合的貨輪 vReq := &vesselPb.Specification{ Capacity: int32(len(req.Containers)), MaxWeight: req.Weight, } vResp, err := h.vesselClient.FindAvailable(context.Background(), vReq) if err != nil { return err } // 貨物被承運(yùn) log.Printf("found vessel: %s ", vResp.Vessel.Name) req.VesselId = vResp.Vessel.Id //consignment, err := h.repo.Create(req) err = h.GetRepo().Create(req) if err != nil { return err } resp.Created = true resp.Consignment = req return nil } func (h *handler)GetConsignments(ctx context.Context, req *pb.GetRequest, resp *pb.Response) error { defer h.GetRepo().Close() consignments, err := h.GetRepo().GetAll() if err != nil { return err } resp.Consignments = consignments return nil }
至此,main.go 拆分完畢,代碼文件分工明確,十分清爽。
mgo 庫(kù)的 Copy() 與 Clone()在 handler.go 的 GetRepo() 中我們使用 Clone() 來(lái)創(chuàng)建新的數(shù)據(jù)庫(kù)連接。
可看到在 main.go 中創(chuàng)建主會(huì)話后我們就再也沒(méi)用到它,反而使用 session.Clonse() 來(lái)創(chuàng)建新的會(huì)話進(jìn)行查詢處理,可以看 repository.go 中 Close() 的注釋,如果每次查詢都用主會(huì)話,那所有請(qǐng)求都是同一個(gè)底層 socket 執(zhí)行查詢,后邊的請(qǐng)求將會(huì)阻塞,不能發(fā)揮 Go 天生支持并發(fā)的優(yōu)勢(shì)。
為了避免請(qǐng)求的阻塞,mgo 庫(kù)提供了 Copy() 和 Clone() 函數(shù)來(lái)創(chuàng)建新會(huì)話,二者在功能上相差無(wú)幾,但在細(xì)微之處卻有重要的區(qū)別。Clone 出來(lái)的新會(huì)話重用了主會(huì)話的 socket,避免了創(chuàng)建 socket 在三次握手時(shí)間、資源上的開(kāi)銷,尤其適合那些快速寫入的請(qǐng)求。如果進(jìn)行了復(fù)雜查詢、大數(shù)據(jù)量操作時(shí)依舊會(huì)阻塞 socket 導(dǎo)致后邊的請(qǐng)求阻塞。Copy 為會(huì)話創(chuàng)建新的 socket,開(kāi)銷大。
應(yīng)當(dāng)根據(jù)應(yīng)用場(chǎng)景不同來(lái)選擇二者,本文的查詢既不復(fù)雜數(shù)據(jù)量也不大,就直接復(fù)用主會(huì)話的 socket 即可。不過(guò)用完都要 Close(),謹(jǐn)記。
vessel-service 重構(gòu)拆解完 consignment-service/main.go 的代碼,現(xiàn)在用同樣的方式重構(gòu) vessel-service
新增貨輪我們?cè)诖颂砑右粋€(gè)方法:添加新的貨輪,更改 protobuf 文件如下:
syntax = "proto3"; package go.micro.srv.vessel; service VesselService { // 檢查是否有能運(yùn)送貨物的輪船 rpc FindAvailable (Specification) returns (Response) {} // 創(chuàng)建貨輪 rpc Create(Vessel) returns (Response){} } // ... // 貨輪裝得下的話 // 返回的多條貨輪信息 message Response { Vessel vessel = 1; repeated Vessel vessels = 2; bool created = 3; }
我們創(chuàng)建了一個(gè) Create() 方法來(lái)創(chuàng)建新的貨輪,參數(shù)是 Vessel 返回 Response,注意 Response 中添加了 created 字段,標(biāo)識(shí)是否創(chuàng)建成功。使用 make build 生成新的 vessel.pb.go 文件。
拆分?jǐn)?shù)據(jù)庫(kù)操作與業(yè)務(wù)邏輯處理之后在對(duì)應(yīng)的 repository.go 和 handler.go 中實(shí)現(xiàn) Create()
// vesell-service/repository.go // 完成與數(shù)據(jù)庫(kù)交互的創(chuàng)建動(dòng)作 func (repo *VesselRepository) Create(v *pb.Vessel) error { return repo.collection().Insert(v) }
// vesell-service/handler.go func (h *handler) GetRepo() Repository { return &VesselRepository{h.session.Clone()} } // 實(shí)現(xiàn)微服務(wù)的服務(wù)端 func (h *handler) Create(ctx context.Context, req *pb.Vessel, resp *pb.Response) error { defer h.GetRepo().Close() if err := h.GetRepo().Create(req); err != nil { return err } resp.Vessel = req resp.Created = true return nil }引入 MongoDB
兩個(gè)微服務(wù)均已重構(gòu)完畢,是時(shí)候在容器中引入 MongoDB 了。在 docker-compose.yaml 添加 datastore 選項(xiàng):
services: ... datastore: image: mongo ports: - 27017:27017
同時(shí)更新兩個(gè)微服務(wù)的環(huán)境變量,增加 DB_HOST: "datastore:27017",在這里我們使用 datastore 做主機(jī)名而不是 localhost,是因?yàn)?docker 有內(nèi)置強(qiáng)大的 DNS 機(jī)制。參考:docker內(nèi)置dnsserver工作機(jī)制
修改完畢后的 docker-compose.yaml:
# docker-compose.yaml version: "3.1" services: consigment-cli: build: ./consignment-cli environment: MICRO_REGISTRY: "mdns" consignment-service: build: ./consignment-service ports: - 50051:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns" DB_HOST: "datastore:27017" vessel-service: build: ./vessel-service ports: - 50052:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns" DB_HOST: "datastore:27017" datastore: image: mongo ports: - 27017:27017
修改完代碼需重新 make build,構(gòu)建鏡像時(shí)需 docker-compose build --no-cache 來(lái)全部重新編譯。
user-service 引入 Postgres現(xiàn)在來(lái)創(chuàng)建第三個(gè)微服務(wù),在 docker-compose.yaml 中引入 Postgres:
... user-service: build: ./user-service ports: - 50053:50051 environment: MICRO_ADDRESS: ":50051" MICRO_REGISTRY: "mdns" ... database: image: postgres ports: - 5432:5432
在項(xiàng)目根目錄下創(chuàng)建 user-service 目錄,并且像前兩個(gè)服務(wù)那樣依次創(chuàng)建下列文件:
handler.go, main.go, repository.go, database.go, Dockerfile, Makefile,定義 protobuf 文件
創(chuàng)建 proto/user/user.proto 且內(nèi)容如下:
// user-service/user/user.proto syntax = "proto3"; package go.micro.srv.user; service UserService { rpc Create (User) returns (Response) {} rpc Get (User) returns (Response) {} rpc GetAll (Request) returns (Response) {} rpc Auth (User) returns (Token) {} rpc ValidateToken (Token) returns (Token) {} } // 用戶信息 message User { string id = 1; string name = 2; string company = 3; string email = 4; string password = 5; } message Request { } message Response { User user = 1; repeated User users = 2; repeated Error errors = 3; } message Token { string token = 1; bool valid = 2; Error errors = 3; } message Error { int32 code = 1; string description = 2; }
確保你的 user-service 有像類似前兩個(gè)微服務(wù)的 Makefile,使用 make build 來(lái)生成 gRPC 代碼。
實(shí)現(xiàn)業(yè)務(wù)邏輯處理的 handler.go在 handler.go 實(shí)現(xiàn)的服務(wù)端代碼中,認(rèn)證模塊將在下一節(jié)使用 JWT 做認(rèn)證。
// user-service/handler.go package main import ( "context" pb "shippy/user-service/proto/user" ) type handler struct { repo Repository } func (h *handler) Create(ctx context.Context, req *pb.User, resp *pb.Response) error { if err := h.repo.Create(req); err != nil { return nil } resp.User = req return nil } func (h *handler) Get(ctx context.Context, req *pb.User, resp *pb.Response) error { u, err := h.repo.Get(req.Id); if err != nil { return err } resp.User = u return nil } func (h *handler) GetAll(ctx context.Context, req *pb.Request, resp *pb.Response) error { users, err := h.repo.GetAll() if err != nil { return err } resp.Users = users return nil } func (h *handler) Auth(ctx context.Context, req *pb.User, resp *pb.Token) error { _, err := h.repo.GetByEmailAndPassword(req) if err != nil { return err } resp.Token = "`x_2nam" return nil } func (h *handler) ValidateToken(ctx context.Context, req *pb.Token, resp *pb.Token) error { return nil }實(shí)現(xiàn)數(shù)據(jù)庫(kù)交互的 repository.go
package main import ( "github.com/jinzhu/gorm" pb "shippy/user-service/proto/user" ) type Repository interface { Get(id string) (*pb.User, error) GetAll() ([]*pb.User, error) Create(*pb.User) error GetByEmailAndPassword(*pb.User) (*pb.User, error) } type UserRepository struct { db *gorm.DB } func (repo *UserRepository) Get(id string) (*pb.User, error) { var u *pb.User u.Id = id if err := repo.db.First(&u).Error; err != nil { return nil, err } return u, nil } func (repo *UserRepository) GetAll() ([]*pb.User, error) { var users []*pb.User if err := repo.db.Find(&users).Error; err != nil { return nil, err } return users, nil } func (repo *UserRepository) Create(u *pb.User) error { if err := repo.db.Create(&u).Error; err != nil { return err } return nil } func (repo *UserRepository) GetByEmailAndPassword(u *pb.User) (*pb.User, error) { if err := repo.db.Find(&u).Error; err != nil { return nil, err } return u, nil }使用 UUID
我們將 ORM 創(chuàng)建的 UUID 字符串修改為一個(gè)整數(shù),用來(lái)作為表的主鍵或 ID 是比較安全的。MongoDB 使用了類似的技術(shù),但是 Postgres 需要我們使用第三方庫(kù)手動(dòng)來(lái)生成。在 user-service/proto/user 目錄下創(chuàng)建 extension.go 文件:
package go_micro_srv_user import ( "github.com/jinzhu/gorm" uuid "github.com/satori/go.uuid" "github.com/labstack/gommon/log" ) func (user *User) BeforeCreate(scope *gorm.Scope) error { uuid, err := uuid.NewV4() if err != nil { log.Fatalf("created uuid error: %v ", err) } return scope.SetColumn("Id", uuid.String()) }
函數(shù) BeforeCreate() 指定了 GORM 庫(kù)使用 uuid 作為 ID 列值。參考:doc.gorm.io/callbacks
GORMGorm 是一個(gè)簡(jiǎn)單易用輕量級(jí)的 ORM 框架,支持 ?Postgres, MySQL, Sqlite 等數(shù)據(jù)庫(kù)。
到目前三個(gè)微服務(wù)涉及到的數(shù)據(jù)量小、操作也少,用原生 SQL 完全可以 hold 住,所以是不是要 ORM 取決于你自己。
user-cli類比 consignment-service 的測(cè)試,現(xiàn)在創(chuàng)建 user-cli 命令行應(yīng)用來(lái)測(cè)試 user-service
在項(xiàng)目根目錄下創(chuàng)建 user-cli 目錄,并創(chuàng)建 cli.go 文件:
package main import ( "log" "os" pb "shippy/user-service/proto/user" microclient "github.com/micro/go-micro/client" "github.com/micro/go-micro/cmd" "golang.org/x/net/context" "github.com/micro/cli" "github.com/micro/go-micro" ) func main() { cmd.Init() // 創(chuàng)建 user-service 微服務(wù)的客戶端 client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient) // 設(shè)置命令行參數(shù) service := micro.NewService( micro.Flags( cli.StringFlag{ Name: "name", Usage: "You full name", }, cli.StringFlag{ Name: "email", Usage: "Your email", }, cli.StringFlag{ Name: "password", Usage: "Your password", }, cli.StringFlag{ Name: "company", Usage: "Your company", }, ), ) service.Init( micro.Action(func(c *cli.Context) { name := c.String("name") email := c.String("email") password := c.String("password") company := c.String("company") r, err := client.Create(context.TODO(), &pb.User{ Name: name, Email: email, Password: password, Company: company, }) if err != nil { log.Fatalf("Could not create: %v", err) } log.Printf("Created: %v", r.User.Id) getAll, err := client.GetAll(context.Background(), &pb.Request{}) if err != nil { log.Fatalf("Could not list users: %v", err) } for _, v := range getAll.Users { log.Println(v) } os.Exit(0) }), ) // 啟動(dòng)客戶端 if err := service.Run(); err != nil { log.Println(err) } }測(cè)試 運(yùn)行成功
在此之前,需要手動(dòng)拉取 Postgres 鏡像并運(yùn)行:
$ docker pull postgres $ docker run --name postgres -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres用戶數(shù)據(jù)創(chuàng)建并存儲(chǔ)成功: 總結(jié)
到目前為止,我們創(chuàng)建了三個(gè)微服務(wù):consignment-service、vessel-service 和 user-service,它們均使用 go-micro 實(shí)現(xiàn)并進(jìn)行了 Docker 化,使用 docker-compose 進(jìn)行統(tǒng)一管理。此外,我們還使用 GORM 庫(kù)與 Postgres 數(shù)據(jù)庫(kù)進(jìn)行交互,并將命令行的數(shù)據(jù)存儲(chǔ)進(jìn)去。
上邊的 user-cli 僅是測(cè)試使用,明文保存密碼一點(diǎn)也不安全。在本節(jié)完成基本功能的基礎(chǔ)上,下節(jié)將引入 JWT 做驗(yàn)證。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/27336.html
摘要:定義微服務(wù)作為客戶端調(diào)用的函數(shù)實(shí)現(xiàn)中的接口,使作為的服務(wù)端檢查是否有適合的貨輪貨物被承運(yùn)解析命令行參數(shù)作為的客戶端增加貨物并運(yùn)行更新中的貨物,塞入三個(gè)集裝箱,重量和容量都變大。 譯文鏈接:wuYin/blog原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權(quán)。 本節(jié)未細(xì)致介紹 Docker,更多可參考:《第一本Docker書 修訂版》 前言 在...
摘要:編程書籍的整理和收集最近一直在學(xué)習(xí)深度學(xué)習(xí)和機(jī)器學(xué)習(xí)的東西,發(fā)現(xiàn)深入地去學(xué)習(xí)就需要不斷的去提高自己算法和高數(shù)的能力然后也找了很多的書和文章,隨著不斷的學(xué)習(xí),也整理了下自己的學(xué)習(xí)筆記準(zhǔn)備分享出來(lái)給大家后續(xù)的文章和總結(jié)會(huì)繼續(xù)分享,先分享一部分的 編程書籍的整理和收集 最近一直在學(xué)習(xí)deep learning深度學(xué)習(xí)和機(jī)器學(xué)習(xí)的東西,發(fā)現(xiàn)深入地去學(xué)習(xí)就需要不斷的去提高自己算法和高數(shù)的能力然后...
閱讀 1588·2021-09-24 10:38
閱讀 1524·2021-09-22 15:15
閱讀 3074·2021-09-09 09:33
閱讀 913·2019-08-30 11:08
閱讀 650·2019-08-30 10:52
閱讀 1263·2019-08-30 10:52
閱讀 2358·2019-08-28 18:01
閱讀 533·2019-08-28 17:55