何时应用单体 RESTful 服务

对于很多初创公司来说,业务的晚期咱们更应该关注于业务价值的交付,而单体服务具备架构简略,部署简略,开发成本低等长处,能够帮忙咱们疾速实现产品需要。咱们在应用单体服务疾速交付业务价值的同时,也须要为业务的倒退预留可能性,所以咱们个别会在单体服务中清晰的拆分不同的业务模块。

商城单体 RESTful 服务

咱们以商城为例来构建单体服务,商城服务一般来说绝对简单,会由多个模块组成,比拟重要的模块包含账号模块、商品模块和订单模块等,每个模块会有本人独立的业务逻辑,同时每个模块间也会相互依赖,比方订单模块和商品模块都会依赖账号模块,在单体利用中这种依赖关系个别是通过模块间办法调用来实现。个别单体服务会共享存储资源,比方 MySQLRedis 等。

单体服务的整体架构比较简单,这也是单体服务的长处,客户申请通过 DNS 解析后通过 Nginx 转发到商城的后端服务,商城服务部署在 ECS 云主机上,为了实现更大的吞吐和高可用个别会部署多个正本,这样一个简略的平民架构如果优化好的话也是能够承载较高的吞吐的。

商城服务外部多个模块间存在依赖关系,比方申请订单详情接口 /order/detail,通过路由转发到订单模块,订单模块会依赖账号模块和商品模块组成残缺的订单详情内容返回给用户,在单体服务中多个模块个别会共享数据库和缓存。

单体服务实现

接下来介绍如何基于 go-zero 来疾速实现商城单体服务。应用过 go-zero 的同学都晓得,咱们提供了一个 API 格局的文件来形容 Restful API,而后能够通过 goctl 一键生成对应的代码,咱们只须要在 logic 文件里填写对应的业务逻辑即可。商城服务蕴含多个模块,为了模块间互相独立,所以不同模块由独自的 API 定义,然而所有的 API 的定义都是在同一个 service (mall-api) 下。

api 目录下别离创立 user.api, order.api, product.apimall.api,其中 mall.api 为聚合的 api 文件,通过 import 导入,文件列表如下:

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

Mall API 定义

mall.api 的定义如下,其中 syntax = “v1” 示意这是 zero-apiv1 语法

syntax = "v1"import "user.api"import "order.api"import "product.api"

账号模块 API 定义

  • 查看用户详情
  • 获取用户所有订单
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)}

订单模块 API 定义

  • 获取订单详情
  • 生成订单
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)}

商品模块 API 定义

  • 查看商品详情
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)}

生成单体服务

曾经定义好了 API,接下来用 API 生成服务就会变得非常简单,咱们应用 goctl 生成单体服务代码。

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

生成的代码构造如下:

.├── 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

解释一下生成的代码构造:

  • api:寄存 API 形容文件
  • etc:用来定义我的项目配置,所有的配置项都能够写在 mall-api.yaml
  • internal/config:服务的配置定义
  • internal/handlerAPI 文件中定义的路由对应的 handler 的实现
  • internal/logic:用来放每个路由对应的业务逻辑,之所以辨别 handlerlogic 是为了让业务解决局部尽可能减少依赖,把 HTTP requests 和逻辑解决代码隔离开,便于后续拆分成 RPC service
  • internal/svc:用来定义业务逻辑解决的依赖,咱们能够在 main 函数外面创立依赖的资源,而后通过 ServiceContext 传递给 handlerlogic
  • internal/types:定义了 API 申请和返回数据结构
  • mall.gomain 函数所在文件,文件名和 API 定义中的 service 同名,去掉了后缀 -api

生成的服务不须要做任何批改就能够运行:

$ go run mall.goStarting server at 0.0.0.0:8888...
实现业务逻辑

接下来咱们来一起实现一下业务逻辑,出于演示目标逻辑会比较简单,并非真正业务逻辑。

首先,咱们先来实现用户获取所有订单的逻辑,因为在用户模块并没有订单相干的信息,所以咱们须要依赖订单模块查问用户的订单,所以咱们在 UserOrdersLogic 中增加对 OrderLogic 依赖

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),    }}

OrderLogic 中实现依据 用户id 查问所有订单的办法

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}

UserOrdersLogicUserOrders 办法中调用 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}

这时候咱们重新启动 mall-api 服务,在浏览器中申请获取用户所有订单接口

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

返回后果如下,合乎咱们的预期

{    "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"        }    ]}

接下来咱们再来实现创立订单的逻辑,创立订单首先须要查看该商品的库存是否足够,所以在订单模块中须要依赖商品模块。

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),    }}

创立订单的逻辑如下

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}

依赖的商品模块逻辑如下

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}

以上能够看出应用 go-zero 开发单体服务还是非常简单的,有助于咱们疾速开发上线,同时咱们还做了模块的划分,为当前做微服务的拆分也打下了根底。

总结

通过以上的示例能够看出应用 go-zero 实现单体服务非常简单,只须要定义 api 文件,而后通过 goctl 工具就能主动生成我的项目代码,咱们只须要在logic中填写业务逻辑即可,这里只是为了演示如何基于 go-zero 疾速开发单体服务并没有波及数据库和缓存的操作,其实咱们的 goctl 也能够一键生成 CRUD 代码和 cache 代码,对于开发单体服务来说能够起到事倍功半的成果。

并且针对不同的业务场景,定制化的需要也能够通过自定义模板来实现,还能够在团队内通过近程 git 仓库共享自定义业务模板,能够很好的实现团队协同。

我的项目地址

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

欢送应用 go-zerostar 反对咱们!

微信交换群

关注『微服务实际』公众号并点击 交换群 获取社区群二维码。

如果你有 go-zero 的应用心得文章,或者源码学习笔记,欢送通过公众号分割投稿!