ch3-使用MongoDB数据持久化--使用go-gin创建分布式应用

ch3-使用MongoDB数据持久化--使用go-gin创建分布式应用,第1张

系列文章目录

第一章 gin初步认识
第二章 设置API
第三章 使用MongoDB数据持久化


目录 系列文章目录使用MongoDB数据持久化前言go使用mongodbCRUD *** 作实例查找插入一条记录更新一条记录 设计项目的分层结构handler.go设计 使用redis缓存API查询的情况docker运行redis配置redis使用redis对数据缓存的代码直接查看redis缓存是否存在 网站性能跑分


注:

系列文章是对应上述英文原版书的学习笔记相关自己的练习代码包含注释,放在在本人的gitee,欢迎star所有内容允许转载,如果侵犯书籍的著作权益,请联系删除笔记持续更新中
使用MongoDB数据持久化 前言

本章将会用docker部署MongoDB和Redis,实现CRUD,介绍标准go项目目录结构,优化API响应提高网站性能

go使用mongodb 在项目中获取依赖

go get go.mongodb.org/mongo-driver/mongo

这将会下载驱动到系统GOPath,并把其作为依赖写入go.mod文件中

连接MongoDB docker运行mongodb

docker run -d --name mongodb -e MONGO_INITDB_ROOT_ USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -p 27017:27017 mongo:4.4.3

使用免费的mongo atlas数据库数据库的连接和测试代码
package main

import (
   "context"
   "fmt"
   "go.mongodb.org/mongo-driver/mongo"
   "go.mongodb.org/mongo-driver/mongo/options"
   "go.mongodb.org/mongo-driver/mongo/readpref"
   "log"
)

var ctx context.Context
var err error
var client *mongo.Client

// 使用环境变量定义的数据库地址
//var uri = os.Getenv("MONGO_URI")
var uri = "mongodb+srv://root:[email protected]/test?retryWrites=true&w=majority"

func init() {
   ctx = context.Background()
   client, err = mongo.Connect(ctx, options.Client().ApplyURI(uri))
   if err = client.Ping(context.TODO(), readpref.Primary()); err != nil {
      log.Fatal(err)
   }
   fmt.Println("Connected to MongoDB")
}
func main() {
   
}
使用上一章的数据初始化数据库
func init() {
   recipes = make([]model.Recipe, 0)
   // 读取文件中的信息
   file, _ := ioutil.ReadFile("recipes.json")
   // 把信息解析为recipe实体
   _ = json.Unmarshal([]byte(file), &recipes)

   ...

   // InsertMany传递的数据参数就是interface
   var listOfRecipes []interface{}
   for _, recipe := range recipes {
      listOfRecipes = append(listOfRecipes, recipe)
   }
   collection := client.Database(database_name).Collection(collection_name)
   insertManyResult, err := collection.InsertMany(ctx, listOfRecipes)
   if err != nil {
      log.Fatal(err)
   }
   log.Println("Inserted recipes: ", len(insertManyResult.InsertedIDs))
}

MongoDB在插入数据的时候,只要没创建,就会默认创建指定数据模型的库(在MongoDB中库叫做collection 集合,插入的记录叫做document 文档)

collection.InsertMany接收interface{}切片类型的数据,所以上面把recipes数组的数据循环拷贝到listOfRecipes的interface切片中

recipes.json文件有如下内容

[
    {
        "id": "c80e1msc3g21dn3s62e0",
        "name": "Homemade Pizza",
        "tags": [
            "italian",
            "pizza",
            "dinner"
        ],
        "ingredients": [
            "1 1/2 cups (355 ml) warm water (105°F-115°F)",
            "1 package (2 1/4 teaspoons) of active dry yeast",
            "3 3/4 cups (490 g) bread flour",
            "feta cheese, firm mozzarella cheese, grated"
        ],
        "instructions": [
            "Step 1.",
            "Step 2.",
            "Step 3."
        ],
        "PublishedAt": "2022-02-07T17:05:31.9985752+08:00"
    }
]

使用mongoimport导入序列化的数据(json文件),同时初始化表(collection)

mongoimport --username admin --password password  --authenticationDatabase admin --db demo --collection recipes  --file recipes.json --jsonArray
CRUD *** 作实例 查找
//  *** 作数据库的collection
collection = client.Database(database_name).Collection(collection_name)
func ListRecipesHandler(c *gin.Context) {
   // 获得 *** 作数据库的游标
   // cur其实是文档流
   cur, err := collection.Find(ctx, bson.M{})
   if err != nil {
      c.JSON(http.StatusInternalServerError,
         gin.H{"error": err.Error()})
      return
   }
   defer cur.Close(ctx)

   recipes := make([]model.Recipe, 0)
   for cur.Next(ctx) {
      var recipe model.Recipe
      // 将查询到的文档装配为Recipe结构体的实体
      cur.Decode(&recipe)
      recipes = append(recipes, recipe)
   }

   c.JSON(http.StatusOK, recipes)
}
插入一条记录
func NewRecipesHandler(c *gin.Context) {
   var recipe model.Recipe
   // 获取并解析POST请求消息体传递过来的数据
   if err := c.ShouldBindJSON(&recipe); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{
         "error": err.Error()})
      return
   }

   recipe.ID = primitive.NewObjectID()
   recipe.PublishedAt = time.Now()
   _, err := collection.InsertOne(ctx, recipe)
   if err != nil {
      fmt.Println(err)
      c.JSON(http.StatusInternalServerError, gin.H{
         "error": "Error while inserting a new recipe"})
      return
   }
   c.JSON(http.StatusOK, recipe)
}

修改ID字段的类型为primitive.Object,并为结构体的字段加上bson注解

// swagger: parameters recipes newRecipe
type Recipe struct {
   // swagger:ignore
   ID           primitive.ObjectID `json:"id" bson:"_id"`
   Name         string             `json:"name" bson:"name"`
   Tags         []string           `json:"tags" bson:"tags"`
   Ingredients  []string           `json:"ingredients" bson:"ingredients"`
   Instructions []string           `json:"instructions" bson:"instructions"`
   PublishedAt  time.Time          `json:"PublishedAt" bson:"publishedAt"`
}
更新一条记录
func UpdateRecipeHandler(c *gin.Context) {
   // 从上下文获得url传递的参数 host/recipes/{id}
   // 属于位置参数
   id := c.Param("id")
   var recipe model.Recipe
   // 从body获取数据后,
   if err := c.ShouldBindJSON(&recipe); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{
         "error": err.Error()})
      return
   }
   // 从ID string新建ObjectID实体
   objectId, _ := primitive.ObjectIDFromHex(id)
   _, err = collection.UpdateOne(ctx, bson.M{
      "_id": objectId}, bson.D{{"$set", bson.D{
      {"name", recipe.Name},
      {"instructions", recipe.Instructions},
      {"ingredients", recipe.Ingredients},
      {"tags", recipe.Tags}}}})
   if err != nil {
      fmt.Println(err)
      c.JSON(http.StatusInternalServerError, gin.H{
         "error": err.Error()})
      return
   }
   c.JSON(http.StatusOK, gin.H{"message": "Recipes has been updated"})
}
设计项目的分层结构

项目分目录和分文件便于管理,让代码清晰容易

models目录的recipe.go定义数据模型handlers目录的handler.go定义路由处理函数 handler.go设计 把handler函数需要的上下文和数据库连接作为结构体
type RecipesHandler struct {
   collection *mongo.Collection
   ctx        context.Context
}

// 获取handler处理需要的数据实体--上下文和数据库连接
func NewRecipesHandler(ctx context.Context, collection *mongo.Collection) *RecipesHandler {
   return &RecipesHandler{
      collection: collection,
      ctx:        ctx,
   }
}
RecipesHandler添加方法
func (handler *RecipesHandler) ListRecipesHandler(c *gin.Context) {}
main.go存放数据库认证和链接的代码
func init() {
   ctx := context.Background()
   client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
   if err = client.Ping(context.TODO(), readpref.Primary()); err != nil {
      log.Fatal(err)
   }
   log.Println("Connected to MongoDB")
   //  *** 作数据库的collection
   collection := client.Database(database_name).Collection(collection_name)
   recipesHandler = handlers.NewRecipesHandler(ctx, collection)
}

当前项目目录结构

MONGO_URI="mongodb://admin:password@localhost:27017/
test?authSource=admin" MONGO_DATABASE=demo go run *.go

用命令行传递变量参数,然后在程序中获取

var uri = os.Getenv("MONGO_URI")
使用redis缓存API

下面讲解怎么添加redis缓存机制到API

一般在程序运行起来后,经常查询的数据只占数据库全部数据的少部分。将获取的数据缓存到如redis这种内存缓存数据库中,可以避免每次都请求数据库调用,极大地降低数据库查询的负载,提高查询。另一方面,redis是内存数据库,比磁盘数据库的调用速度更快,有个小的系统开销。

查询的情况 查询缓存,得到数据,Cache hit查询缓存没有数据,Cache miss 请求数据库数据返回数据给客户端,并将其缓存到本地cache docker运行redis

docker run -d --name redis -p 6379:6379 redis:6.0

查看日志

docker logs -f <容器ID>

编辑redis.conf定义数据置换算法

使用LRU算法 最近最少使用算法

maxmemory-policy allkeys-lru
maxmemory 512mb

为了把配置文件映射到本地,将启动docker容器命令改为

docker run -v D:/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf --name redis_1 -p 6379:6379 redis:6.0 redis-server /usr/local/etc/redis/redis.conf

配置redis
import "github.com/go-redis/redis"
main.go的init()函数里面配置redis
redisClient := redis.NewClient(&redis.Options{
   Addr:     "localhost:6379",
   Password: "",
   DB:       0,
})
status := redisClient.Ping()
fmt.Println(status)
修改handler.go
type RecipesHandler struct {
   collection  *mongo.Collection
   ctx         context.Context
   redisClient *redis.Client
}

// 获取handler处理需要的数据实体--上下文和数据库连接
func NewRecipesHandler(ctx context.Context, collection *mongo.Collection, redisClient *redis.Client) *RecipesHandler {
   return &RecipesHandler{
      collection:  collection,
      ctx:         ctx,
      redisClient: redisClient,
   }
}
使用redis对数据缓存的代码
func (handler *RecipesHandler) ListRecipesHandler(c *gin.Context) {
   var recipes []models.Recipe
   val, err := handler.redisClient.Get("recipes").Result()
   // 如果抛出的错误是redis没有这个数据
   if err == redis.Nil {
      log.Printf("Request to MongoDB")
      // 获得 *** 作数据库的游标
      // cur其实是文档流
      cur, err := handler.collection.Find(handler.ctx, bson.M{})
      if err != nil {
         c.JSON(http.StatusInternalServerError,
            gin.H{"error": err.Error()})
         return
      }
      defer cur.Close(handler.ctx)

      recipes = make([]models.Recipe, 0)
      for cur.Next(handler.ctx) {
         var recipe models.Recipe
         // 将查询到的文档装配为Recipe结构体的实体
         cur.Decode(&recipe)
         recipes = append(recipes, recipe)
      }
      // 把新查到的数据已键值对的方式存入redis
      data, _ := json.Marshal(recipes)
      handler.redisClient.Set("recipes", string(data), 0)
   } else if err != nil {
      c.JSON(http.StatusInternalServerError, gin.H{
         "error": err.Error()})
   } else {
      log.Printf("Request to Redis")
      recipes = make([]models.Recipe, 0)
      json.Unmarshal([]byte(val), &recipes)
   }
   c.JSON(http.StatusOK, recipes)

}
直接查看redis缓存是否存在 打开redis-cliEXISTS recipes

使用web工具查看redis缓存

docker run -d --name redisinsight --link redis_1 -p 8001:8001 redislabs/redisinsight

网站性能跑分

apache2-utils中的软件ab用于网站压力测试(windows版本是Apache httpd这个软件)

ab.exe -n 2000 -c 100 -g without-cache.data http://localhost:8080/recipes

2000次请求,100每次的并发

将得到如下的结果:

关闭缓存-g without-cache.data

打开使用redis缓存-g with-cache.data

关注Time taken for tests(完成所有测试的用时)和Time per request(测试的时间平均数)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/995643.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存