何时使用单体 RESTful 服务
对于很多初创公司来说,业务的早期我们更应该关注于业务价值的交付,而单体服务具有架构简单,部署简单,开发成本低等优点,可以帮助我们快速实现产品需求。我们在使用单体服务快速交付业务价值的同时,也需要为业务的发展预留可能性,所以我们一般会在单体服务中清晰的拆分不同的业务模块。
商城单体 RESTful 服务
我们以商城为例来构建单体服务,商城服务一般来说相对复杂,会由多个模块组成,比较重要的模块包括账号模块、商品模块和订单模块等,每个模块会有自己独立的业务逻辑,同时每个模块间也会相互依赖,比如订单模块和商品模块都会依赖账号模块,在单体应用中这种依赖关系一般是通过模块间方法调用来完成。一般单体服务会共享存储资源,比如 MySQL
和 Redis
等。
单体服务的整体架构比较简单,这也是单体服务的优点,客户请求通过 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.api
和 mall.api
,其中 mall.api
为聚合的 api
文件,通过 import
导入,文件列表如下:
api |-- mall.api |-- order.api |-- product.api |-- user.api
Mall API 定义
mall.api
的定义如下,其中 syntax = “v1”
表示这是 zero-api
的 v1
语法
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/handler
:API
文件中定义的路由对应的handler
的实现internal/logic
:用来放每个路由对应的业务逻辑,之所以区分handler
和logic
是为了让业务处理部分尽可能减少依赖,把HTTP requests
和逻辑处理代码隔离开,便于后续拆分成RPC service
internal/svc
:用来定义业务逻辑处理的依赖,我们可以在main
函数里面创建依赖的资源,然后通过ServiceContext
传递给handler
和logic
internal/types
:定义了API
请求和返回数据结构mall.go
:main
函数所在文件,文件名和API
定义中的service
同名,去掉了后缀-api
生成的服务不需要做任何修改就可以运行:
$ go run mall.go Starting 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 }
在 UserOrdersLogic
的 UserOrders
方法中调用 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
仓库共享自定义业务模板,可以很好的实现团队协同。