likes
comments
collection
share

[Golang]使用gRPC、API网关和权限校验创建Go微服务 Part 2/2

作者站长头像
站长
· 阅读数 83

使用Go语言创建3个微服务和1个API网关 (2022版)

本文是系列文章使用gRPC、API网关和权限校验创建Go微服务的下篇。

我们继续讲解。

商品微服务 (go-grpc-product-svc)

Github: github.com/alanhou/go-…

这是三个微服务中的第二个。这里我们实现三个功能:

  • 创建商品
  • 根据ID查找某一商品
  • 根据商品ID或订单ID减少商品库存

 在命令行终端进入 go-grpc-product-svc

项目初始化

go mod init go-grpc-product-svc

安装模块

$ go get github.com/spf13/viper 
$ go get google.golang.org/grpc 
$ go get gorm.io/gorm 
$ go get gorm.io/driver/mysql

项目结构

我们需要配置整个项目。认证微服务相较API网关要精简很多。

目录

$ mkdir -p cmd pkg/config/envs pkg/db pkg/models pkg/pb pkg/services

Files

$ touch Makefile cmd/main.go pkg/config/envs/dev.env pkg/config/config.go 
$ touch pkg/pb/product.proto pkg/db/db.go pkg/models/stock_decrease_log.go pkg/models/product.go pkg/services/product.go

项目结构如下所示:

[Golang]使用gRPC、API网关和权限校验创建Go微服务 Part 2/2

Makefile

又到了快乐的编码时间了。老规矩,先编写Makefile来简化命令。

先在Makefile中添加代码:

proto:
	protoc pkg/pb/*.proto --go_out=. --go-grpc_out=.

server:
	go run cmd/main.go

Proto文件

生成protobuf文件需要先编写一个商品的proto文件。我们声明了三个功能: CreateProductFindOneDecreaseStock

pkg/pb/product.proto中添加如下代码:

syntax = "proto3";

package product;

option go_package = "./pkg/pb";

service ProductService {
  rpc CreateProduct(CreateProductRequest) returns (CreateProductResponse) {}
  rpc FindOne(FindOneRequest) returns (FindOneResponse) {}
  rpc DecreaseStock(DecreaseStockRequest) returns (DecreaseStockResponse) {}
}

// CreateProduct

message CreateProductRequest {
  string name = 1;
  int64 stock = 2;
  int64 price = 3;
}

message CreateProductResponse {
  int64 status = 1;
  string error = 2;
  int64 id = 3;
}

// FindOne

message FindOneData {
  int64 id = 1;
  string name = 2;
  int64 stock = 3;
  int64 price = 4;
}

message FindOneRequest { int64 id = 1; }

message FindOneResponse {
  int64 status = 1;
  string error = 2;
  FindOneData data = 3;
}

// DecreaseStock

message DecreaseStockRequest {
  int64 id = 1;
  int64 orderId = 2;
}

message DecreaseStockResponse {
  int64 status = 1;
  string error = 2;
}

生成Protobuf文件

接下来使用下面的命令生成protobuf文件:

$ make proto

环境变量

同样我们需要定义一些环境变量。在pkg/config/envs/dev.env中添加如下代码:

PORT=:50052
DB_URL=<USER>:<PASSWORD>@tcp(<HOST>:<PORT>)/product_svc?charset=utf8mb4&parseTime=True&loc=Local

配置

我们需要使用Viper模块初始化来加载这里环境变量。

pkg/config/config.go文件中添加如下代码:

package config

import "github.com/spf13/viper"

type Config struct {
    Port  string `mapstructure:"PORT"`
    DBUrl string `mapstructure:"DB_URL"`
}

func LoadConfig() (config Config, err error) {
    viper.AddConfigPath("./pkg/config/envs")
    viper.SetConfigName("dev")
    viper.SetConfigType("env")

    viper.AutomaticEnv()

    err = viper.ReadInConfig()

    if err != nil {
        return
    }

    err = viper.Unmarshal(&config)

    return
}

减库存记录模型

这是我们唯一包含两个模型的微服务。出于幂等考虑我们需要记录下所有减少的库存。

什么是幂等?

幂等(Idempotence)是一种特性,可以保障同一运算的调用不会导致服务状态的任何改变进而导致其它的副作用。

也就是说,我们要确保库存只减少一次。设想一下如果出于某种原因同一订单中库存单位减少了两次,就会导致数据的不一致性。

pkg/models/stock_decrease_log.go中添加如下代码:

package models

type StockDecreaseLog struct {
	Id           int64 `json:"id" gorm:"primaryKey"`
	OrderId      int64 `json:"order_id"`
	ProductRefer int64 `json:"product_id"`
}

商品模型

然后我们需要添加商品模型。在pkg/models/product.go中添加如下代码:

package models

type Product struct {
	Id                int64            `json:"id" gorm:"primaryKey"`
	Name              string           `json:"name"`
	Stock             int64            `json:"stock"`
	Price             int64            `json:"price"`
	StockDecreaseLogs StockDecreaseLog `gorm:"foreignKey:ProductRefer"`
}

数据库连接

我们还要像第一部分中一样连接数据库。在pkg/db/db.go中添加如下代码:

package db

import (
	"go-grpc-product-svc/pkg/models"
	"log"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Handler struct {
	DB *gorm.DB
}

func Init(url string) Handler {
	db, err := gorm.Open(mysql.Open(url), &gorm.Config{})

	if err != nil {
		log.Fatalln(err)
	}

	db.AutoMigrate(&models.Product{})
	db.AutoMigrate(&models.StockDecreaseLog{})

	return Handler{db}
}

商品服务

此处我们需要处理所有进入的gRPC请求。从第60行开始的DecreaseStock函数值得注意,我们检查了特定订单ID已减去的库存来保障数据的一致性,避免不小心连续两次调了同一端点。这是微服务中非常重要的课题。

pkg/services/product.go中添加代码:

package services

import (
	"context"
	"net/http"

	"go-grpc-product-svc/pkg/db"
	"go-grpc-product-svc/pkg/models"
	pb "go-grpc-product-svc/pkg/pb"
)

type Server struct {
	H db.Handler
	pb.UnimplementedProductServiceServer
}

func (s *Server) CreateProduct(ctx context.Context, req *pb.CreateProductRequest) (*pb.CreateProductResponse, error) {
	var product models.Product

	product.Name = req.Name
	product.Stock = req.Stock
	product.Price = req.Price

	if result := s.H.DB.Create(&product); result.Error != nil {
		return &pb.CreateProductResponse{
			Status: http.StatusConflict,
			Error:  result.Error.Error(),
		}, nil
	}

	return &pb.CreateProductResponse{
		Status: http.StatusCreated,
		Id:     product.Id,
	}, nil
}

func (s *Server) FindOne(ctx context.Context, req *pb.FindOneRequest) (*pb.FindOneResponse, error) {
	var product models.Product

	if result := s.H.DB.First(&product, req.Id); result.Error != nil {
		return &pb.FindOneResponse{
			Status: http.StatusNotFound,
			Error:  result.Error.Error(),
		}, nil
	}

	data := &pb.FindOneData{
		Id:    product.Id,
		Name:  product.Name,
		Stock: product.Stock,
		Price: product.Price,
	}

	return &pb.FindOneResponse{
		Status: http.StatusOK,
		Data:   data,
	}, nil
}

func (s *Server) DecreaseStock(ctx context.Context, req *pb.DecreaseStockRequest) (*pb.DecreaseStockResponse, error) {
	var product models.Product

	if result := s.H.DB.First(&product, req.Id); result.Error != nil {
		return &pb.DecreaseStockResponse{
			Status: http.StatusNotFound,
			Error:  result.Error.Error(),
		}, nil
	}

	if product.Stock <= 0 {
		return &pb.DecreaseStockResponse{
			Status: http.StatusConflict,
			Error:  "Stock too low",
		}, nil
	}

	var log models.StockDecreaseLog

	if result := s.H.DB.Where(&models.StockDecreaseLog{OrderId: req.OrderId}).First(&log); result.Error == nil {
		return &pb.DecreaseStockResponse{
			Status: http.StatusConflict,
			Error:  "Stock already decreased",
		}, nil
	}

	product.Stock = product.Stock - 1

	s.H.DB.Save(&product)

	log.OrderId = req.OrderId
	log.ProductRefer = product.Id

	s.H.DB.Create(&log)

	return &pb.DecreaseStockResponse{
		Status: http.StatusOK,
	}, nil
}

main文件

最后的重头戏,我们需要启用应用。在cmd/main.go中添加代码

package main

import (
	"fmt"
	"log"
	"net"

	"go-grpc-product-svc/pkg/config"
	"go-grpc-product-svc/pkg/db"
	pb "go-grpc-product-svc/pkg/pb"
	services "go-grpc-product-svc/pkg/services"

	"google.golang.org/grpc"
)

func main() {
	c, err := config.LoadConfig()

	if err != nil {
		log.Fatalln("Failed at config", err)
	}

	h := db.Init(c.DBUrl)

	lis, err := net.Listen("tcp", c.Port)

	if err != nil {
		log.Fatalln("Failed to listing:", err)
	}

	fmt.Println("Product Svc on", c.Port)

	s := services.Server{
		H: h,
	}

	grpcServer := grpc.NewServer()

	pb.RegisterProductServiceServer(grpcServer, &s)

	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalln("Failed to serve:", err)
	}
}

 

这时就可以使用下面命令运行应用了:

$ make server

订单微服务(go-grpc-order-svc)

Github: https://github.com/alanhou/go-grpc-project

这是三个微服务中的最后一个。我们会添加一个功能。。

  • 按用户ID和商品ID创建订单

在终端中进入go-grpc-order-svc目录执行后续操作!

初始化项目

$ go mod init go-grpc-order-svc

安装模块

$ go get github.com/spf13/viper 
$ go get google.golang.org/grpc 
$ go get gorm.io/gorm 
$ go get gorm.io/driver/mysql

项目结构

我们需要搭建项目。订单服务比API网关要简洁一些。

文件夹

$ mkdir -p cmd pkg/config/envs pkg/client pkg/db pkg/models pkg/pb pkg/services

文件

$ touch Makefile cmd/main.go pkg/config/envs/dev.env pkg/config/config.go
$ touch pkg/pb/product.proto pkg/pb/order.proto pkg/db/db.go pkg/models/order.go pkg/services/order.go pkg/client/product_client.go

文件结构如下所示:

[Golang]使用gRPC、API网关和权限校验创建Go微服务 Part 2/2

Makefile

同样需要编写Makefile文件。

Makefile中加入如下代码:

proto:
	protoc pkg/pb/*.proto --go_out=. --go-grpc_out=.

server:
	go run cmd/main.go

订单Proto文件

pkg/pb/order.proto中添加如下代码:

syntax = "proto3";

package order;

option go_package = "./pkg/pb";

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {}
}

message CreateOrderRequest {
  int64 productId = 1;
  int64 quantity = 2;
  int64 userId = 3;
}

message CreateOrderResponse {
  int64 status = 1;
  string error = 2;
  int64 id = 3;
}

商品Proto文件

这个有些特别,因为订单微服务中包含了商品微服务中的商品Proto。原因是我们创建订单时需要调用商品微服务,有两大原因。

第一是我们需要检查商品是否存在。第二是我们需要根据订单请求减少商品的库存量。

pkg/pb/product.proto中添加如下代码:

syntax = "proto3";

package product;

option go_package = "./pkg/pb";

service ProductService {
  rpc CreateProduct(CreateProductRequest) returns (CreateProductResponse) {}
  rpc FindOne(FindOneRequest) returns (FindOneResponse) {}
  rpc DecreaseStock(DecreaseStockRequest) returns (DecreaseStockResponse) {}
}

// CreateProduct

message CreateProductRequest {
  string name = 1;
  int64 stock = 2;
  int64 price = 3;
}

message CreateProductResponse {
  int64 status = 1;
  string error = 2;
  int64 id = 3;
}

// FindOne

message FindOneData {
  int64 id = 1;
  string name = 2;
  int64 stock = 3;
  int64 price = 4;
}

message FindOneRequest { int64 id = 1; }

message FindOneResponse {
  int64 status = 1;
  string error = 2;
  FindOneData data = 3;
}

// DecreaseStock

message DecreaseStockRequest {
  int64 id = 1;
  int64 orderId = 2;
}

message DecreaseStockResponse {
  int64 status = 1;
  string error = 2;
}

生成Protobuf文件

老规矩,运行以下命令生成两个protobuf文件:

$ make proto

环境文件

pkg/config/envs/dev.env中添加如下代码:

PORT=:50053
DB_URL=<USER>:<PASSWORD>@tcp(<HOST>:<PORT>)/order_svc?charset=utf8mb4&parseTime=True&loc=Local
PRODUCT_SVC_URL=localhost:50052

配置

pkg/config/config.go中添加代码:

package config

import "github.com/spf13/viper"

type Config struct {
	Port          string `mapstructure:"PORT"`
	DBUrl         string `mapstructure:"DB_URL"`
	ProductSvcUrl string `mapstructure:"PRODUCT_SVC_URL"`
}

func LoadConfig() (config Config, err error) {
	viper.AddConfigPath("./pkg/config/envs")
	viper.SetConfigName("dev")
	viper.SetConfigType("env")

	viper.AutomaticEnv()

	err = viper.ReadInConfig()

	if err != nil {
		return
	}

	err = viper.Unmarshal(&config)

	return
}

订单模型

pkg/models/order.go中添加代码:

package models

type Order struct {
	Id        int64 `json:"id" gorm:"primaryKey"`
	Price     int64 `json:"price"`
	ProductId int64 `json:"product_id"`
	UserId    int64 `json:"user_id"`
}

数据库连接

pkg/db/db.go中添加代码:

package db

import (
	"go-grpc-order-svc/pkg/models"
	"log"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Handler struct {
	DB *gorm.DB
}

func Init(url string) Handler {
	db, err := gorm.Open(mysql.Open(url), &gorm.Config{})

	if err != nil {
		log.Fatalln(err)
	}

	db.AutoMigrate(&models.Order{})

	return Handler{db}
}

商品微服务客户端

前面提到我们需要连接商品微服务。这就要创建一个客户端。

pkg/client/product_client.go中添加代码:

package client

import (
	"context"
	"fmt"

	"go-grpc-order-svc/pkg/pb"

	"google.golang.org/grpc"
)

type ProductServiceClient struct {
	Client pb.ProductServiceClient
}

func InitProductServiceClient(url string) ProductServiceClient {
	cc, err := grpc.Dial(url, grpc.WithInsecure())

	if err != nil {
		fmt.Println("Could not connect:", err)
	}

	c := ProductServiceClient{
		Client: pb.NewProductServiceClient(cc),
	}

	return c
}

func (c *ProductServiceClient) FindOne(productId int64) (*pb.FindOneResponse, error) {
	req := &pb.FindOneRequest{
		Id: productId,
	}

	return c.Client.FindOne(context.Background(), req)
}

func (c *ProductServiceClient) DecreaseStock(productId int64, orderId int64) (*pb.DecreaseStockResponse, error) {
	req := &pb.DecreaseStockRequest{
		Id:      productId,
		OrderId: orderId,
	}

	return c.Client.DecreaseStock(context.Background(), req)
}

订单服务

pkg/services/order.go中添加代码:

package services

import (
	"context"
	"net/http"

	"go-grpc-order-svc/pkg/client"
	"go-grpc-order-svc/pkg/db"
	"go-grpc-order-svc/pkg/models"
	"go-grpc-order-svc/pkg/pb"
)

type Server struct {
	H          db.Handler
	ProductSvc client.ProductServiceClient
	pb.UnimplementedOrderServiceServer
}

func (s *Server) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
	product, err := s.ProductSvc.FindOne(req.ProductId)

	if err != nil {
		return &pb.CreateOrderResponse{Status: http.StatusBadRequest, Error: err.Error()}, nil
	} else if product.Status >= http.StatusNotFound {
		return &pb.CreateOrderResponse{Status: product.Status, Error: product.Error}, nil
	} else if product.Data.Stock < req.Quantity {
		return &pb.CreateOrderResponse{Status: http.StatusConflict, Error: "Stock too less"}, nil
	}

	order := models.Order{
		Price:     product.Data.Price,
		ProductId: product.Data.Id,
		UserId:    req.UserId,
	}

	s.H.DB.Create(&order)

	res, err := s.ProductSvc.DecreaseStock(req.ProductId, order.Id)

	if err != nil {
		return &pb.CreateOrderResponse{Status: http.StatusBadRequest, Error: err.Error()}, nil
	} else if res.Status == http.StatusConflict {
		s.H.DB.Delete(&models.Order{}, order.Id)

		return &pb.CreateOrderResponse{Status: http.StatusConflict, Error: res.Error}, nil
	}

	return &pb.CreateOrderResponse{
		Status: http.StatusCreated,
		Id:     order.Id,
	}, nil
}

main文件

最后需要有一个启动文件,cmd/main.go中添加代码:

package main

import (
	"fmt"
	"log"
	"net"

	"go-grpc-order-svc/pkg/client"
	"go-grpc-order-svc/pkg/config"
	"go-grpc-order-svc/pkg/db"
	"go-grpc-order-svc/pkg/pb"
	"go-grpc-order-svc/pkg/services"

	"google.golang.org/grpc"
)

func main() {
	c, err := config.LoadConfig()

	if err != nil {
		log.Fatalln("Failed at config", err)
	}

	h := db.Init(c.DBUrl)

	lis, err := net.Listen("tcp", c.Port)

	if err != nil {
		log.Fatalln("Failed to listing:", err)
	}

	productSvc := client.InitProductServiceClient(c.ProductSvcUrl)

	if err != nil {
		log.Fatalln("Failed to listing:", err)
	}

	fmt.Println("Order Svc on", c.Port)

	s := services.Server{
		H:          h,
		ProductSvc: productSvc,
	}

	grpcServer := grpc.NewServer()

	pb.RegisterOrderServiceServer(grpcServer, &s)

	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalln("Failed to serve:", err)
	}
}

太棒了!我们已经完成了所有的微服务以及API网关。下面就来进行全面测试。但首先要确保已经启动了API网关和所有这3个微服务,在相应的项目中运行命令:

$ make server

测试所有端点

可以使用Insomnia、Postman等软件逐一测试,也可以像本文中一样使用cURL测试各端点。

首先需要注册一个用户:

$ curl --request POST \
  --url http://localhost:3000/auth/register \
  --header 'Content-Type: application/json' \
  --data '{
 "email": "elon@musk.com",
 "password": "12345678"
}'

登录

接下要进行登录来获取JSON Web Token:

$ curl --request POST \
  --url http://localhost:3000/auth/login \
  --header 'Content-Type: application/json' \
  --data '{
 "email": "elon@musk.com",
 "password": "12345678"
}'

这里的响应非常重要,因为后续的请求都需要使用响应中的token

响应

{
  "status":200,
  "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTI3OTk1MzgsImlzcyI6ImdvLWdycGMtYXV0aC1zdmMiLCJJZCI6MiwiRW1haWwiOiJlbG9uQG11c2suY29tIn0.-9zHeYgS-VHyvRoz5UXg6nMrNkJ1HU2vTfW13QlT2lE"
}

创建商品

此时我们需要在请求头中添加token来创建商品。

$ curl --request POST \
  --url http://localhost:3000/product/ \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTI3OTk1MzgsImlzcyI6ImdvLWdycGMtYXV0aC1zdmMiLCJJZCI6MiwiRW1haWwiOiJlbG9uQG11c2suY29tIn0.-9zHeYgS-VHyvRoz5UXg6nMrNkJ1HU2vTfW13QlT2lE' \
  --header 'Content-Type: application/json' \
  --data '{
 "name": "Product A",
 "stock": 5,
 "price": 15
}'

查找商品

需要在URL中添加商品ID来查找商品。

$ curl --request GET \
  --url http://localhost:3000/product/1 \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTI3OTk1MzgsImlzcyI6ImdvLWdycGMtYXV0aC1zdmMiLCJJZCI6MiwiRW1haWwiOiJlbG9uQG11c2suY29tIn0.-9zHeYgS-VHyvRoz5UXg6nMrNkJ1HU2vTfW13QlT2lE'

创建订单

需要传递商品ID和数量来创建订单。

$ curl --request POST \
  --url http://localhost:3000/order/ \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTI3OTk1MzgsImlzcyI6ImdvLWdycGMtYXV0aC1zdmMiLCJJZCI6MiwiRW1haWwiOiJlbG9uQG11c2suY29tIn0.-9zHeYgS-VHyvRoz5UXg6nMrNkJ1HU2vTfW13QlT2lE' \
  --header 'Content-Type: application/json' \
  --data '{
 "productId": 1,
 "quantity": 1
}'

恭喜你成功了!

感谢阅读本系列有关如何使用Go语言开发微服务的第二部分。真心希望读者能从中学到一些新知识。

加油!

整理自Kevin Vogel的文章。

转载自:https://juejin.cn/post/7135085640030371854
评论
请登录