这是我参与「第五届青训营 」笔记创作活动的第6天
一、本堂课重点内容
本节课主要讲解一个笔记项目,主要包括项目介绍,功能介绍,关键代码讲解,由于项目代码庞大,所以在关键代码讲解部分,主要介绍creat API来串起三个框架。 项目地址:
其中优化版使用了统一协议,并且使用了更多的扩展。
项目介绍
笔记项目是一个使用Hertz、Kitex、Grom搭建出来的具备一定业务逻辑的后端API项目。
| 服务名称 | 服务介绍 | 传输协议 | 主要技术栈 |
|---|---|---|---|
| demoapi | API服务 | HTTP | Kitex/Hertz |
| demouser | 用户数据管理 | Protobuf(pb) | Gorm/Kitex |
| demonote | 笔记数据管理 | Thrift | Gorm/Kitex |
表格中三个服务分别使用了不同的传输协议(普通版),仅是为了进行差别演示,实际开发中也可以使用同种协议。
功能介绍
- api服务要对外提供 api 接口,通过协议供前端或后端调用,完成相应功能。
- user服务是一个 RPC 服务,主要提供查询、创建、校验用户的接口。
- note是提供笔记的 CRUD 的基础能力,也是一个RPC服务
调用关系
- 首先user服务和note服务在启动时会通过服务注册把自己的信息注册到etcd中,并且只由这两个服务访问数据库。
- 调用方向 api 服务发送HTTP请求,api 服务通过服务发现调用相应的服务,当需要用户服务时便调用user服务,需要笔记服务时便调用note服务,然后在api服务中对获取的数据做组装。
- api服务不直接访问MySQL,这样设计的一个好处是,当需要再创建admin服务时,可以复用一些代码接口。
IDL介绍
idl定义结束后,使用Kitex进行代码生成。
user IDL介绍
user的idl使用proto3(pb)来定义。以下为user.proto文件内容。
syntax = "proto3";
package user;
option go_package = "userdemo";
message BaseResp {
int64 status_code = 1;
string status_message = 2;
int64 service_time = 3;
}
message User {
int64 user_id = 1;
string user_name = 2;
string avatar = 3;
}
message CreateUserRequest {
string user_name = 1;
string password = 2;
}
message CreateUserResponse {
BaseResp base_resp = 1;
}
message MGetUserRequest { //批量获取请求
repeated int64 user_ids = 1;
}
message MGetUserResponse { //批量获取相应
repeated User users = 1;
BaseResp base_resp = 2;
}
message CheckUserRequest{ //校验用户信息请求
string user_name = 1;
string password = 2;
}
message CheckUserResponse{
int64 user_id = 1;
BaseResp base_resp = 2;
}
service UserService { //对外提供RPC接口
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) {}
rpc MGetUser (MGetUserRequest) returns (MGetUserResponse) {}
rpc CheckUser (CheckUserRequest) returns (CheckUserResponse) {}
}
note IDL介绍
note idl使用thrift定义。以下为note.thrift文件内容。
namespace go notedemo
struct BaseResp {
1:i64 status_code
2:string status_message
3:i64 service_time
}
struct Note {
1:i64 note_id
2:i64 user_id
3:string user_name
4:string user_avatar
5:string title
6:string content
7:i64 create_time
}
struct CreateNoteRequest {
1:string title
2:string content
3:i64 user_id
}
struct CreateNoteResponse {
1:BaseResp base_resp
}
struct DeleteNoteRequest {
1:i64 note_id
2:i64 user_id
}
struct DeleteNoteResponse {
1:BaseResp base_resp
}
struct UpdateNoteRequest {
1:i64 note_id
2:i64 user_id
3:optional string title
4:optional string content
}
struct UpdateNoteResponse {
1:BaseResp base_resp
}
struct MGetNoteRequest { //批量查找请求
1:list<i64> note_ids
}
struct MGetNoteResponse { //批量查找响应
1:list<Note> notes
2:BaseResp base_resp
}
struct QueryNoteRequest {
1:i64 user_id
2:optional string search_key
3:i64 offset
4:i64 limit
}
struct QueryNoteResponse {
1:list<Note> notes
2:i64 total
3:BaseResp base_resp
}
service NoteService { //对外提供RPC接口
CreateNoteResponse CreateNote(1:CreateNoteRequest req)
MGetNoteResponse MGetNote(1:MGetNoteRequest req)
DeleteNoteResponse DeleteNote(1:DeleteNoteRequest req)
QueryNoteResponse QueryNote(1:QueryNoteRequest req)
UpdateNoteResponse UpdateNote(1:UpdateNoteRequest req)
}
项目技术栈
关键代码介绍
关键代码讲解部分以普通版代码为例。当用户创建新笔记时,会产生一系列的函数调用,以下将按照调用次序介绍各个函数。Hertz 调用 Kitex Client(rpc.CreateNote)创建笔记,Client调用下游的Kitex Server(noteClient.CreateNote)创建笔记,Server通过调用Grom(db.CreateNote)创建笔记,Grom对数据库操作(DB.WithContext(ctx).Create(notes).Error)写入笔记。
Hertz关键代码
对Hertz生成的代码进行修改。HTTP框架Hertz主要做的是对接口或数据进行聚合(比如聚合note和user数据),然后对外提供API服务。文件路径:easy_note/cmd/api/handlers/create_note.go
package handlers
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/cmd/api/rpc"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/errno"
"github.com/hertz-contrib/jwt"
)
// CreateNote create note info
func CreateNote(ctx context.Context, c *app.RequestContext) {
var noteVar NoteParam //声明结构体
if err := c.Bind(¬eVar); err != nil { //绑定结构体
SendResponse(c, errno.ConvertErr(err), nil) //反馈给前端绑定失败
return
}
if len(noteVar.Title) == 0 || len(noteVar.Content) == 0 { //参数校验
SendResponse(c, errno.ParamErr, nil) //标题和内容非法返回err
return
}
claims := jwt.ExtractClaims(ctx, c) //使用 jwt做用户数据处理
userID := int64(claims[constants.IdentityKey].(float64))
err := rpc.CreateNote(context.Background(), ¬edemo.CreateNoteRequest{
UserId: userID, //调用api创建新笔记
Content: noteVar.Content, Title: noteVar.Title,
})
if err != nil {
SendResponse(c, errno.ConvertErr(err), nil) //创建失败
return
}
SendResponse(c, errno.Success, nil) //创建成功
}
Kitex Client关键代码
使用RPC框架Kitex,创建Client,调用Server服务。文件路径:easy_note/cmd/api/rpc/note.go
package rpc
import (
"context"
"time"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo/noteservice"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/errno"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/middleware"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/retry"
etcd "github.com/kitex-contrib/registry-etcd"
trace "github.com/kitex-contrib/tracer-opentracing"
)
var noteClient noteservice.Client
func initNoteRpc() { //初始化Client
r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress}) //生成用于Client服务发现对象
if err != nil {
panic(err)
}
c, err := noteservice.NewClient( //封装 Client,定义服务名
constants.NoteServiceName,
client.WithMiddleware(middleware.CommonMiddleware), //注入中间件
client.WithInstanceMW(middleware.ClientMiddleware),
client.WithMuxConnection(1), // mux 用于I/O多路复用
client.WithRPCTimeout(3*time.Second), // rpc timeout
client.WithConnectTimeout(50*time.Millisecond), // conn timeout
client.WithFailureRetry(retry.NewFailurePolicy()), // retry
client.WithSuite(trace.NewDefaultClientSuite()), // tracer (分布式路径跟踪)
client.WithResolver(r), // resolver
)
if err != nil {
panic(err)
}
noteClient = c //赋值给全局对象
}
// CreateNote create note info
func CreateNote(ctx context.Context, req *notedemo.CreateNoteRequest) error {
resp, err := noteClient.CreateNote(ctx, req) //请求服务创建笔记
if err != nil {
return err //出现网络异常错误
}
if resp.BaseResp.StatusCode != 0 { //不等于0时 出现业务错误
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
// QueryNotes query list of note info
func QueryNotes(ctx context.Context, req *notedemo.QueryNoteRequest) ([]*notedemo.Note, int64, error) {
resp, err := noteClient.QueryNote(ctx, req)
if err != nil {
return nil, 0, err
}
if resp.BaseResp.StatusCode != 0 {
return nil, 0, errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return resp.Notes, resp.Total, nil
}
// UpdateNote update note info
func UpdateNote(ctx context.Context, req *notedemo.UpdateNoteRequest) error {
resp, err := noteClient.UpdateNote(ctx, req)
if err != nil {
return err
}
if resp.BaseResp.StatusCode != 0 {
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
// DeleteNote delete note info
func DeleteNote(ctx context.Context, req *notedemo.DeleteNoteRequest) error {
resp, err := noteClient.DeleteNote(ctx, req)
if err != nil {
return err
}
if resp.BaseResp.StatusCode != 0 {
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
Kitex Server关键代码
Server调用Grom创建笔记。文件路径:easy_note/cmd/note/service/create_note.go
package service
import (
"context"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/cmd/note/dal/db"
)
type CreateNoteService struct {
ctx context.Context
}
// NewCreateNoteService new CreateNoteService
func NewCreateNoteService(ctx context.Context) *CreateNoteService {
return &CreateNoteService{ctx: ctx}
}
// CreateNote create note info
func (s *CreateNoteService) CreateNote(req *notedemo.CreateNoteRequest) error {
noteModel := &db.Note{ //调用grom方法,kitex结构体与grom结构体可能不一致,所以需要转换
UserID: req.UserId,
Title: req.Title,
Content: req.Content,
}
return db.CreateNote(s.ctx, []*db.Note{noteModel}) //调用grom代码创建note
}
Grom关键代码
ORM框架对数据库操作。文件路径:easy_note/cmd/note/dal/db/note.go
package db
import (
"context"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"gorm.io/gorm"
)
type Note struct { //grom结构体
gorm.Model
UserID int64 `json:"user_id"`
Title string `json:"title"`
Content string `json:"content"`
}
func (n *Note) TableName() string { //表名
return constants.NoteTableName
}
// CreateNote create note info
func CreateNote(ctx context.Context, notes []*Note) error {
if err := DB.WithContext(ctx).Create(notes).Error; err != nil {
return err //使用Grom写入数据库,WithContext用来传递上下文,用于链路追踪等
}
return nil
}
// MGetNotes multiple get list of note info
func MGetNotes(ctx context.Context, noteIDs []int64) ([]*Note, error) {
var res []*Note
if len(noteIDs) == 0 {
return res, nil
}
if err := DB.WithContext(ctx).Where("id in ?", noteIDs).Find(&res).Error; err != nil {
return res, err
}
return res, nil
}
// UpdateNote update note info
func UpdateNote(ctx context.Context, noteID, userID int64, title, content *string) error {
params := map[string]interface{}{}
if title != nil {
params["title"] = *title
}
if content != nil {
params["content"] = *content
}
return DB.WithContext(ctx).Model(&Note{}).Where("id = ? and user_id = ?", noteID, userID).
Updates(params).Error
}
// DeleteNote delete note info 软删
func DeleteNote(ctx context.Context, noteID, userID int64) error {
return DB.WithContext(ctx).Where("id = ? and user_id = ? ", noteID, userID).Delete(&Note{}).Error
}
// QueryNote query list of note info
func QueryNote(ctx context.Context, userID int64, searchKey *string, limit, offset int) ([]*Note, int64, error) {
var total int64
var res []*Note
conn := DB.WithContext(ctx).Model(&Note{}).Where("user_id = ?", userID)
if searchKey != nil {
conn = conn.Where("title like ?", "%"+*searchKey+"%")
}
if err := conn.Count(&total).Error; err != nil {
return res, total, err
}
if err := conn.Limit(limit).Offset(offset).Find(&res).Error; err != nil {
return res, total, err
}
return res, total, nil
}
总结
easy_note 是使用 go语言写的一个微服务项目。并不是所有的服务都需要写成大单体服务,比如 HTTP框架 + 操作数据库,随着微服务的普及,我们可以把一些基础服务抽象出来,比如easy_note服务抽象出来note服务和user服务,这样做的好处是其他API也可以进行复用其中的微服务。