go-zero的基本使用
安装就不赘述了,照着官方文档 即可,装好Goctl和protoc
1. 创建API工程
goctl 工具创建工程分为两种,一种是api工程,一种是rpc工程,如下:
api:goctl api go -api user.api -dir .
rpc:goctl rpc proto -src user.proto -dir .
其中,rpc工程的创建依赖.proto文件,而api工程的创建依赖.api文件
1 mkdir -p user/api && cd user/api
用goland打开这个文件,创建go.mod
添加api文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 vim user.api type ( HelloReq { Name string `form:"name"` } HelloRes { Code int `json:"code"` Msg string `json:"msg"` } ) service user-api { @handler hello get /user/hello (HelloReq) returns (HelloRes) }
1 goctl api go -api user.api -dir .
可以看到生成了很多文件,文件结构看这里
2. 编写业务逻辑
user/api/internal/logic/hellologic.go
1 2 3 4 func (l *HelloLogic) Hello(req *types.HelloReq) (*types.HelloRes, error ) { msg := fmt.Sprintf("Hello %s" , req.Name) return &types.HelloRes{Code: 0 , Msg: msg}, nil }
3. model层使用
可以归纳为以下步骤:
执行命令生成model文件 goctl model xxx
config.go和yaml添加数据库和缓存的配置项
上下文中注入依赖 servicecontext.go
使用,从上下文中取出来用,l.svcCtx.XxxModel.Xxx ()
另外,model层可以简便的切换为Gorm,但是api层就很难切换为Gin了。
3.1 model生成
在数据库中创建一张user表
在user目录下创建model目录
执行命令生成
1 2 3 4 cd user/model goctl model mysql datasource -url="root:123456@tcp(127.0.0.1:3306)/go-zero-test" -table="user" -c -dir . # 也可以用sql文件 # goctl model mysql ddl -src user.sql -dir . -c
3.2 配置
添加mysql配置项 vim api/internal/config/config.go
1 2 3 4 5 6 7 8 9 10 11 12 13 package config import "github.com/tal-tech/go-zero/rest" type Config struct { rest.RestConf Mysql struct{ DataSource string } CacheRedis cache.CacheConf }
配置文件添加 vim api/etc/user-api.yaml
1 2 3 4 5 6 Mysql: DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: $host Pass: $pass Type: node
3.3 上下文注入model
api/internal/svc/servicecontext.go
1 2 3 4 5 6 7 8 9 10 11 12 type ServiceContext struct { Config config.Config UserModel model.UserModel } func NewServiceContext (c config.Config) *ServiceContext { conn:=sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ Config: c, UserModel: model.NewUserModel(conn,c.CacheRedis), } }
3.4 修改接口文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 syntax = "v1" type ( HelloReq { Name string `form:"name"` } HelloRes { Code int `json:"code"` Msg string `json:"msg"` } LoginReq { Username string `json:"username"` Password string `json:"password"` } LoginReply { Id int64 `json:"id"` Name string `json:"name"` Gender string `json:"gender"` AccessToken string `json:"accessToken"` AccessExpire int64 `json:"accessExpire"` RefreshAfter int64 `json:"refreshAfter"` } ) service user-api { @handler login post /api/user/login (LoginReq) returns (LoginReply) @handler hello get /api/user/hello (HelloReq) returns (HelloRes) }
1 goctl api go -api user.api -dir .
3.5 逻辑层使用model
api/internal/logic/loginlogic.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (l *LoginLogic) Login(req types.LoginReq) (*types.LoginReply, error ) { if len (strings.TrimSpace(req.Username)) == 0 || len (strings.TrimSpace(req.Password)) == 0 { return nil , errors.New("参数错误" ) } userInfo, err := l.svcCtx.UserModel.FindOneByName(req.Username) switch err { case nil : case model.ErrNotFound: return nil , errors.New("用户名不存在" ) default : return nil , err } if userInfo.Password != req.Password { return nil , errors.New("用户密码不正确" ) } return &types.LoginReply{ Id: userInfo.Id, Name: userInfo.Username, Gender: strconv.FormatInt(userInfo.Gender, 10 ), }, nil }
model自动生成的只有简单的增删改查,没有根据字段名的查询,这里可以自己实现FindOneByName
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 func (m *defaultUserModel) FindOneByName(username string ) (*User, error ) { var resp User query := "select * from user where username = ? limit 1;" err := m.QueryRowNoCache(&resp, query, username) switch err { case nil : return &resp, nil case sqlc.ErrNotFound: return nil , ErrNotFound default : return nil , err } }
4. jwt的使用
步骤归纳:
config.go和yaml添加Auth配置项
编写token生成函数
需要鉴权的接口在api文件中添加jwt:Auth声明
重新执行命令生成api代码
5. 中间件
步骤归纳:
api文件中添加middleware声明
重新执行命令生成api代码
上下文中注入依赖
编写中间件的Handle 处理逻辑
6. rpc服务
6.1 创建服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 vim user/rpc /user.proto syntax = "proto3" ;package user;option go_package = "user" ;message IdReq { int64 id = 1 ; } message UserInfoReply { int64 id = 1 ; string name = 2 ; string gender = 3 ; } service user { rpc getUser(IdReq) returns (UserInfoReply) ; }
1 2 cd service/user/rpc goctl rpc proto -src user.proto -dir .
1 2 3 4 Etcd: Hosts: - 127.0 .0 .1 :2379 Key: user.rpc
1 2 3 4 5 6 7 8 9 10 11 12 13 func (l *GetUserLogic) GetUser(in *user.IdReq) (*user.UserInfoReply, error ) { one, err := l.svcCtx.UserModel.FindOne(in.Id) if err != nil { return nil , err } return &user.UserInfoReply{ Id: one.Id, Name: one.Name, Number: one.Number, Gender: one.Gender, }, nil }
6.2 调用服务
可以归纳为以下步骤:
config.go和yaml添加rpc服务端的配置项
上下文中注入依赖
逻辑层可以从上下文中取出来调用
在调用端的config.go中加入rpc服务端的配置项
1 2 3 4 5 6 7 8 type Config struct { rest.RestConf Auth struct { AccessSecret string AccessExpire int64 } UserRpc zrpc.RpcClientConf }
1 2 3 4 5 UserRpc: Etcd: Hosts: - 127.0 .0 .1 :2379 Key: user.rpc
上下文注入依赖(servicecontext.go)
1 2 3 4 5 6 7 8 9 10 11 12 13 type ServiceContext struct { Config config.Config ... UserRpc userclient.User } func NewServiceContext (c config.Config) *ServiceContext { return &ServiceContext{ Config: c, ... UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), } }
1 2 3 4 5 6 7 8 9 func (l *PingLogic) Ping() error { fmt.Println("ping..." ) user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &userclient.IdReq{Id: 3 }) fmt.Println(user, err) fmt.Println("api调用rpc咯" ) return nil }
7. // 自适应降载保护
讲白了就是根据CPU压力来保护服务,压力过高时拒绝新的请求,直到当前积攒的请求处理完、CPU压力降下来后再次开放
但是目前我还测不出来,不知道是不是因为在windows上的原因,读取不到cpu使用率,从stat日志打出来的内容可以看到每次读取的cpu使用率都是0
在rest和zrpc框架里有可选激活配置
CpuThreshold,0-1000,默认值900,如果把值设置为大于0的值,则激活该服务的自动降载机制
如果请求被drop,那么错误日志里会有dropped
关键字
官方文档
8. 熔断
在gprc调用中已经内置了,无需额外编码
熔断的算法看官方文档
测试起来比较简单
一个api接口、一个rpc接口,api接口调用rpc
rpc接口内延时1秒,返回错误
连续请求api接口,会发现刚开始会等待1秒后才接收到响应,到后面已经是瞬间失败了,说明api接口没有再去调用rpc
9. 并发限制
RestConf中有有一个MaxConns配置用来限制并发数量
当程序中未处理完毕、未返回响应的请求数量超过此配置,后续请求将被直接拒绝
10. 引擎并发控制方案 - PeriodLimit
通过对zo-zero框架的了解,认为可以将其限流工具-PeriodLimit 作为引擎的并发控制方案之一
1 2 3 4 5 var ( seconds = 3 quota = 10 ) l := limit.NewPeriodLimit(seconds, quota, redis.NewRedis("127.0.0.1:6379" , redis.NodeType), "periodlimit" )
do函数假设是调用引擎的处理函数,而这个函数执行完毕需要耗时3秒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func do (l *limit.PeriodLimit, userID string , i int ) { code, err := l.Take(userID) if err != nil { logx.Error(err) } time.Sleep(time.Second*3 ) switch code { case limit.OverQuota: logx.Errorf("OverQuota key: %v" , i) case limit.Allowed: logx.Infof("AllowedQuota key: %v" , i) case limit.HitQuota: logx.Errorf("HitQuota key: %v" , i) default : logx.Errorf("DefaultQuota key: %v" , i) } }
1 2 3 for i := 0 ; i < 100 ; i++ { go do(l, "user1" , i) }
优点:
使用简单、开箱即用;
基于 redis
计数器,通过调用 redis lua script
,保证计数过程的原子性,同时也支持分布式情况下正常计数;
缺点: