微服务
打通GRPC后我们需要划分后端的微服务
传统模式是一整个大的服务,微服务是将其划分为多个小的服务,特性如下:
- 快速开发/迭代
- 自动化部署
- 独立部署
- 错误隔离
- 负载均衡
- 出错处理
- 链路跟踪
- 服务发现
- 服务治理
- 动态扩容
- 熔断,降级
微服务的初衷是想实现快速开发、独立部署和错误隔离,但是将一个服务拆分为多个微服务会提高系统的复杂性,因此需要自动化部署、负载均衡、出错处理、链路跟踪、服务发现、服务治理,在此基础上可以相对方便的实现动态扩容和熔断降级。
微服务划分
微服务的划分:分的不够细等服务庞大后再分就比较困难了,分的太细了后续再整合服务也很困难,划分步骤:
- 划分领域
- 确定上下文边界
- 上下文映射
- 确定微服务边界
租辆酷车微服务划分,每个虚框是一个微服务
登陆服务
删除server下的文件只保留go.mod和go.sum,新建auth目录
小程序登录文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
开发者服务器:就是我们要做的登录微服务
appid:建立小程序的id
appid+appsecret:认证开发者
openId:针对小程序,每个用户的openId都不同
unionId:针对开发者或企业,同一个开发者的所有小程序中的同一个用户的unionId都相同
自定义登录态:用户token,通过token可以查询用户的openid,token可以过期,过期之后需要重新获取code
登录微服务就是实现这个流程
编写proto生成代码
首先要先写proto,写完proto才能生成前后端的代码
auth目录下新建api目录,api下新建auth.proto
- code:登录请求的code
- access_token:返回给用户的token
- expires_in:token过期时间
syntax = "proto3";
package auth.v1;
option go_package = "coolcar/auth/api/gen/v1;authpb";
message LogiRequest {
string code = 1;
}
message LoginResponse {
string access_token = 1;
int32 expires_in = 2;
}
service authService{
rpc Login (LoginRequest) returns (LoginResponse);
}
api目录下新建auth.yaml
body: “*” 表示body中会携带我们的请求(code)
type: google.api.service
config_version: 3
http:
rules:
- selector: auth.v1.authService.Login
post: /v1/auth/login
body: "*"
server目录下新建脚本执行;
(注意这里有一个坑,我用的是windows系统在使用以下命令生成js文件首部添加’import…'时再生成ts文件会报错,考虑是乱码问题,这里可以直接用生成的js文件生成ts文件,再手动添加import到js文件中;还有其他个别错误可以不使用变量直接把路径复制到命令中执行)
$PROTO_PATH="./auth/api"
$GO_OUT_PATH="./auth/api/gen/v1"
mkdir -p $GO_OUT_PATH
protoc -I =$PROTO_PATH --go_out=plugins=grpc,paths=source_relative:$GO_OUT_PATH auth.proto
protoc -I =$PROTO_PATH --grpc-gateway_out=paths=source_relative,grpc_api_configuration=$PROTO_PATH/auth.yaml:$GO_OUT_PATH auth.proto
$PBTS_BIN_DIR="../wx/miniprogram/node_modules/.bin"
$PBTS_OUT_DIR="../wx/miniprogram/service/proto_gen/auth"
mkdir -p $PBTS_OUT_DIR
# 生成js文件
../wx/miniprogram/node_modules/.bin/pbjs -t static -w es6 $PROTO_PATH/auth.proto --no-create --no-encode --no-decode --no-verify --no-delimited -o $PBTS_OUT_DIR/auth_pb_tmp.js
echo 'import * as $protobuf from "protobufjs";\n' > $PBTS_OUT_DIR/auth_pb.js
cat $PBTS_OUT_DIR/auth_pb_tmp.js >> $PBTS_OUT_DIR/auth_pb.js
rm $PBTS_OUT_DIR/auth_pb_tmp.js
# 生成ts文件
../wx/miniprogram/node_modules/.bin/pbts -o $PBTS_OUT_DIR/auth_pb.d.ts $PBTS_OUT_DIR/auth_pb.js
需要把auth_pb.js也交到代码库,修改gitignore
!**/wx/miniprogram/service/proto_gen/**/*.js
实现服务
在auth(微服务)目录下新建auth(处理逻辑)目录,再新建auth.go,实现AuthServiceServer
auth.pb.go中的代码如下:
实现接口:
使用zap打日志,go get go.uber.org/zap
auth.go
package auth
import (
"context"
authpb "coolcar/auth/api/gen/v1"
"go.uber.org/zap"
)
type Service struct {
Logger *zap.Logger
}
func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) {
s.Logger.Info("received code", zap.String("code", req.Code))
return &authpb.LoginResponse{AccessToken: "token for " + req.Code, ExpiresIn: 7200}, nil
}
启动grpc服务
auth微服务目录下新建main.go,启动一个grpc服务
package main
import (
authpb "coolcar/auth/api/gen/v1"
"coolcar/auth/auth"
"log"
"net"
"go.uber.org/zap"
"google.golang.org/grpc"
)
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger: %v", err)
}
lis, err := net.Listen("tcp", ":8081")
if err != nil {
logger.Fatal("cannot listen", zap.Error(err))
}
s := grpc.NewServer()
authpb.RegisterAuthServiceServer(s, &auth.Service{Logger: logger})
err = s.Serve(lis)
logger.Fatal("cannot server", zap.Error(err))
}
启动grpc gateway服务
在server下新建gateway目录新建main.go,启动一个grpc gateway服务
package main
import (
"context"
authpb "coolcar/auth/api/gen/v1"
"log"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
)
func main() {
c := context.Background()
c, cancel := context.WithCancel(c)
defer cancel()
mux := runtime.NewServeMux(runtime.WithMarshalerOption(
runtime.MIMEWildcard, &runtime.JSONPb{
EnumsAsInts: true,
OrigName: true,
},
))
err := authpb.RegisterAuthServiceHandlerFromEndpoint(
c,
mux,
"localhost:8081",
[]grpc.DialOption{grpc.WithInsecure()},
)
if err != nil {
log.Fatalf("cannot register auth service: %v", err)
}
log.Fatal(http.ListenAndServe(":8080", mux))
}
小程序端发送请求
在小程序端app.ts发送登录请求
wx.login({
success: res => {
wx.request({
url: 'http://localhost:8080/v1/auth/login',
method: 'POST',
data: {
code: res.code,
} as auth.v1.ILoginRequest,
success: console.log,
fail: console.error,
})
// 发送 res.code 到后台换取 openId, sessionKey, unionId
},
})
成功将code发送到了对应的服务,并获得了返回
同时auth.Service也打出了日志
重新格式化一下success的日志,直接使用consolo.log的输出为:
格式化输出为:
success: res => {
const loginResp: auth.v1.ILoginResponse = auth.v1.LoginResponse.fromObject(camelcaseKeys(res.data as object))
console.log(loginResp)
},
获取openid
在服务实现中将其抽象为一个接口
实现接口
需要向微信接口发送请求获取openid,有两种方法,一种是使用go的httpclient直接发送请求(麻烦)
另一种使用微信go语言客户端,先安装第三方库go get github.com/medivhzhan/weapp/v2
package wechat
import (
"fmt"
"github.com/medivhzhan/weapp/v2"
)
type service struct {
Appid string
AppSecret string
}
func (s *service) Resolve(code string) (string, error) {
resp, err := weapp.Login(s.Appid, s.AppSecret, code)
if err != nil {
return "", fmt.Errorf("weapp.Login: %v", err)
}
if err := resp.GetResponseError(); err != nil {
return "", fmt.Errorf("weapp response error: %v", err)
}
return resp.OpenID, nil
}
appid:project.config.json中的appid
appsecret:在微信公众平台的开发管理中获取
但是我们不能直接将这个id传入Login方法,这样就直接写死了,需要进行配置化
接下来配置appid和secret
我们将appid定义在启动grpc服务的main.go文件中,这里面的值都是搭环境的一些代码,所有配置的参数我们在后期都会进行配置化通过命令行参数或环境变量的方法让外界进行提供。
AppSecret作为用户身份的象征不能明文存放,用session_key进行加密
4.3 push github
MongoDB数据库
sql vs no-sql
sql:MySQl,PostgreSQL
优点:
- 成熟
- 熟悉
- 丰富的生态
- 一致性保证(事务)
缺点:
- 性能欠缺(保证一致性)
- Object-Relational Mapping
用途:
- 遗留系统
- ToB系统
no-sql:MongoDB,HBase,ElasticSearch
MongoDB优点:
- 以JSON文档的形式存储
- 丰富的查询能力
- 性能
MongoDB缺点:
- 事务支持差
- 不能Join
用途:
- 快速开发
- ToC系统
- Serverless云开发的宠儿
- Firebase
- Leancloud
- 腾讯云开发
使用docker启动MongoDB的启动
拉取镜像:docker pull ccr.ccs.tencentyun.com/coolcar/coolenv:latest
启动容器:docker run -p 18000:18000 -p 27017:27017 -e ICODE=J3295B0D5848C6421 ccr.ccs.tencentyun.com/coolcar/coolenv
(这里的ICODE是慕课网的动态验证码,这里拉取的是课程提供的镜像)
安装vscode插件
添加MongoDB的连接
![](https://i-blog.csdnimg.cn/blog_migrate/69ab48c7de26318152096dbd41eae064.png)
可以新建一个playground执行测试一下
新建了一个test数据库,向sales表插入了一些数据,又从sales中查询了一些数据,又执行了一个聚合查询
这里插入的数据为json形式,没有任何具体的定义,只要是json文档就能存
这里每次操作前都需要使用use()选择数据库,可以使用copy connect string,删除这个链接再建立通过connection string连接,在之后加上我们要操作的数据库名,在这个连接操作就无需每次都选择数据库