说明
本次主要按照这个博主的go-zero微服务实战进行学习
安装依赖
- 安装依赖go-zero:
go get -u github.com/zeromicro/go-zero@latest
- 安装依赖goctl,用命令进行安装:
go install github.com/zeromicro/go-zero/tools/goctl@latest
- 安装成功后,输入
go env
找到GOPATH
1 | E:\proj\gowork\studyGoZero>go env |
- 把
C:\Users\Administrator\go\bin\goctl.exe
拷贝到GOROOT\bin目录下 - 查看版本成功
1 | PS E:\proj\gowork\studyGoZero\apps\order> goctl --version |
- 安装
protoc
,查看他的作用
1 | goctl env check --install --verbose --force |
- 安装
protoc
成功后,把C:\Users\Administrator\go\bin\
的相关文件拷贝到GOROOT\bin
服务划分
从以上思维导图可以看出整个电商系统功能还是比较多的,我们根据业务职能做如下微服务的划分:
- 商品服务(product) - 商品的添加、信息查询、库存管理等功能
- 购物车服务(cart) - 购物车的增删改查
- 订单服务(order) - 生成订单,订单管理
- 支付服务(pay) - 通过调用第三方支付实现支付功能
- 账号服务(user) - 用户信息、等级、封禁、地址管理
- 推荐服务(recommend) - 首页商品推荐
- 评论服务(reply) - 商品的评论功能、评论的回复功能
BFF层
- 但对于一个复杂的高并发的系统来说,我们需要处理各种异常的场景,比如某个页面需要依赖多个微服务提供的数据,为了避免串行请求导致的耗时过长,我们一般会并行的请求多个微服务,这个时候其中的某个服务请求异常的话我们可能需要做一些特殊的处理,比如提供一些降级的数据等。我们的解决方案就是加一层,即BFF层,通过BFF对外提供HTTP接口,客户端只与BFF进行交互,我们的这个项目为了简化只会采用一个BFF服务
- 我们可以提供多个BFF吗?答案是当然可以。BFF的目的是为客户端提供一个集中的接口,例如移动端页面和浏览器页面的数据协议不同,这种情况下为了更好的表示数据,可以使用两个BFF,同时只供一个BFF如果该BFF异常就会导致所有的业务受影响,提供多个BFF也可以提高服务的可用性,降低业务异常的影响面。多个BFF架构图如下:
项目结构
- 初始化项目
1 | cd E:\proj\gowork |
- 目录结构如下
1 | ─apps |
- 其中apps存放的是我们所有的微服务,比如order为订单相关的微服务,pkg目录为所有服务共同依赖的包的存放路径,比如所有的服务都需要依赖鉴权就可以放到pkg目录下
- app - BFF服务
- cart - 购物车服务
- order - 订单服务
- pay - 支付服务
- product - 商品服务
- recommend - 推荐服务
- reply - 评论服务
- user - 账号服务
在每个服务目录下我们又会分为多个服务,主要会有如下几类服务:
- api - 对外的BFF服务,接受来自客户端的请求,暴露HTTP接口
- rpc - 对内的微服务,仅接受来自内部其他微服务或者BFF的请求,暴露gRPC接口
- rmq - 负责进行流式任务处理,上游一般依赖消息队列,比如kafka等
- admin - 也是对内的服务,区别于rpc,更多的是面向运营侧的且数据权限较高,通过隔离可带来更好的代码级别的安全,直接提供HTTP接口
API 定义
首页API
首页功能主要分为四个部分,搜索、Banner图、限时抢购和推荐商品列表,点击搜索框会跳转到搜索页,推荐部分是分页展示的,用户通过不断地往上滑动可以加载下一页。通过分析首页我们大致需要提供三个接口,分别是Banner接口,限时抢购接口和推荐接口。
- 打开
apps\app\api\api.api
文件,写入对外api的代码定义
1 | syntax = "v1" |
- 定义好api后,我们使用如下命令重新生成项目代码,输出如下信息表明生成成功
1 | PS E:\proj\gowork\studyGoZero\apps\app\api> goctl api go -api api.api -dir . |
admin 层代码
order目录
目录为: E:\proj\gowork\studyGoZero\apps\order
执行如下命令即可初始化
order admin
代码,注意order admin
为api
服务,直接对前端提供HTTP接口1
E:\proj\gowork\studyGoZero\apps\order> goctl api new admin
代码结构如下
1 | ─admin |
生成的服务代码我们可以直接运行
go run admin.go
,默认侦听在8888端口1
2PS E:\proj\gowork\studyGoZero\apps\order\admin> go run .\admin.go
Starting server at 0.0.0.0:8888...对于rmq服务我们会使用go-zero提供的 kq 功能,这里先初始化
main.go
其他层
- pay、pruduct、recommend、reply、user 等按照同样的方式进行初始化
rpc服务层
Goctl Rpc是
goctl
脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码作者博客中初始化rpc使用的这种
goctl rpc new rpc
,并不推荐,研究代码后发现和作者提供的仓库代码不一致,使用的第二种方式生成的rpc服务层的代码和目录:通过指定proto生成rpc服务
因为BFF只负责数据的组装工作,数据真正的来源是各个微服务通过RPC接口提供,接下来我们来定义各个微服务的proto。如下展示的订单列表页面由两部分数据组成,分别是订单数据和商品数据,也就是我们的BFF需要依赖order-rpc和product-rpc来完成该页面数据的组装,下面我们分别来定义order-rpc和product-rpc
order层
- 在
E:\proj\gowork\studyGoZero\apps\order\rpc\order.proto
,定义如下,service
名字为Order
,添加了Orders
获取订单列表rpc
接口。
1 | syntax = "proto3"; |
使用如下命令重新生成代码,注意这里需要依赖protoc-gen-go和protoc-gen-go-grpc两个插件,木有安装的话执行下面命令会报错:
1 | PS E:\proj\gowork\studyGoZero\apps\order\rpc> goctl rpc protoc order.proto --go_out=. --go-grpc_out=. --zrpc_out=. |
注意最终生成的服务端的代码目录为:E:\proj\gowork\studyGoZero\apps\order\rpc\orderclient\order.go
目录结构如下:
1 | PS E:\proj\gowork\studyGoZero\apps\order\rpc> tree /f |
- 本地安装etcd,打开
E:\app\etcd-v3.4.27-windows-amd64\etcd.exe
启动服务
etcd是一个分布式一致性键值存储,其主要用于分布式系统的共享配置和服务发现。
etcd由Go语言编写
- 生成好后然后启动
order-rpc
服务,需要连接etcd服务
1 | PS E:\proj\gowork\studyGoZero\apps\order\rpc> go run .\order.go |
在E:\proj\gowork\studyGoZero\apps\order\rpc\etc\order.yaml中,可以看到etcd的连接服务器地址,和order-rpc启动的地址信息:
1
2
3
4
5
6 Name: order.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
127.0.0.1:2379
Key: order.rpc
product
在E:\proj\gowork\studyGoZero\apps\product\rpc\product.proto
,定义如下,service
名字为Order
,添加了product
产品的rpc
接口。
1 | syntax = "proto3"; |
- 在
E:\proj\gowork\studyGoZero\apps\product\rpc\etc\product.yaml
配置启动服务地址端口为8081
1 | Name: product.rpc |
- 启动
product-rpc
服务,需要连接etcd服务
1 | PS E:\proj\gowork\studyGoZero\apps\product\rpc> go run .\product.go |
BUFF 层配置
因为我们的BFF需要依赖order.rpc和product.rpc,我们需要先添加配置文件(
E:\proj\gowork\studyGoZero\apps\app\api\etc\api-api.yaml
),如下:1
2
3
4
5
6
7
8
9
10
11
12
13Name: api-api
Host: 0.0.0.0
Port: 8888
OrderRPC:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: order.rpc
ProductRPC:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: product.rpc然后在
E:\proj\gowork\studyGoZero\apps\app\api\internal\svc\servicecontext.go
中添加RPC的客户端,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package svc
import (
"example.com/studyGoZero/apps/app/api/internal/config"
"example.com/studyGoZero/apps/order/rpc/orderclient"
"example.com/studyGoZero/apps/product/rpc/productclient"
)
type ServiceContext struct {
Config config.Config
OrderRPC orderclient.Order
ProductRPC productclient.Product
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
}
}
最后只要在订单接口的E:\proj\gowork\studyGoZero\apps\app\api\internal\logic\orderlistlogic.go
方法中添加逻辑就可以啦,这里只是演示,所以会比较简单:
1 | func (l *OrderListLogic) OrderList(req *types.OrderListRequest) (resp *types.OrderListResponse, err error) { |
测试
- 请确保order、product 的rpc服务器已经启动
1 | PS E:\proj\gowork\studyGoZero\apps\product\rpc> go run .\product.go |
- 启动api层服务
1 | E:\proj\gowork\studyGoZero\apps\app\api>go run .\api.go |
- 浏览器打开
http://127.0.0.1:8888/v1/order/list?uid=123
发现报错空指针
1 | "content":"(/v1/order/list?uid=123 - 127.0.0.1:57933) runtime error: invalid memory address or nil pointer dereference\ngoroutine 57 [running]:\nruntime/debug.Stack()\n\tE:/app/Go/src/runtime/debug/stack.go:24 +0x65\ngithub.com/zeromicro/go-zero/rest/handler.RecoverHandler.func1.1()\n\tC:/Users/Administrator/go/pkg/mod/github.com/zeromicro/go-zero@v1.5.4/rest/handler/recoverhandler.go:16 +0x66\npanic({0x1f56680, 0x331f9d0})\n\tE:/app/Go/src/runtime/panic.go:884 +0x213\nexample.com/studyGoZero/apps/app/api/internal/logic.(*OrderListLogic).OrderList(0xc0002cfcf8, 0xc00011e380)\n\tE:/proj/gowork/studyGoZero/apps/app/api/internal/logic/orderlistlogic.go:31 |
- 虽然报错 了,但是可以知道整个服务已经是通的,因为现在并没有订单的任何数据
总结
- api层- 对外的BFF服务。接受来自客户端的请求,暴露HTTP接口。本次实例启动了api.go,并且连接了order和product的rpc
api/api.api
为具体的对我暴露的请求app\api\internal\logic
对应api/api.api
为对外暴露请求的具体逻辑,具体代码又调用了order和product的rpc的逻辑