5

When to use a monolithic RESTful service

For many startups, in the early stage of business, we should focus more on the delivery of business value, and the single service has the advantages of simple architecture, simple deployment, and low development cost, which can help us quickly realize product requirements. While we use the single service to quickly deliver business value, we also need to reserve the possibility for the development of the business, so we generally separate different business modules clearly in the single service.

Mall single RESTful service

We take the mall as an example to build a single service. Generally speaking, the mall service is relatively complex and consists of multiple modules. The more important modules include the account module, the commodity module, and the order module. Each module will have its own independent business. At the same time, each module will also depend on each other. For example, the order module and the commodity module will depend on the account module. In a monolithic application, this dependency is generally accomplished through method calls between modules. Generally, a single service will share storage resources, such as MySQL and Redis and so on.

The overall architecture of the single service is relatively simple, which is also the advantage of the single service. The customer request passes through DNS after parsing and then passes through Nginx is forwarded to the back-end service of the mall, and the mall service is deployed ECS On cloud hosts, in order to achieve greater throughput and high availability, multiple copies are generally deployed. Such a simple 平民架构 can also carry higher throughput if optimized.

There are dependencies between multiple modules within the mall service, such as the request for order details interface /order/detail , which is forwarded to the order module through routing. The order module will rely on the account module and the commodity module to form a complete order details and return it to the user. In a monolithic service, multiple modules typically share databases and caches.

Monolithic service implementation

Next, we will introduce how to quickly realize the single service of the mall based on go-zero . Students who have used go-zero know that we provide a API format file to describe Restful API , and then you can use the goctl key To generate the corresponding code, we only need to fill in the corresponding business logic in the logic file. The mall service contains multiple modules. In order to be independent of each other, different modules are defined by separate API , but all API are defined in the same service (mall-api) down.

api目录user.api , order.api , product.api mall.api ,其中mall.api the aggregated api file, imported through import , the file list is as follows:

 api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api

Mall API Definition

mall.api is defined as follows, where syntax = “v1” means this is the zero-api 6fabe3737a2d70ae8045042 syntax of v1

 syntax = "v1"

import "user.api"
import "order.api"
import "product.api"

Account Module API Definition

  • View user details
  • Get all user orders
 syntax = "v1"

type (
    UserRequest {
        ID int64 `path:"id"`
    }

    UserReply {
        ID      int64   `json:"id"`
        Name    string  `json:"name"`
        Balance float64 `json:"balance"`
    }

    UserOrdersRequest {
        ID int64 `path:"id"`
    }

    UserOrdersReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }
)

service mall-api {
    @handler UserHandler
    get /user/:id (UserRequest) returns (UserReply)

    @handler UserOrdersHandler
    get /user/:id/orders (UserOrdersRequest) returns (UserOrdersReply)
}

Order Module API Definition

  • Get order details
  • Generate orders
 syntax = "v1"

type (
    OrderRequest {
        ID string `path:"id"`
    }

    OrderReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }

    OrderCreateRequest {
        ProductID int64 `json:"product_id"`
    }

    OrderCreateReply {
        Code int `json:"code"`
    }
)

service mall-api {
    @handler OrderHandler
    get /order/:id (OrderRequest) returns (OrderReply)

    @handler OrderCreateHandler
    post /order/create (OrderCreateRequest) returns (OrderCreateReply)
}

Product Module API Definition

  • View product details
 syntax = "v1"

type ProductRequest {
    ID int64 `path:"id"`
}

type ProductReply {
    ID    int64   `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    Count int64   `json:"count"`
}

service mall-api {
    @handler ProductHandler
    get /product/:id (ProductRequest) returns (ProductReply)
}

Generate a monolithic service

It has been defined API , and then use API to generate the service will become very simple, we use goctl to generate the single service code.

 $ goctl api go -api api/mall.api -dir .

The generated code structure is as follows:

 .
├── api
│   ├── mall.api
│   ├── order.api
│   ├── product.api
│   └── user.api
├── etc
│   └── mall-api.yaml
├── internal
│   ├── config
│   │   └── config.go
│   ├── handler
│   │   ├── ordercreatehandler.go
│   │   ├── orderhandler.go
│   │   ├── producthandler.go
│   │   ├── routes.go
│   │   ├── userhandler.go
│   │   └── userordershandler.go
│   ├── logic
│   │   ├── ordercreatelogic.go
│   │   ├── orderlogic.go
│   │   ├── productlogic.go
│   │   ├── userlogic.go
│   │   └── userorderslogic.go
│   ├── svc
│   │   └── servicecontext.go
│   └── types
│       └── types.go
└── mall.go

Explain the structure of the generated code:

  • api : store API description file
  • etc : used to define project configuration, all configuration items can be written in mall-api.yaml
  • internal/config : Configuration definition of the service
  • internal/handler : API ---The implementation of the route defined in the file corresponds to handler
  • internal/logic : used to put the business logic corresponding to each route, the reason to distinguish handler and logic is to reduce the dependency of the business processing part as much as possible HTTP requests is isolated from the logic processing code, which is convenient for subsequent splitting into RPC service
  • internal/svc : used to define the dependencies of business logic processing, we can create dependent resources in the main function, and then pass ServiceContext to handler logic
  • internal/types : defines API request and return data structures
  • mall.gomain函数所在文件,文件名API定义中的service-api

The resulting service will run without any modification:

 $ go run mall.go
Starting server at 0.0.0.0:8888...
Implement business logic

Next, let's implement the business logic together. For demonstration purposes, the logic will be relatively simple, not real business logic.

First of all, let's implement the logic that the user obtains all orders, because there is no order-related information in the user module, so we need to rely on the order module to query the user's orders OrderLogic so we add the UserOrdersLogic OrderLogic depends

 type UserOrdersLogic struct {
    logx.Logger
    ctx        context.Context
    svcCtx     *svc.ServiceContext
    orderLogic *OrderLogic
}

func NewUserOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserOrdersLogic {
    return &UserOrdersLogic{
        Logger:     logx.WithContext(ctx),
        ctx:        ctx,
        svcCtx:     svcCtx,
        orderLogic: NewOrderLogic(ctx, svcCtx),
    }
}

Implement the method of querying all orders according to 用户id OrderLogic ---425f9b86979b1e7f0ce702ba72c6b5fa---

 func (l *OrderLogic) ordersByUser(uid int64) ([]*types.OrderReply, error) {
    if uid == 123 {
        // It should actually be queried from database or cache
        return []*types.OrderReply{
            {
                ID:       "236802838635",
                State:    1,
                CreateAt: "2022-5-12 22:59:59",
            },
            {
                ID:       "236802838636",
                State:    1,
                CreateAt: "2022-5-10 20:59:59",
            },
        }, nil
    }

    return nil, nil
}

UserOrdersLogic the UserOrders method in the ---71e730dc65b0bbd85718ea6f4d2ba5c5--- method of ordersByUser

 func (l *UserOrdersLogic) UserOrders(req *types.UserOrdersRequest) (*types.UserOrdersReply, error) {
    orders, err := l.orderLogic.ordersByUser(req.ID)
    if err != nil {
        return nil, err
    }

    return &types.UserOrdersReply{
        Orders: orders,
    }, nil
}

At this time, we restart the mall-api service and request to get all the user's order interface in the browser

 http://localhost:8888/user/123/orders

The returned results are as follows, which is in line with our expectations

 {
    "orders": [
        {
            "id": "236802838635",
            "state": 1,
            "create_at": "2022-5-12 22:59:59"
        },
        {
            "id": "236802838636",
            "state": 1,
            "create_at": "2022-5-10 20:59:59"
        }
    ]
}

Next, let's implement the logic of creating an order. To create an order, we first need to check whether the inventory of the product is sufficient, so we need to rely on the product module in the order module.

 type OrderCreateLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
    productLogic *ProductLogic
}

func NewOrderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderCreateLogic {
    return &OrderCreateLogic{
        Logger:       logx.WithContext(ctx),
        ctx:          ctx,
        svcCtx:       svcCtx,
        productLogic: NewProductLogic(ctx, svcCtx),
    }
}

The logic for creating an order is as follows

 const (
    success = 0
    failure = -1
)

func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateRequest) (*types.OrderCreateReply, error) {
    product, err := l.productLogic.productByID(req.ProductID)
    if err != nil {
        return nil, err
    }

    if product.Count > 0 {
        return &types.OrderCreateReply{Code: success}, nil
    }

    return &types.OrderCreateReply{Code: failure}, nil
}

The logic of the dependent commodity module is as follows

 func (l *ProductLogic) Product(req *types.ProductRequest) (*types.ProductReply, error) {
    return l.productByID(req.ID)
}

func (l *ProductLogic) productByID(id int64) (*types.ProductReply, error) {
    return &types.ProductReply{
        ID:    id,
        Name:  "apple watch 3",
        Price: 3333.33,
        Count: 99,
    }, nil
}

It can be seen from the above that it is very simple to use go-zero to develop a single service, which will help us to quickly develop and go online. At the same time, we have also divided modules, which lays the foundation for the splitting of microservices in the future. .

Summarize

From the above example, it can be seen that using go-zero to implement a single service is very simple, just need to define api file, and then use the goctl tool to automatically generate the project code , we only need to fill in the business logic in logic, here is just to demonstrate how to quickly develop a single service based on go-zero does not involve database and cache operations, in fact, our goctl You can also generate CRUD code and cache code with one click, which can achieve twice the result with half the effort for developing a single service.

And for different business scenarios, customized requirements can also be achieved through custom templates, and you can also share custom business templates within the team through a remote git warehouse, which can well achieve team collaboration.

project address

https://github.com/zeromicro/go-zero

Welcome go-zero and star support us!

WeChat exchange group

Follow the official account of " Microservice Practice " and click on the exchange group to get the QR code of the community group.

If you have go-zero use experience articles, or source code study notes, please contact us through the public account to contribute!


kevinwan
931 声望3.5k 粉丝

go-zero作者