何時使用單體 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
倉庫共享自定義業務模板,可以很好的實現團隊協同。
專案地址
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支援我們!
微信交流群
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。
如果你有 go-zero
的使用心得文章,或者原始碼學習筆記,歡迎通過公眾號聯絡投稿!