Golang基础第二次作业-支持发帖 | 青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记
- 支持生成自增的帖子id,满足高并发场景
- 发帖时将帖子存入map也要考虑并发场景
实现:
1. 首先在原有的基础上,初始化repository时,记录当前帖子的最大id->maxID:
repository/db_init.go
文件中:定义一个全局maxPostID
repository/db_init.go
文件中:initPostIndexMap
函数中记录最大maxPostID
- 增加获取最大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)
}
结果:
- 多次初始化Generator:
- 高并发场景(加锁结果):
- 高并发场景(不锁结果):
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):
不加锁的结果(3000gourotine):
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
- postman测试2: