feat: 首页增加内存缓存(30s TTL)
- 新增 internal/cache/home_cache.go,实现带 TTL 的首页文章列表内存缓存 - frontend.go 首页 handler 优先读取缓存,未命中/过期时查询 DB 并回填 - PostService 注入失效回调,在 Create/Update/Delete/Approve 后自动失效缓存 - 减少首页每次请求都查 DB 的开销
This commit is contained in:
55
internal/cache/home_cache.go
vendored
Normal file
55
internal/cache/home_cache.go
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"metazone.cc/metalab/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HomePageCache 首页内存缓存,30 秒 TTL,创建/更新/删除/审核通过时失效
|
||||||
|
type HomePageCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
posts []model.Post
|
||||||
|
total int64
|
||||||
|
expires time.Time
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHomePageCache 创建首页缓存,ttl 为缓存有效期
|
||||||
|
func NewHomePageCache(ttl time.Duration) *HomePageCache {
|
||||||
|
return &HomePageCache{ttl: ttl}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 返回缓存数据,ok=false 表示缓存未命中或已过期
|
||||||
|
func (c *HomePageCache) Get() (posts []model.Post, total int64, ok bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if time.Now().After(c.expires) {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
// 返回浅拷贝以防止调用方修改缓存内部数据
|
||||||
|
copied := make([]model.Post, len(c.posts))
|
||||||
|
copy(copied, c.posts)
|
||||||
|
return copied, c.total, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 写入缓存数据
|
||||||
|
func (c *HomePageCache) Set(posts []model.Post, total int64) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
// 深拷贝一份避免外部修改影响缓存
|
||||||
|
c.posts = make([]model.Post, len(posts))
|
||||||
|
copy(c.posts, posts)
|
||||||
|
c.total = total
|
||||||
|
c.expires = time.Now().Add(c.ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate 主动失效缓存
|
||||||
|
func (c *HomePageCache) Invalidate() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.posts = nil
|
||||||
|
c.total = 0
|
||||||
|
c.expires = time.Time{}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"metazone.cc/metalab/internal/cache"
|
||||||
"metazone.cc/metalab/internal/config"
|
"metazone.cc/metalab/internal/config"
|
||||||
"metazone.cc/metalab/internal/controller"
|
"metazone.cc/metalab/internal/controller"
|
||||||
adminCtrl "metazone.cc/metalab/internal/controller/admin"
|
adminCtrl "metazone.cc/metalab/internal/controller/admin"
|
||||||
@ -42,6 +43,7 @@ type dependencies struct {
|
|||||||
favoriteCtrl *controller.FavoriteController
|
favoriteCtrl *controller.FavoriteController
|
||||||
favoriteService *service.FavoriteService
|
favoriteService *service.FavoriteService
|
||||||
trendsService *service.TrendsService
|
trendsService *service.TrendsService
|
||||||
|
homeCache *cache.HomePageCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) (
|
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) (
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"metazone.cc/metalab/internal/cache"
|
||||||
"metazone.cc/metalab/internal/config"
|
"metazone.cc/metalab/internal/config"
|
||||||
"metazone.cc/metalab/internal/controller"
|
"metazone.cc/metalab/internal/controller"
|
||||||
adminCtrl "metazone.cc/metalab/internal/controller/admin"
|
adminCtrl "metazone.cc/metalab/internal/controller/admin"
|
||||||
@ -99,6 +102,10 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
|||||||
|
|
||||||
maintenanceMdw := middleware.NewMaintenanceMiddleware(cfg, siteSettings, sessionMgr)
|
maintenanceMdw := middleware.NewMaintenanceMiddleware(cfg, siteSettings, sessionMgr)
|
||||||
|
|
||||||
|
// 首页内存缓存(30s TTL),文章变更时失效
|
||||||
|
homeCache := cache.NewHomePageCache(30 * time.Second)
|
||||||
|
postService.WithHomePageInvalidator(func() { homeCache.Invalidate() })
|
||||||
|
|
||||||
return &dependencies{
|
return &dependencies{
|
||||||
authCtrl: authCtrl, authMdw: authMdw, maintenanceMdw: maintenanceMdw,
|
authCtrl: authCtrl, authMdw: authMdw, maintenanceMdw: maintenanceMdw,
|
||||||
settingsCtrl: settingsCtrl,
|
settingsCtrl: settingsCtrl,
|
||||||
@ -119,5 +126,6 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
|||||||
favoriteCtrl: favoriteCtrl,
|
favoriteCtrl: favoriteCtrl,
|
||||||
favoriteService: favoriteService,
|
favoriteService: favoriteService,
|
||||||
trendsService: trendsService,
|
trendsService: trendsService,
|
||||||
|
homeCache: homeCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,10 +22,16 @@ func setupFrontendRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
})
|
})
|
||||||
{
|
{
|
||||||
pages.GET("/", func(c *gin.Context) {
|
pages.GET("/", func(c *gin.Context) {
|
||||||
posts, total, err := d.postService.List("", 1, 6)
|
posts, total, ok := d.homeCache.Get()
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
posts, total, err = d.postService.List("", 1, 6)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
posts = nil
|
posts = nil
|
||||||
total = 0
|
total = 0
|
||||||
|
} else {
|
||||||
|
d.homeCache.Set(posts, total)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "home/index.html", common.BuildPageData(c, gin.H{
|
c.HTML(http.StatusOK, "home/index.html", common.BuildPageData(c, gin.H{
|
||||||
"Title": "首页",
|
"Title": "首页",
|
||||||
|
|||||||
@ -21,7 +21,11 @@ func (s *PostService) Update(postID uint, title, body string) error {
|
|||||||
post.PendingTitle = title
|
post.PendingTitle = title
|
||||||
post.PendingBody = body
|
post.PendingBody = body
|
||||||
post.RejectReason = "" // 清空旧退回理由
|
post.RejectReason = "" // 清空旧退回理由
|
||||||
return s.repo.Update(post)
|
err := s.repo.Update(post)
|
||||||
|
if err == nil {
|
||||||
|
s.invalidateHomeCache()
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 草稿/已退回:直接修改原文(现有逻辑)
|
// 草稿/已退回:直接修改原文(现有逻辑)
|
||||||
@ -34,12 +38,20 @@ func (s *PostService) Update(postID uint, title, body string) error {
|
|||||||
post.RejectReason = ""
|
post.RejectReason = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.repo.Update(post)
|
err = s.repo.Update(post)
|
||||||
|
if err == nil {
|
||||||
|
s.invalidateHomeCache()
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 软删除(权限在 controller 层检查)
|
// Delete 软删除(权限在 controller 层检查)
|
||||||
func (s *PostService) Delete(postID uint) error {
|
func (s *PostService) Delete(postID uint) error {
|
||||||
return s.repo.SoftDelete(postID)
|
if err := s.repo.SoftDelete(postID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.invalidateHomeCache()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmitForAudit 提交审核:draft → pending
|
// SubmitForAudit 提交审核:draft → pending
|
||||||
@ -79,6 +91,7 @@ func (s *PostService) Approve(postID uint) error {
|
|||||||
if err := s.repo.Update(post); err != nil {
|
if err := s.repo.Update(post); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.invalidateHomeCache()
|
||||||
// 通知作者修订审核通过
|
// 通知作者修订审核通过
|
||||||
if s.notifier != nil {
|
if s.notifier != nil {
|
||||||
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
||||||
@ -95,6 +108,7 @@ func (s *PostService) Approve(postID uint) error {
|
|||||||
if err := s.repo.Update(post); err != nil {
|
if err := s.repo.Update(post); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.invalidateHomeCache()
|
||||||
// 通知作者审核通过
|
// 通知作者审核通过
|
||||||
if s.notifier != nil {
|
if s.notifier != nil {
|
||||||
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ type PostService struct {
|
|||||||
ss *config.SiteSettings
|
ss *config.SiteSettings
|
||||||
notifier postNotifier
|
notifier postNotifier
|
||||||
userRepo userExpReader
|
userRepo userExpReader
|
||||||
|
onHomePageChanged func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPostService 构造函数
|
// NewPostService 构造函数
|
||||||
@ -29,6 +30,11 @@ func NewPostService(repo postStore, ss *config.SiteSettings, notifier postNotifi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHomePageInvalidator 注入首页缓存失效回调(文章变更时触发)
|
||||||
|
func (s *PostService) WithHomePageInvalidator(fn func()) {
|
||||||
|
s.onHomePageChanged = fn
|
||||||
|
}
|
||||||
|
|
||||||
// auditEnabled 审核是否开启
|
// auditEnabled 审核是否开启
|
||||||
func (s *PostService) auditEnabled() bool {
|
func (s *PostService) auditEnabled() bool {
|
||||||
return s.ss.IsAuditEnabled()
|
return s.ss.IsAuditEnabled()
|
||||||
@ -78,6 +84,7 @@ func (s *PostService) Create(userID uint, title, body string) (*model.Post, erro
|
|||||||
if err := s.repo.Create(post); err != nil {
|
if err := s.repo.Create(post); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
s.invalidateHomeCache()
|
||||||
return post, nil
|
return post, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +163,13 @@ func (s *PostService) CanCreatePost(userID uint) bool {
|
|||||||
return user.Exp >= 1
|
return user.Exp >= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invalidateHomeCache 失效首页缓存(异步安全调用)
|
||||||
|
func (s *PostService) invalidateHomeCache() {
|
||||||
|
if s.onHomePageChanged != nil {
|
||||||
|
s.onHomePageChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RecordRead 记录已登录用户阅读文章(已登录去重 + 防刷冷却)
|
// RecordRead 记录已登录用户阅读文章(已登录去重 + 防刷冷却)
|
||||||
func (s *PostService) RecordRead(userID, postID uint) {
|
func (s *PostService) RecordRead(userID, postID uint) {
|
||||||
firstRead, err := s.repo.RecordRead(userID, postID)
|
firstRead, err := s.repo.RecordRead(userID, postID)
|
||||||
|
|||||||
Reference in New Issue
Block a user