Go语言工程实践课后作业 | 青训营

93 阅读3分钟

前言

本篇笔记主要根据后端青训营Go语言工程实践篇课程项目实践内容中实现的展示话题和回帖列表功能基础上,实现发布帖子功能以及避免Map的并发安全问题。

正文

实现思路

发布帖子功能

需要明确的是,该示例中帖子强依附于话题存在。因此发布帖子时需要传入的参数包括话题Id以及帖子内容,首先根据Map以及话题Id查询出该话题下已发布的帖子列表。根据传入的帖子内容构建对象并添加到查询出的帖子列表中。如果考虑持久化,则应该将该条帖子内容写入到本地文件中。

本地Id保证不重复、唯一性

关于解决Id可能重复或不唯一的问题时,一个容易想到的思路是根据时间戳生成随机数。但如果将随机数范围设置的过大则存储成本高,设置的不够大则仍然可能出现Id重复的情况。

基于上述考虑,可以设置全局的本地id值,例如以1开始,在创建新帖的逻辑中对id值进行自增操作。这样在单协程情况下可以保证唯一性,但时需要考虑并发安全问题,多用户同时发帖时可能会出现修改覆盖问题,导致id重复。此时可以考虑对id自增逻辑的代码加并发悲观锁,同一时间只有单个协程能拥有锁并对id进行自增。若并发情况不那么频繁,也可以考虑通过基于乐观锁实现的CAS机制保证id更新操作的原子性。

解决并发更新Map时可能出现的安全问题

Go语言中的Map并发操作时可能出现数据竞争问题,导致程序抛出panic。这一点类似于数据库的并发问题,一个协程如果读取有其他协程在写入的Map时,可能读取到过期、不正确或空值。多个协程对同一Map执行写入操作时可能出现写入丢失、覆盖等情况。

想要实现Map安全并发安全主要有两种思路:

  1. 使用读写互斥锁。读操作不用加锁,写操作前加锁,写操作后释放锁。
  2. 使用Go语言提供的并发安全的sync.Map。

前者由于写操作会出发互斥锁导致性能有一定下降;后者通过分段锁实现并发安全,缩小加锁范围,对性能影响小。

代码

发布帖子

// router
r.POST("/post", controller.PublishPost)
​
// controller
func PublishPost(c *gin.Context) {
    topicIdStr, _ := c.GetPostForm("topicId")
    topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
    if err != nil {
        c.JSON(400, gin.H{
            "msg": "非法格式",
        })
    }
    content, _ := c.GetPostForm("content")
    err = service.PublishPost(topicId, content)
    if err != nil {
        c.JSON(400, gin.H{
            "msg": "话题不存在",
        })
    }
    data := service.QueryPageInfo(topicId)
    c.JSON(http.StatusOK, data)
}
​
// service
var pIdLock sync.Mutex
​
func PublishPost(topicId int64, content string) error {
    _, err := dao.TopicIndexMap.Get(topicId)
    if !err {
        return errors.New("话题不存在")
    }
    post := repository.Post{dao.PId.Start, topicId, content, time.Now()}
    // 更新PId时加锁进行并发控制
    pIdLock.Lock()
    dao.PId.Start++
    pIdLock.Unlock()
    dao.AddPostByTopicId(topicId, &post)
    return nil
}
​
// dao
func AddPostByTopicId(topicId int64, post *repository.Post) {
    tempList, err := PostIndexMap.Get(topicId)
    if !err {
        tempList = []*repository.Post{}
    }
    tempList = append(tempList, post)
    PostIndexMap.Set(topicId, tempList)
}

并发Map

上述为加了并发锁的TopicMap,PostMap同理。在原生map的增删改查操作前后加锁进行并发控制。

type MutexTopicMap struct {
    lock sync.Mutex
    M    map[int64]*repository.Topic
}
​
func (receiver *MutexTopicMap) Get(key int64) (*repository.Topic, bool) {
    receiver.lock.Lock()
    value, ok := receiver.M[key]
    receiver.lock.Unlock()
    return value, ok
}
​
func (receiver *MutexTopicMap) Set(key int64, value *repository.Topic) {
    receiver.lock.Lock()
    receiver.M[key] = value
    receiver.lock.Unlock()
}
func (receiver *MutexTopicMap) Del(key int64) {
    receiver.lock.Lock()
    delete(receiver.M, key)
    receiver.lock.Unlock()
}
​
type MutexPostMap struct {
    lock sync.Mutex
    M    map[int64][]*repository.Post
}

测试

  1. 发布帖子(成功)

发布帖子(成功).png

  1. 发布帖子(Topic不存在)

发布帖子(失败).png

结语

并发问题往往有多种实现手段,需要根据具体业务特点选择实现方式。