课后实践要求
- 支持发布帖子
- 本地ID生成不重复,保证唯一
- Append文件,更新索引,注意
map并发安全问题
实现笔记
由于另外两个需求都依赖于索引,所以我们从第三点的索引开始实现。
首先解决map并发安全问题,已知有如下两种解决方法:
- 对原有
map加读写锁 - 使用
sync.Map代替map
这里使用第二种方法,对原有initTopicIndexMap和initPostIndexMap函数进行修改:
- 定义部分:
var (
topicIndexMap sync.Map
postIndexMap sync.Map
)
- 函数实现
这里实现上需要修改的只有两个函数里的如下部分:
func initXxxxIndexMap(filePath string) error {
...
xxxxTmpMap := make(map[int64]*Xxxx)
for scanner.Scan() {
...
xxxxTmpMap[xxxx.Id] = &xxxx
}
xxxxIndexMap = xxxxTmpMap
...
}
改为如下即可:
func initXxxxIndexMap(filePath string) error {
...
xxxxIndexMap.Clear()
for scanner.Scan() {
...
xxxxIndexMap.Store(xxxx.Id, &xxxx)
}
...
}
- 修改
TopicDao和PostDao下相关使用
func (*TopicDao) QueryTopicById(id int64) *Topic {
if topic, ok := topicIndexMap.Load(id); ok {
return topic.(*Topic)
}
return nil
}
func (*PostDao) QueryPostByTopicId(id int64) []*Post {
var postList []*Post
postIndexMap.Range(func(k, v interface{}) bool {
post := v.(*Post)
if post.TopicId == id {
postList = append(postList, post)
}
return true
})
return postList
}
至此第三点就已经解决了,然后考虑ID生成。
这里使用一种简单的很原始的方法(bushi):
- 首先可以想到一种简单的不考虑唯一性的做法:生成一个固定位数的随机数
- 然后我们已知topicIndexMap和postIndexMap的结构都为
ID -> Entry - 考虑利用topicIndexMap和postIndexMap保证唯一性
然后可以写出以下睿智代码:
func GenerateID(ids *sync.Map) int64 {
for {
rd := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
id := rd.Int63n(90000000) + 10000000
if _, ok := ids.Load(id); !ok {
return id
}
}
}
最后我们实现帖子发布,这里主要对repository层讨论(文件操作部分)
和构建索引部分读文件流程基本一致,区别在于我们写操作应该是追加操作。
这里使用 os.OpenFile,并为其设置 os.O_APPEND flag即可。
如下:
open, err := os.OpenFile(topicFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
整个流程即为:生成ID -> 设置发布时间戳 -> 反序列化 -> 追加文件 -> 更新索引 -> 关闭文件。
完整代码如下:
func (*TopicDao) CreateNewTopic(topic *Topic) error {
topic.Id = utils.GenerateID(&topicIndexMap)
topic.CreateTime = time.Now().UnixNano()
jsonBytes, err := json.Marshal(&topic)
if err != nil {
return err
}
jsonStr := string(jsonBytes) + "\n"
open, err := os.OpenFile(topicFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer func(open *os.File) {
err := open.Close()
if err != nil {
return
}
}(open)
if _, err := open.WriteString(jsonStr); err != nil {
return err
}
postIndexMap.Store(topic.Id, topic)
return nil
}
新建post同理,代码基本一致。
然后在service层添加对传入参数的校验检查,controller层包装一下返回数据,然后暴露http接口即可。
r.POST("/community/topic/create", func(c *gin.Context) {
var jsonData controller.TopicRequest
if err := c.ShouldBind(&jsonData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
data := controller.CreateTopic(jsonData)
c.JSON(http.StatusOK, data)
})
需要注意的是post的service层参数校验时应该检查TopicId是否合法(帖子的存在性)。