golang微服务教程(三)-betway必威下载_必威体育 betway-倾情赞助英超

Golang 微服务教程(三)

发表于 2018-05-22 | 阅览次数: | 字数统计: 5,102

原文链接:ewanvalentine.io,翻译已获作者 Ewan Valentine 授权。

本文完好代码:GitHub

在上节中,咱们运用 go-micro 从头完结了微服务并进行了 Docker 化,可是每个微服务都要独自保护自己的 Makefile 不免过于繁琐。本节将学习 docker-compose 来统一办理和布置微服务,引进第三个微服务 user-service 并进行存储数据。

MongoDB 与 Posgolang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 tgres

微服务的数据存储

到现在为止,consignment-cli 要邮寄的货品数据直接存储在 consignment-service 办理的内存中,当服务重启时这些数据将会丢掉。为了便于办理和查找货品信息,需将其存储到数据库中。

可认为每个独立运转的微服务供给独立的数据库,不过因为办理繁琐少有人这么做。怎么为不同的微服务挑选适宜的数据库,可参阅:How to choose a database for your microservices

挑选联系型数据库与 NoSQL

假如对存储数据的可靠性、共同性要求不那么高,那 NoSQL 将是很好的挑选,因为它能存储的数据格式非常灵敏,比方常常将数据存为 JSON 进行处理,在本节中选用功用和生态俱佳的MongoDB

假如要存储的数据自身就比较完好,数据之间联系也有较强关联性的话,能够选用联系型数据库。事前捋一下要存储数据的结构,依据事务看一下是读更多仍是写更多?高频查询的复不杂乱?… 鉴于本文的较小的数据量与操作,作者选用了 Postgres,读者可自行更换为 MySQL 等。

更多参阅:怎么挑选NoSQL数据库、整理联系型数据库和NoSQL的运用情形

docker-compose

引进原因

上节把微服务 Docker 化后,使其运转在轻量级、只包括服务必需依靠的容器中。到现在为止,要想发动微服务的容器,均在其 Makgolang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 efile 中 docker run 的一起设置其环境变量,服务多了今后办理起来非常费事。

根本运用

docker-compose 东西能直接用一个 docker-compose.yaml 来编列办理多个容器,一起设置各容器的 metadata 和 run-time 环境(环境变量),文件的 service 装备项来像从前 docker run 指令相同来发动容器。举个比方:

docker 指令办理容器

$ docker run -p 50052:50051 \
-e MICRO_SERVER_ADDRESS=:50051 \
-e MICRO_REGISTRY=mdns \
vessel-service

等效于 docker-compose 来办理

version: '3.1'
vessel-service:
build: ./vessel-service
ports:
- 50052:50051
environment:
MICRO_ADRESS: ":50051"
MICRO_REGISTRY: "mdns"

想加减和装备微服务,直接修正 docker-compose.yaml,是非常便利的。

更多参阅:运用 docker-compose 编列容器

编列当时项目的容器

针对当时项目,运用 docker-compose 办理 3 个容器,在项目根目录下新建文件:

# docker-compose.yaml
# 相同遵从严厉的缩进
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 来列出了三个待办理的容器。

每个微服务都界说了自己容器的姓名, build 指定目录下的 Dockerfile 将会用来编译镜像,也能够直接运用 image 选项直接指向已编译好的镜像(后边会用到);其他选项则指定了容器的端口映射规矩、环境变量等。

可运用 docker-compose build 来编译生成三个对应的镜像;运用 docker-compose run 来运转指定的容器, docker-compose up -d 可在后台运转;运用 docker stop $(docker ps -aq ) 来中止一切正在运转的容器。

运转作用

运用 docker-compose 的运转作用如下:


Protobuf 与数据库操作

复用及其局限性

到现在为止,咱们的两个 protobuf 协议文件,界说了微服务客户端与服务端数据恳求、呼应的数据结构。因为 protobuf 的标准性,也可将其生成的 struct 作为数据库表 Model 进行数据操作。这种复用有其局限性,比方 protobuf 中数据类型有必要与数据库表字段严厉共同,二者是高耦合的。许多人并不赞将 protobuf 数据结构作为数据库中的表结构:Do you use Protobufs in place of structs ?

中心层逻辑转化

一般来说,在表结构改变后与 protobuf 不共同,需求在二者之间做一层逻辑转化,处理差异字段:

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)
// 无中心转化层
// err := service.repo.Create(req)
...
}

这样阻隔数据库实体 models 和 proto.* 结构体,好像很便利。但当 .proto 中界说 message 各种嵌套时,models 也要对应嵌套,比较费事。

上边隔不阻隔由读者自行决定,就我个人而言,中心用 models 做转化是不太有必要的,protobuf 已满足标准,直接运用即可。

consig申东旭nment-service 重构

回头看榜首个微服务 consignment-service,会发现服务端完结、接口完结等都往 main.go 里边塞,功用跑通了,现在要拆分代码,使项目结构愈加明晰,更易保护。

MVC 代码结构

关于了解 MVC 开发形式的同学来说,或许会把代码按功用拆分到不同目录中,比方:

main.go
models/
user.go
handlers/
auth.go
user.go
services/
auth.go

微服务代码结构

不过这种安排办法并不是 Golang 的 style,因为微服务是切开出来独立的,要做到简洁明了。关于大型 Golang 项目,应该如下安排:

main.go
users/
services/
auth.go
handlers/
auth.go
user.go
users/
user.go
containers/
services/
manage.go
models/
container.go

这种安排办法叫范畴(domain)驱动,而不是 MVC 的功用驱动。

consignment-service 的重构

因为微服务的简洁性,咱们会把该服务相关的代码全放到一个文件夹下,一起为每个文件起一个适宜的姓名。

在 consignmet-service/ 下创立三个文件:handler.go、datastore.go 和 repository.go

consignmet-service/ 
├── Dockerfile
├── Makefile
├── datastore.go # 创立与 MongoDB 的主会话
├── handler.go # 完结微服务的服务端,处理事务逻辑
├── main.go # 注册并发动服务
├── proto
└── repository.go # 完结数据库的根本 CURD 操作

担任衔接 MongoDB 的 datastore.go

package main
import "gopkg.in/mgo.v2"
// 创立与 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 的代码够精简,传参是数据库地址,回来数据库会话以及或许发生的过错,在微服务发动的时分就会去衔接数据库。

担任与 MongoDB 交互的 repository.go

现在让咱们来将 main.go 与数据库交互的代码拆解出来,能够参阅注释加以了解:

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
}
// 接口完结
func (repo *ConsignmentRepository) Create(c *pb.Consignment) error {
return repo.collection().Insert(c)
}
// 获取悉数数据
func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) {
var cons []*pb.Consignment
// Find() 一般用来履行查询,假如想履行 select * 则直接传入 nil 即可
// 经过 .All() 将查询结快手成人果绑定到 cons 变量上
// 对应的 .One() 则只取榜首行记载
err := repo.collection().Find(nil).All(&cons)
return cons, err
}
// 封闭衔接
func (repo *ConsignmentRepository) Close() {
// Close() 会在每次查询结束的时分封闭会话
// Mgo 会在发动的时分生成一个 "主" 会话
// 你能够运用 Copy() 直接从主会话仿制出新会话来履行,即每个查询都会有自己的数据库会话
// 一起每个会话都有自己衔接到数据库的 socket 及过错处理,这么做既安全又高效
// 假如只运用一个衔接到数据库的主 socket 来履行查询,那许多恳求处理都会堵塞
// Mgo 因而能在不运用锁的情况下完美处理并发恳求
// 不过坏处便是,每次查询结束之后,有必要保证数据库会话要手动 Close
// 不然将树立过多无用的衔接,白白浪费数据库资源
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() {
// 获取容器设置的数据库地址环境变量的值
dbHost := os.Getenv("DB_HOST")
if dbHost == ""{
dbHost = DEFAULT_HOST
}
session, err := CreateSession(dbHost)
// 创立于 MongoDB 的主会话,需在退出 main() 时分手动开释衔接
defer session.Close()
if err != nil {
log.Fatalf("create session error: %v\n", err)
}
server := micro.NewService(
// 有必要和 consignment.proto 中的 package 共同
micro.Name("go.micro.srv.consignment"),
micro.Version("latest"),
)
// 解析指令行参数
server.Init()
// 作为 vessel-service 的客户端
vClient := vesselPb.NewVesselServiceClient("go.micro.srv.vessel", server.Client())
// 将 server 作为微服务的服务端
pb.RegisterShippingServiceHandler(server.Server(), &handler{session, vClient})
if err := server.Run(); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

完结服务端的 handler.go

将 main.go 中完结微服务服务端 interface 的代码乐华独自拆解到 handler.go,完结事务逻辑的处理。

package main
impor依江春世界有限公司t (...)
// 微服务服golang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 务端 struct handler 有必要完结 protobuf 中界说的 rpc 办法
// 完结办法的传参等可参阅生成的 consignmengolang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 t.pb.go
type handler struct {
session *mgo.Session
vesselClient vesselPb.VesselServiceClient
}
// 从主会话中 Clone() 出新会话处理查询
func (h *han弟弟妹妹dler)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
}
// 货品被承运
log.Printf("found vessel: %s\n", 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
ret疖肿urn 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 库的 Copy() 与 Clone()

在 handler.go 的 GetRepo() 中咱们运用 Clone() 来创立新的数据库衔接。

可看到在 main.go 中创立主会话后咱们就再也没用到它,反而运用 session.Clonse() 来创立新的会话进行查询处理,能够看 repository.go 中 Close() 的注释,假如每次查询都用主会话,那一切恳求都是同一个底层 socket 履行查询,后边的恳求将会堵塞,不能发挥 Go 天然生成支撑并发的优势。

为了防止恳求的堵塞,mgo 库供给了 Copy() 和 Clone() 函数来创立新会话,二者在功用上相差无几,但在纤细之处却有重要的差异。Clone 出来的新会话重用了主会话的 socket,防止了创立 socket 在三次握手时刻、资源上的开支,特别合适那些快速写入的恳求。假如进行了杂乱查询、大数据量操作时依旧会堵塞 socket 导致后边的恳求堵塞。Copy 为会话创立新的 socket,开支大。

应当依据运用场景不同来挑选二者,本文的查询既不杂乱数据量也不大,就直接复用主会话的 socket 即可。不过用完都要 Close(),谨记。

vessel-service 重构

拆解完 consignment-service/main.go 的代码,现在用相同的办法重构 vessel-service

新增货轮

咱们在此添加一个办法:添加新的货轮,更改 protobuf 文件如下:

syntax = "proto3";
package go.micro.srv.vessel;
service VesselService {
// 查看是否有能运送货品的轮船
rpc FindAvailable (Spec下载电影ification) returns (Response) {}
// 创立货轮wapi
rpc Create(Vessel) returns (Response){}
}
// ...
// 货轮装得下的话
// 回来的多条货轮信息
message Response {
Vessel vessel = 1;
repeated Vessel vessels = 2;
bool created = 3;
}

咱们创立了一个 Create() 办法来创立新的货轮,参数是 Vessel 回来 Response,留意 Response 中添加了 created 字段,标识是否创立成功。运用 make build 生成新的 vessel.pb.go 文件。

拆分数据库操作与事务逻辑处理

之后在对应的 repository.go 和 handler.go 中完结 Create()

// vesell-service/repository.go
// 完结与数据库交互的创立动作
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()}
}
// 完结微服务的服务端
fgolang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 unc (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
}

引进 MongoconfrenceDB

两个微服务均已重构结束,是时分在容器中引进 MongoDB 了。在 docker-compose.yaml 添加 datastore 选项:

services:
...
datastore:
image: mongo
ports:
- 27017:27017

一起更新两个微服务的环境变量,添加 DB_HOST: "datastore:27017",在这里咱们运用 datastore 做主机名而不是 localhost,是因为 docker 有内置强壮的 DNS 机制。参阅:docker内置dnsserver作业机制

修正结束后的 docker-compose.yaml:

# docker-compose.yaml
version: '3.1'
services:
consigment-cli:
build: ./consignment-cli
environment:
MICRO_REGISTRY: "mdns"
consignment-service:
build: ./consig凉拌牛肉nment-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,构建镜像时需 docker-compose build --no-cache 来悉数从头编译。

user-service

引进 Postgres

现在来创立第三个微服务,在 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

在项目根目录下创立 user-service 目录,而且像前两个服务那样顺次创立下列文件:

1
handler.go, main.go, repository.go, database.go, Dockerfile, Makefile

界说 protobuf 文件

创立 proto/user/user.proto 且内容如下:

// 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;
golang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 Error errors = 3;
}
message Error {
int32 code = 1;
string description = 2;
}

保证你的 user-service 有像相似前两个微服务的 Makefile,运用 make build 来生成 gRPC 代码。

完结事务逻辑处理的 handler.go

在 handler.go 完结的服务端代码中,认证模块将在下一节运用 JWT 做认证。

// 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.G雾面褐etByEmailAndPassword(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
}

完结数据库交互的 repository.go

package main
import (
"github.com/jinz李瑞英退隐的本相hu/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春风650 != 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 创立的 UUID 字符串修正为一个整数,用来作为表的主键或 ID 是比较安全的。MongoDB 运用了相似的技能,可是 Postgres 需求咱们运用第三方库手动来生成。在 user-service/proto/user 目录下创立 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\n", err)
}
return scope.SetColumn("Id", uuid.String())
}

函数 BeforeCreate() 指定了 GORM 库运用 uuid 作为 ID 列值。参阅:doc.gorm.io/callbacks

GORM

Gorm 是一个简略易用轻量级的 ORM 结构,支撑 Postgres, MySQL, Sqlite 等数据库。

到现在三个微服务涉及到的数据量小、操作也少,用原生 SQL 完全能够 hold 住,所以是不是要 ORM 取决于你自己。

user-cli

类比 consignment-service 的测验,现在创立 user-cli 指令行运用来测验 user-service

在项目根目录下创立 user-cli 目录,并创立 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()
// 创立 user-service 微服务的客户端
client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient)
// 设置指令行参数
golang微服务教程(三)-betway必威下载_必威体育 betway-倾情资助英超 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)
}),
)
// 发动客户端
if err := service.Run(); err != nil {
log.Println(err)
}
}

测验

运转成功


在此之前,需求手动拉取 Postgres 镜像并运转:

$ docker pull postgres
$ docker run --name postgres -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres

用户数据创立并存储成功:


总结

到现在为止,咱们创立了三个微服务:consignment-service、vessel-service 和 user-service,它们均运用 go-micro 完结并进行了 Docker 化,运用 docker-compose 进行统一办理。此外,咱们还运用 GORM 库与 Postgres 数据库进行交互,并将指令行的数据存储进去。

上边的 user-cli 仅是测验运用,明文保存暗码一点也不安全。在本节完结根本功用的基础上,下节将引进 JWT 做验证。精油按摩

评论(0)