第一章 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使用免费的mongo atlas数据库数据库的连接和测试代码docker run -d --name mongodb -e MONGO_INITDB_ROOT_ USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -p 27017:27017 mongo:4.4.3
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
用命令行传递变量参数,然后在程序中获取
使用redis缓存APIvar uri = os.Getenv("MONGO_URI")
下面讲解怎么添加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容器命令改为
配置redisdocker 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
在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
(测试的时间平均数)
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)