开发日志
这是在搭建好基础框架后进行的项目优化。文档记录了每次代码优化的详细过程。🎉🎉🎉

2025.05.26
优化内容
Fixed
- 修复了从缓存获取不到 article_views,直接 return 的问题; max() 保证了在 cacheViews 比 article.Views 小的情况下,至少返回 DB 中的 Views
- 修复了当Cache中article消失,重新加载时会把 Cache 中 article_views 覆盖为旧值的问题
- 使用 HIncrBy(key, field, 1) 代替
num:=Get() ---> num++ ---> Set(num)
代码部分
service/article.go ---> ArticleService.ArticleInfoByID()
// Before
views, err := ArticleFieldCache.Get("views", id)
if err == nil {
return article, errors.New("failed to get views")
}
article.Views = views
// Fixeds
if cacheViews, ok := ArticleFieldCache.Get("views", id); ok == nil {
article.Views = max(cacheViews, article.Views)
} else {
ArticleFieldCache.Set("views", id, article.Views)
}service/article_helpers.go ---> ArticleService.Get()
// Before
ArticleFieldCache.Set("views", id, a.Views)
// Fixed
if cacheViews, ok := ArticleFieldCache.Get("views", id); ok != nil {
ArticleFieldCache.Set("views", id, max(a.Views, cacheViews))
}2025.05.25
优化内容
Add
- 增加了结合文章 create_time, views, comments, likes 4个字段的综合排序评分算法,具体如下:
-
时间衰减因子:[ \text{timeDecay} = e^{-\frac{\text{hoursSinceCreation}}{2 \times 24 \times 7}} ],[hoursSinceCreation]为文章从创建时间到当前的小时数,[2 \times 24 \times 7]表示半衰期(14天)。
-
浏览数因子:[ \text{viewFactor} = \frac{\ln(1 + \text{views})}{10} ],[views]为文章的浏览次数,[ln(1 + x)]表示对数函数,用来平滑处理浏览数。
-
互动因子:[ \text{interactionFactor} = \frac{\ln(1 + 0.3 \times \text{comments} + 0.7 \times \text{likes})}{5} ],[comments]为文章的评论数,[likes]为文章的点赞数,[0.3]和[0.7]分别是评论和点赞的权重。[ln(1 + x)]对互动因子进行对数处理。
-
综合评分:[ \text{score} = \text{timeDecay} \times 0.4 + \text{viewFactor} \times 0.3 + \text{interactionFactor} \times 0.3 ]
-
确保评分在0到1之间:[ \text{finalScore} = \max(0, \min(1, \text{score})) ]
Fixed
- 修复了 Cache 中文章浏览量会被旧数据覆盖的问题
2025.05.24
优化内容
Add
- articleService.Get() 方法:加入了互斥锁,应对缓存击穿问题
Fixed
- 修复了了前端 文章浏览 页面样式
代码部分
service/article_helpers.go ---> ArticleService.Get()
// Get 用于通过ID从 Elasticsearch 获取文章 旁路缓存 缓存穿透
func (articleService *ArticleService) Get(id string) (elasticsearch.Article, error) {
var a elasticsearch.Article
// 先从缓存获取
if err := articleCacheDB.Get(id, &a); err == nil {
return a, nil // 缓存命中,直接返回
}
// 尝试获取或创建锁
lockInterface, _ := articleLocks.LoadOrStore(id, &sync.Mutex{})
lock := lockInterface.(*sync.Mutex)
lock.Lock()
defer lock.Unlock()
// 再次检查缓存,防止其他请求已经加载数据
if err := articleCacheDB.Get(id, &a); err == nil {
return a, nil // 缓存命中,直接返回
}
// 从 Elasticsearch 获取文章
res, err := global.ESClient.Get(elasticsearch.ArticleIndex(), id).Do(context.TODO())
if err != nil {
return elasticsearch.Article{}, err
}
if !res.Found {
return elasticsearch.Article{}, errors.New("Article not found")
}
// 将返回的源数据反序列化为 Article 对象
err = json.Unmarshal(res.Source_, &a)
if err != nil {
return elasticsearch.Article{}, err
}
// 新获取的文章缓存
articleCacheDB.Set(id, &a)
ArticleFieldCache.Set("views", id, a.Views)
return a, nil
}2025.05.23
优化内容
Add
- articleService.Get() 方法:加入了旁路缓存策略 + 延迟双删,应对高并发带来的 DB 负载增加和 DB-Cache 数据不一致问题
代码部分
service/article_helper.go
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"server/global"
"server/model/database"
"server/model/elasticsearch"
"server/utils"
"sync"
"time"
"github.com/elastic/go-elasticsearch/v8/typedapi/core/bulk"
"github.com/elastic/go-elasticsearch/v8/typedapi/core/search"
"github.com/elastic/go-elasticsearch/v8/typedapi/core/update"
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/refresh"
"gorm.io/gorm"
)
var (
// 用于同步加载文章的锁
articleLocks sync.Map
)
// Create 用于将文章创建到 Elasticsearch
func (articleService *ArticleService) Create(a *elasticsearch.Article) error {
// 将文章索引到Elasticsearch中,并设置刷新操作为 true
res, err := global.ESClient.Index(elasticsearch.ArticleIndex()).Request(a).Refresh(refresh.True).Do(context.TODO())
articleCacheDB.Set(res.Id_, a)
return err
}
// Delete 用于删除 Elasticsearch 中的文章,并实现延迟双删策略
func (articleService *ArticleService) Delete(ids []string) error {
// 第一次删除缓存,构建批量删除请求
var request bulk.Request
for _, id := range ids {
articleCacheDB.Delete(id)
request = append(request, types.OperationContainer{Delete: &types.DeleteOperation{Id_: &id}})
}
// 执行批量删除请求,并设置刷新操作为 true
_, err := global.ESClient.Bulk().Request(&request).Index(elasticsearch.ArticleIndex()).Refresh(refresh.True).Do(context.TODO())
if err != nil {
return err
}
// 延迟第二次删除缓存
go func() {
// 延迟一段时间(如1秒),等待可能的数据库主从同步完成
time.Sleep(200 * time.Millisecond)
for _, id := range ids {
articleCacheDB.Delete(id)
global.Log.Info(fmt.Sprintf("Delete article id:%s from cache", id))
}
}()
return nil
}
// Get 用于通过ID从 Elasticsearch 获取文章 旁路缓存策略
func (articleService *ArticleService) Get(id string) (elasticsearch.Article, error) {
var a elasticsearch.Article
// 先从缓存获取
if err := articleCacheDB.Get(id, &a); err == nil {
return a, nil // 缓存命中,直接返回
}
// 缓存未命中,从Elasticsearch获取文章
res, err := global.ESClient.Get(elasticsearch.ArticleIndex(), id).Do(context.TODO())
if err != nil {
return elasticsearch.Article{}, err
}
// 如果找不到该文档,则返回错误
if !res.Found {
return elasticsearch.Article{}, errors.New("document not found")
}
// 将返回的源数据反序列化为 Article 对象
err = json.Unmarshal(res.Source_, &a)
if err != nil {
return elasticsearch.Article{}, err
}
// 缓存新获取的文章(可选,根据业务需求)
articleCacheDB.Set(id, &a)
return a, nil
}
func (articleService *ArticleService) Update(articleID string, v any) error {
// 1. 先删除缓存(第一次删除)
articleCacheDB.Delete(articleID)
// 2. 将待更新的值转换为 JSON
bytes, err := json.Marshal(v)
if err != nil {
return err
}
// 3. 执行 Elasticsearch 更新操作
_, err = global.ESClient.Update(elasticsearch.ArticleIndex(), articleID).
Request(&update.Request{Doc: bytes}).
Refresh(refresh.True).
Do(context.TODO())
if err != nil {
return err
}
// 4. 延迟第二次删除缓存
go func(id string) {
// 延迟一段时间(如1秒),等待可能的数据库主从同步完成
time.Sleep(200 * time.Millisecond)
articleCacheDB.Delete(id)
global.Log.Info(fmt.Sprintf("Delete article id:%s from cache", id))
}(articleID)
return nil
}
// Exits 用于检查文章标题是否存在
func (articleService *ArticleService) Exits(title string) (bool, error) {
// 创建查询请求,匹配标题字段
req := &search.Request{
Query: &types.Query{
Match: map[string]types.MatchQuery{"keyword": {Query: title}},
},
}
// 执行搜索查询,查找是否存在该标题的文章
res, err := global.ESClient.Search().Index(elasticsearch.ArticleIndex()).Request(req).Size(1).Do(context.TODO())
if err != nil {
return false, err
}
// 如果存在该标题,返回 true
return res.Hits.Total.Value > 0, nil
}
// UpdateCategoryCount 更新文章类别的计数(增加或减少)
func (articleService *ArticleService) UpdateCategoryCount(tx *gorm.DB, oldCategory, newCategory string) error {
// 如果新类别和旧类别相同,直接返回,不进行更新
if newCategory == oldCategory {
return nil
}
// 如果新类别不为空,更新新类别的文章计数
if newCategory != "" {
var newArticleCategory database.ArticleCategory
// 如果新类别不存在,则创建新类别并设置计数为1
if errors.Is(tx.Where("category = ?", newCategory).First(&newArticleCategory).Error, gorm.ErrRecordNotFound) {
if err := tx.Create(&database.ArticleCategory{Category: newCategory, Number: 1}).Error; err != nil {
return err
}
} else {
// 如果类别已存在,更新该类别的计数
if err := tx.Model(&newArticleCategory).Update("number", gorm.Expr("number + ?", 1)).Error; err != nil {
return err
}
}
}
// 如果旧类别不为空,更新旧类别的文章计数
if oldCategory != "" {
var oldArticleCategory database.ArticleCategory
// 更新旧类别的文章计数,减少 1
if err := tx.Where("category = ?", oldCategory).First(&oldArticleCategory).Update("number", gorm.Expr("number - ?", 1)).Error; err != nil {
return err
}
// 如果旧类别的计数为 1(减少 1 之前),则删除该类别
if oldArticleCategory.Number == 1 {
if err := tx.Delete(&oldArticleCategory).Error; err != nil {
return err
}
}
}
return nil
}
// UpdateTagsCount 更新文章标签的计数(增加或减少)
func (articleService *ArticleService) UpdateTagsCount(tx *gorm.DB, oldTags, newTags []string) error {
// 比较旧标签和新标签,获取新增和移除的标签
addedTags, removedTags := utils.DiffArrays(oldTags, newTags)
// 处理新增的标签
for _, addedTag := range addedTags {
var t database.ArticleTag
// 如果标签不存在,则创建该标签并设置计数为1
if errors.Is(tx.Where("tag = ?", addedTag).First(&t).Error, gorm.ErrRecordNotFound) {
if err := tx.Create(&database.ArticleTag{Tag: addedTag, Number: 1}).Error; err != nil {
return err
}
} else {
// 如果标签已存在,更新标签的计数
if err := tx.Model(&t).Update("number", gorm.Expr("number + ?", 1)).Error; err != nil {
return err
}
}
}
// 处理移除的标签
for _, removedTag := range removedTags {
var t database.ArticleTag
// 更新标签计数,减少 1
if err := tx.Where("tag = ?", removedTag).First(&t).Update("number", gorm.Expr("number - ?", 1)).Error; err != nil {
return err
}
// 如果标签的计数为 1(减少 1 之前),则删除该标签
if t.Number == 1 {
if err := tx.Delete(&t).Error; err != nil {
return err
}
}
}
return nil
}改进效果
没有缓存,耗时 7.93ms;加入缓存,耗时 0.63ms。效果显著!
没有缓存(尾部 "cost" 字段为耗时)
{"level":"INFO","time":"2025-05-23T19:47:55.645+0800","caller":"middleware/logger.go:35","msg":"/api/article/t-o5B5cBaaB3PF7pQFhr","status":200,"method":"GET","path":"/api/article/t-o5B5cBaaB3PF7pQFhr","query":"","ip":"120.85.135.78","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","errors":"","cost":0.007933367}
加入缓存
{"level":"INFO","time":"2025-05-23T19:48:06.689+0800","caller":"middleware/logger.go:35","msg":"/api/article/t-o5B5cBaaB3PF7pQFhr","status":200,"method":"GET","path":"/api/article/t-o5B5cBaaB3PF7pQFhr","query":"","ip":"120.85.135.78","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","errors":"","cost":0.000635325}
评论 (2)
后参与评论
:emoji:e40:
:emoji:e2::emoji:e2: