perf(expiry): 完成 #34 性能优化与缓存
This commit is contained in:
@@ -121,6 +121,10 @@ func main() {
|
|||||||
|
|
||||||
expiryRepo := expiry.NewRepository(database.DB)
|
expiryRepo := expiry.NewRepository(database.DB)
|
||||||
expiryService := expiry.NewService(expiryRepo)
|
expiryService := expiry.NewService(expiryRepo)
|
||||||
|
if redisClient != nil {
|
||||||
|
expirySummaryCache := expiry.NewSummaryCache(redisClient.Redis(), redisClient.KeyPrefix(), 5*time.Minute)
|
||||||
|
expiryService.BindSummaryCache(expirySummaryCache)
|
||||||
|
}
|
||||||
expiryHandler := expiry.NewHandler(expiryService)
|
expiryHandler := expiry.NewHandler(expiryService)
|
||||||
|
|
||||||
// 6) 注册路由:把 URL 映射到 handler
|
// 6) 注册路由:把 URL 映射到 handler
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 性能优化说明(2026-03-04)
|
||||||
|
|
||||||
|
## 后端优化
|
||||||
|
|
||||||
|
### 1. summary Redis 缓存(5分钟)
|
||||||
|
- 缓存 key: `expiry_summary:{user_id}`
|
||||||
|
- 缓存时机:`GetSummary`
|
||||||
|
- 失效时机:
|
||||||
|
- 创建物品
|
||||||
|
- 更新物品
|
||||||
|
- 删除物品
|
||||||
|
- 标记状态(used/discarded)
|
||||||
|
|
||||||
|
### 2. 查询路径优化
|
||||||
|
- 继续使用以下索引支撑核心查询:
|
||||||
|
- `idx_user_expiry (user_id, expiry_date)`
|
||||||
|
- `idx_user_category (user_id, category)`
|
||||||
|
- `idx_user_status (user_id, status)`
|
||||||
|
- 修复汇总统计中基查询复用导致的统计偏差风险。
|
||||||
|
|
||||||
|
## 前端优化
|
||||||
|
|
||||||
|
### 1. 列表滚动优化
|
||||||
|
- 列表采用分页加载(`page/page_size`)与上拉触底加载,避免一次性渲染大量节点。
|
||||||
|
- 筛选与排序切换时采用增量刷新,降低页面抖动。
|
||||||
|
|
||||||
|
### 2. 首页加载优化
|
||||||
|
- 首页仅请求 `summary` + `expiring` 前 10 条,减少首屏数据量。
|
||||||
|
|
||||||
|
## 验证建议
|
||||||
|
1. 在压测环境执行:
|
||||||
|
```bash
|
||||||
|
hey -n 100 -c 20 -H "Authorization: Bearer <token>" http://<host>/api/expiry/summary
|
||||||
|
```
|
||||||
|
2. 观察均值响应时间与 P95。
|
||||||
|
3. 观察 Redis 命中率与数据库 QPS 变化。
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package expiry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SummaryCache 封装 summary 接口的 Redis 缓存。
|
||||||
|
type SummaryCache struct {
|
||||||
|
rc *redis.Client
|
||||||
|
keyPrefix string
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSummaryCache(rc *redis.Client, keyPrefix string, ttl time.Duration) *SummaryCache {
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = 5 * time.Minute
|
||||||
|
}
|
||||||
|
return &SummaryCache{
|
||||||
|
rc: rc,
|
||||||
|
keyPrefix: keyPrefix,
|
||||||
|
ttl: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SummaryCache) Get(ctx context.Context, userID uint) (*SummaryResponse, bool, error) {
|
||||||
|
if c == nil || c.rc == nil || userID == 0 {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := c.rc.Get(ctx, c.summaryKey(userID)).Bytes()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return nil, false, fmt.Errorf("redis get summary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary SummaryResponse
|
||||||
|
if err := json.Unmarshal(val, &summary); err != nil {
|
||||||
|
_ = c.rc.Del(ctx, c.summaryKey(userID)).Err()
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &summary, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SummaryCache) Set(ctx context.Context, userID uint, summary *SummaryResponse) error {
|
||||||
|
if c == nil || c.rc == nil || userID == 0 || summary == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(summary)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal summary: %w", err)
|
||||||
|
}
|
||||||
|
if err := c.rc.Set(ctx, c.summaryKey(userID), body, c.ttl).Err(); err != nil {
|
||||||
|
return fmt.Errorf("redis set summary: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SummaryCache) Delete(ctx context.Context, userID uint) error {
|
||||||
|
if c == nil || c.rc == nil || userID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := c.rc.Del(ctx, c.summaryKey(userID)).Err(); err != nil {
|
||||||
|
return fmt.Errorf("redis del summary: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SummaryCache) summaryKey(userID uint) string {
|
||||||
|
prefix := c.keyPrefix
|
||||||
|
if prefix != "" && !strings.HasSuffix(prefix, ":") {
|
||||||
|
prefix += ":"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%sexpiry_summary:%d", prefix, userID)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package expiry
|
package expiry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,7 +25,8 @@ var (
|
|||||||
|
|
||||||
// Service 封装保质期模块业务逻辑。
|
// Service 封装保质期模块业务逻辑。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
repo *Repository
|
repo *Repository
|
||||||
|
summaryCache *SummaryCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateItemRequest 创建物品请求。
|
// CreateItemRequest 创建物品请求。
|
||||||
@@ -96,6 +98,11 @@ func NewService(repo *Repository) *Service {
|
|||||||
return &Service{repo: repo}
|
return &Service{repo: repo}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindSummaryCache 绑定 summary 的 Redis 缓存(可选)。
|
||||||
|
func (s *Service) BindSummaryCache(cache *SummaryCache) {
|
||||||
|
s.summaryCache = cache
|
||||||
|
}
|
||||||
|
|
||||||
// CreateItem 创建物品,并处理过期日期自动计算。
|
// CreateItem 创建物品,并处理过期日期自动计算。
|
||||||
func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error) {
|
func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error) {
|
||||||
if req.MiniProgramID == 0 {
|
if req.MiniProgramID == 0 {
|
||||||
@@ -135,6 +142,7 @@ func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, e
|
|||||||
if err := s.repo.Create(item); err != nil {
|
if err := s.repo.Create(item); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
s.invalidateSummaryCache(userID)
|
||||||
|
|
||||||
item.Status = item.CalculateStatus()
|
item.Status = item.CalculateStatus()
|
||||||
return item, nil
|
return item, nil
|
||||||
@@ -186,6 +194,7 @@ func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryIte
|
|||||||
if err := s.repo.Update(item); err != nil {
|
if err := s.repo.Update(item); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
s.invalidateSummaryCache(userID)
|
||||||
|
|
||||||
item.Status = item.CalculateStatus()
|
item.Status = item.CalculateStatus()
|
||||||
return item, nil
|
return item, nil
|
||||||
@@ -193,7 +202,11 @@ func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryIte
|
|||||||
|
|
||||||
// DeleteItem 删除物品。
|
// DeleteItem 删除物品。
|
||||||
func (s *Service) DeleteItem(id, userID uint) error {
|
func (s *Service) DeleteItem(id, userID uint) error {
|
||||||
return s.repo.Delete(id, userID)
|
if err := s.repo.Delete(id, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.invalidateSummaryCache(userID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetItem 获取单个物品。
|
// GetItem 获取单个物品。
|
||||||
@@ -237,19 +250,26 @@ func (s *Service) GetItems(userID uint, filters ItemFilters) (*ItemListResponse,
|
|||||||
|
|
||||||
// GetSummary 获取首页汇总。
|
// GetSummary 获取首页汇总。
|
||||||
func (s *Service) GetSummary(userID uint) (*SummaryResponse, error) {
|
func (s *Service) GetSummary(userID uint) (*SummaryResponse, error) {
|
||||||
|
if cached, ok := s.getSummaryFromCache(userID); ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
data, err := s.repo.GetSummary(userID)
|
data, err := s.repo.GetSummary(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SummaryResponse{
|
summary := &SummaryResponse{
|
||||||
TotalItems: data["total_items"],
|
TotalItems: data["total_items"],
|
||||||
ExpiringSoon: data["expiring_soon"],
|
ExpiringSoon: data["expiring_soon"],
|
||||||
Expired: data["expired"],
|
Expired: data["expired"],
|
||||||
Normal: data["normal"],
|
Normal: data["normal"],
|
||||||
Used: data["used"],
|
Used: data["used"],
|
||||||
Discarded: data["discarded"],
|
Discarded: data["discarded"],
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
s.setSummaryToCache(userID, summary)
|
||||||
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateItemStatus 标记物品状态。
|
// UpdateItemStatus 标记物品状态。
|
||||||
@@ -258,7 +278,11 @@ func (s *Service) UpdateItemStatus(id, userID uint, status string) error {
|
|||||||
if status != StatusUsed && status != StatusDiscarded {
|
if status != StatusUsed && status != StatusDiscarded {
|
||||||
return ErrExpiryStatusInvalid
|
return ErrExpiryStatusInvalid
|
||||||
}
|
}
|
||||||
return s.repo.UpdateStatus(id, userID, status)
|
if err := s.repo.UpdateStatus(id, userID, status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.invalidateSummaryCache(userID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSettings 获取用户提醒设置;若未配置则返回默认值。
|
// GetSettings 获取用户提醒设置;若未配置则返回默认值。
|
||||||
@@ -433,3 +457,28 @@ func validateRemindDays(days []int) ([]int, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) getSummaryFromCache(userID uint) (*SummaryResponse, bool) {
|
||||||
|
if s.summaryCache == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
summary, ok, err := s.summaryCache.Get(context.Background(), userID)
|
||||||
|
if err != nil || !ok || summary == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return summary, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) setSummaryToCache(userID uint, summary *SummaryResponse) {
|
||||||
|
if s.summaryCache == nil || summary == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.summaryCache.Set(context.Background(), userID, summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) invalidateSummaryCache(userID uint) {
|
||||||
|
if s.summaryCache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.summaryCache.Delete(context.Background(), userID)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user