Golang基础:第二次作业支持发帖 | 青训营笔记

65 阅读3分钟

Golang基础第二次作业-支持发帖 | 青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记

  1. 支持生成自增的帖子id,满足高并发场景
  2. 发帖时将帖子存入map也要考虑并发场景

实现:

1. 首先在原有的基础上,初始化repository时,记录当前帖子的最大id->maxID:

  • repository/db_init.go文件中:定义一个全局maxPostID

image.png

  • repository/db_init.go文件中:initPostIndexMap函数中记录最大maxPostID

image.png

  • 增加获取最大id的函数:GetMaxID
func GetMaxID() int64 {
   return maxPostId
}

2. 在controller层中增加发帖相关的实现

  • 新增put_post.go文件
  • 实现生成自增id相关的功能 自增id采用函数闭包形式,闭包函数持有外部变量的引用,每次调用都会对这个id自增,实现如下:
// 这里也可以换成其他方法 自由替换
func genPostIdFunc(currId int64) func() int64 {
   var start = currId   // currId是当前帖子Id的最大值
   return func() int64 {  // 返回一个变量自增的函数,每次调用都会++
      start++
      return start
   }
}
  • 为了满足高并发场景下的可用性,做了进一步的封装:
// 定义一个id生成器
type generator struct {
   genFunc func() int64 // 一个id生成函数
   lock    sync.Mutex   // 一个读写锁
}

// GenID 挂载一个生成ID的方法 调用自己内部的的genFunc 
func (p *generator) GenID() int64 {
   p.lock.Lock()  // 加锁解锁
   defer p.lock.Unlock()
   id := p.genFunc()
   return id
}
  • 暴露给外部调用时的实现:
// 定义全局的id生成器 
var ( 
   gen  *generator 
   once sync.Once
)

// InitGenerator 保证全局id生成器只被初始化一次
func InitGenerator() {
   once.Do(func() {
      gen = &generator{
         genFunc: genPostIdFunc(repository.GetMaxID()),  // 这里传入生成id的函数
         lock:    sync.Mutex{},
      }
   })
}

2.1 测试ID生成器的可用性:

  • 使用Testing进行测试:
// 单次测试
func TestGenID(t *testing.T) {
   InitGeneratorSimple(3) // 这里初始化当前 maxID 为3
   id := gen.GenID()  // 生成id  应该等于4
   expectedId := int64(4) 
   assert.Equal(t, expectedId, id)
}

结果:

image.png

  • 多次初始化Generator:

image.png

  • 高并发场景(加锁结果):

image.png

  • 高并发场景(不锁结果):

image.png

3.在controller层增加发帖的逻辑代码:

func PutPost(post *repository.Post) *PageData {
   if post.ParentId < 1 || post.ParentId > 2 {  // 假定就这两种社区id
      return &PageData{Code: -1, Msg: "no such community", Data: nil}
   }
   // 初始化帖子id和创建时间
   post.Id = gen.GenID()  // 可以安全的生成id了
   post.CreateTime = time.Now().Unix()
   if err := service.PutPost(post); err != nil {  // 调用service层的逻辑
      return &PageData{Code: -1, Msg: err.Error(), Data: nil}
   }

   return &PageData{Code: 1, Msg: "publish post success", Data: post.Id}
}

4. 增加service的代码逻辑:

  • post.go中个增加往map中存放post的方法:
func (pd *PostDao) PutPostToMap(post *Post) error {
   if pd != nil {
      pd.lock.Lock()  // 加锁应对并发
      posts := postIndexMap[post.ParentId] //根据社区id得到切片
      posts = append(posts, post) //追加
      postIndexMap[post.ParentId] = posts //再放回去
      pd.lock.Unlock()
      return nil
   }
   return errors.New("service busy")
}
  • Service层的put_post.go就可以这么写了:
package service

import (
   "errors"
   "github.com/Moonlight-Zhao/go-project-example/repository"
   "sync"
)

// PutPoster 往 map
type PutPoster struct {
   topicId int64
   postId  int64
   post    *repository.Post
}

// PutPost 真正被 controller调用的函数
func PutPost(post *repository.Post) error {
   return NewPutPoster(post).Do()
}

func NewPutPoster(post *repository.Post) *PutPoster {
   return &PutPoster{
      topicId: post.ParentId,
      postId:  post.Id,
      post:    post,
   }
}

func (p *PutPoster) Do() (err error) {
   var wg sync.WaitGroup
   wg.Add(2)
   if err = p.checkParam(); err != nil {
      return err
   }
   // 没有考虑其中一个写失败的情况
   go func() {
      defer wg.Done()
      err = p.putPostToMap() // 往map中存储
   }()
   go func() {
      defer wg.Done()
      err = p.putPostToLocal() // 往本地文件写
   }()
   wg.Wait()
   return err
}

func (p *PutPoster) checkParam() error {
   if p.topicId < 1 || p.topicId > 2 {
      return errors.New("topic id must be equals 1 or 2")
   }
   return nil
}

func (p *PutPoster) putPostToMap() error {
   return repository.NewPostDaoInstance().PutPostToMap(p.post)
}
func (p *PutPoster) putPostToLocal() error {  // 往本地文件写就不多说了
   return repository.NewLocalFileDaoInstance("../data/post").PutPostToLocal(p.post)
}

4.1 测试一波:

func TestPutPost(t *testing.T) {
   err := repository.Init("../data/") // 初始化map这些
   if err != nil {
      os.Exit(1)
   }
   InitGenerator()    // 初始化ID生成器
   curr := 13         // 当前已有的帖子数量
   goroutines := 5000 // 狗routine数量
   var wg sync.WaitGroup
   wg.Add(goroutines)
   // 并发执行
   for i := 0; i < goroutines; i++ {
      go func(i int) {
         defer wg.Done()
         var pid int64
         if i%2 == 0 { // 往不同的社区里写 单数写2 社区 双数写 1 社区
            pid = 1
         } else {
            pid = 2
         }

         // 执行controller的PutPost
         PutPost(&repository.Post{
            ParentId: pid,
            Content:  fmt.Sprintf("大家快来青训营%v", i),
         })
      }(i)
   }
   wg.Wait()
   // 实际帖子的数量为 5000 + 13
   expectedRes := goroutines + curr
   // 返回map中帖子的数量
   res := repository.NewPostDaoInstance().Size()
   assert.Equal(t, expectedRes, res)

   // 判断一下ID生成器是否还正常
   expectedId := int64(5014) // 再生成一次就是5014
   id := gen.GenID()
   assert.Equal(t, expectedId, id)
}

加锁的结果(5000goroutine):

image.png

不加锁的结果(3000gourotine):

image.png

4.2 Postman测试:

  • service.go添加路由:
r.POST("/community/page/post", func(c *gin.Context) {
   var jsonPost repository.Post
   c.ShouldBindJSON(&jsonPost)
   data := cotroller.PutPost(&jsonPost)
   c.JSON(200, data)
})
  • postman测试1:错误community_id

image.png

  • postman测试2:

image.png