• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
scenery_coder
帮助1

系列文章目录

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



学新通


注:

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

使用MongoDB数据持久化

前言

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

go使用mongodb

  1. 在项目中获取依赖

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

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

  1. 连接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 = "***********************************************************"

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() {
   
}
学新通
  1. 使用上一章的数据初始化数据库
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设计

  1. 把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,
   }
}
  1. 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是内存数据库,比磁盘数据库的调用速度更快,有个小的系统开销。

学新通

查询的情况

  1. 查询缓存,得到数据,Cache hit
  2. 查询缓存没有数据,Cache miss 请求数据库数据
  3. 返回数据给客户端,并将其缓存到本地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-cli
  • EXISTS 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(测试的时间平均数)

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgkcebh
系列文章
更多 icon
同类精品
更多 icon
继续加载