refactor(P3): Redis 缓存键版本化 — Client 增加 Key() 方法,默认前缀 v1:

This commit is contained in:
Sisyphus
2026-04-25 16:57:02 +08:00
parent 41ae86f908
commit dd47b48473
7 changed files with 544 additions and 536 deletions

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"feedsystem_video_go/internal/auth" "feedsystem_video_go/internal/auth"
"fmt"
"log" "log"
"time" "time"
@@ -65,7 +64,7 @@ func (as *AccountService) Rename(ctx context.Context, accountID uint, newUsernam
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel() defer cancel()
if err := as.cache.SetBytes(cacheCtx, fmt.Sprintf("account:%d", accountID), []byte(token), 24*time.Hour); err != nil { if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", accountID), []byte(token), 24*time.Hour); err != nil {
log.Printf("failed to set cache: %v", err) log.Printf("failed to set cache: %v", err)
} }
} }
@@ -129,7 +128,7 @@ func (as *AccountService) Login(ctx context.Context, username, password string)
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel() defer cancel()
if err := as.cache.SetBytes(cacheCtx, fmt.Sprintf("account:%d", account.ID), []byte(token), 24*time.Hour); err != nil { if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(token), 24*time.Hour); err != nil {
log.Printf("failed to set cache: %v", err) log.Printf("failed to set cache: %v", err)
} }
} }
@@ -148,7 +147,7 @@ func (as *AccountService) Logout(ctx context.Context, accountID uint) error {
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel() defer cancel()
if err := as.cache.Del(cacheCtx, fmt.Sprintf("account:%d", account.ID)); err != nil { if err := as.cache.Del(cacheCtx, as.cache.Key("account:%d", account.ID)); err != nil {
log.Printf("failed to del cache: %v", err) log.Printf("failed to del cache: %v", err)
} }
} }

View File

@@ -44,7 +44,7 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
//L1:本地缓存 //L1:本地缓存
var missedL1 []uint var missedL1 []uint
for _, id := range videoIDs { for _, id := range videoIDs {
cacheKey := fmt.Sprintf("video:entity:%d", id) cacheKey := f.rediscache.Key("video:entity:%d", id)
if f.localcache != nil { if f.localcache != nil {
if v, found := f.localcache.Get(cacheKey); found { if v, found := f.localcache.Get(cacheKey); found {
if data, ok := v.(video.Video); ok { if data, ok := v.(video.Video); ok {
@@ -66,7 +66,7 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
if len(missedL1) > 0 { if len(missedL1) > 0 {
cacheKeys := make([]string, len(missedL1)) cacheKeys := make([]string, len(missedL1))
for i, id := range missedL1 { for i, id := range missedL1 {
cacheKeys[i] = fmt.Sprintf("video:entity:%d", id) cacheKeys[i] = f.rediscache.Key("video:entity:%d", id)
} }
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
@@ -109,7 +109,7 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
wg.Add(1) wg.Add(1)
go func(videoID uint) { go func(videoID uint) {
defer wg.Done() defer wg.Done()
sfKey := fmt.Sprintf("sf:entity:%d", videoID) sfKey := f.rediscache.Key("sf:entity:%d", videoID)
v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) { v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) {
videoList, err := f.repo.GetByIDs(ctx, []uint{videoID}) videoList, err := f.repo.GetByIDs(ctx, []uint{videoID})
@@ -119,7 +119,7 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
} }
safeCopy := *videoList[0] safeCopy := *videoList[0]
cachekey := fmt.Sprintf("video:entity:%d", safeCopy.ID) cachekey := f.rediscache.Key("video:entity:%d", safeCopy.ID)
if b, err := json.Marshal(safeCopy); err == nil { if b, err := json.Marshal(safeCopy); err == nil {
//异步回写redis //异步回写redis
go func(k string, b []byte) { go func(k string, b []byte) {
@@ -137,7 +137,7 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
mu.Lock() mu.Lock()
videoMap[id] = &safeCopy videoMap[id] = &safeCopy
mu.Unlock() mu.Unlock()
f.localcache.Set(fmt.Sprintf("video:entity:%d", safeCopy.ID), safeCopy, 5*time.Second) f.localcache.Set(f.rediscache.Key("video:entity:%d", safeCopy.ID), safeCopy, 5*time.Second)
} }
}(id) }(id)
} }
@@ -148,7 +148,7 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
// 查询最新视频 (冷热分离 + 游标分页) // 查询最新视频 (冷热分离 + 游标分页)
func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore time.Time, viewerAccountID uint) (ListLatestResponse, error) { func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore time.Time, viewerAccountID uint) (ListLatestResponse, error) {
// 获取 ZSET 中最老的一条数据 // 获取 ZSET 中最老的一条数据
zsetTail, err := f.rediscache.ZRangeWithScores(ctx, "feed:global_timeline", 0, 0) zsetTail, err := f.rediscache.ZRangeWithScores(ctx, f.rediscache.Key("feed:global_timeline"), 0, 0)
if err != nil { if err != nil {
return ListLatestResponse{}, err return ListLatestResponse{}, err
@@ -158,7 +158,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
if isZsetEmpty { if isZsetEmpty {
//全局静态锁:无视所有用户的不同时间戳游标 //全局静态锁:无视所有用户的不同时间戳游标
sfKey := "sf:fallback:global_timeline_rebuild" sfKey := f.rediscache.Key("sf:fallback:global_timeline_rebuild")
v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) { v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) {
// 无视游标,直接去 MySQL 捞最新的 1000 条 // 无视游标,直接去 MySQL 捞最新的 1000 条
@@ -180,7 +180,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
Member: fmt.Sprintf("%d", vid.ID), Member: fmt.Sprintf("%d", vid.ID),
}) })
} }
f.rediscache.ZAdd(bgCtx, "feed:global_timeline", zElements...) f.rediscache.ZAdd(bgCtx, f.rediscache.Key("feed:global_timeline"), zElements...)
return "SUCCESS", nil return "SUCCESS", nil
}) })
@@ -207,7 +207,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
//冷数据降级查库 //冷数据降级查库
// 针对个别用户的防并发(此时可以用时间戳做锁,因为冷尾流量极小) // 针对个别用户的防并发(此时可以用时间戳做锁,因为冷尾流量极小)
sfKey := fmt.Sprintf("sf:cold:listLatest:%d:%d", limit, reqTime) sfKey := f.rediscache.Key("sf:cold:listLatest:%d:%d", limit, reqTime)
v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) { v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) {
return f.repo.ListLatest(ctx, limit, latestBefore) return f.repo.ListLatest(ctx, limit, latestBefore)
}) })
@@ -224,7 +224,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
maxScore = fmt.Sprintf("%d", reqTime-1) // 防重复 maxScore = fmt.Sprintf("%d", reqTime-1) // 防重复
} }
videoIDsStr, err := f.rediscache.ZRevRangeByScore(ctx, "feed:global_timeline", maxScore, "-inf", 0, int64(limit)) videoIDsStr, err := f.rediscache.ZRevRangeByScore(ctx, f.rediscache.Key("feed:global_timeline"), maxScore, "-inf", 0, int64(limit))
if err != nil { if err != nil {
return ListLatestResponse{}, err return ListLatestResponse{}, err
} }
@@ -254,7 +254,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
coldCursor = latestBefore coldCursor = latestBefore
} }
sfKey := fmt.Sprintf("sf:stitch:listLatest:%d:%d", remainLimit, coldCursor.UnixMilli()) sfKey := f.rediscache.Key("sf:stitch:listLatest:%d:%d", remainLimit, coldCursor.UnixMilli())
v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) { v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) {
return f.repo.ListLatest(ctx, remainLimit, coldCursor) return f.repo.ListLatest(ctx, remainLimit, coldCursor)
}) })
@@ -343,7 +343,7 @@ func (f *FeedService) ListByFollowing(ctx context.Context, limit int, latestBefo
if !latestBefore.IsZero() { if !latestBefore.IsZero() {
before = latestBefore.Unix() before = latestBefore.Unix()
} }
cacheKey = fmt.Sprintf("feed:listByFollowing:limit=%d:accountID=%d:before=%d", limit, viewerAccountID, before) cacheKey = f.rediscache.Key("feed:listByFollowing:limit=%d:accountID=%d:before=%d", limit, viewerAccountID, before)
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel() defer cancel()
@@ -413,10 +413,10 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
const win = 60 const win = 60
keys := make([]string, 0, win) keys := make([]string, 0, win)
for i := 0; i < win; i++ { for i := 0; i < win; i++ {
keys = append(keys, "hot:video:1m:"+asOf.Add(-time.Duration(i)*time.Minute).Format("200601021504")) keys = append(keys, f.rediscache.Key("hot:video:1m:%s", asOf.Add(-time.Duration(i)*time.Minute).Format("200601021504")))
} }
dest := "hot:video:merge:1m:" + asOf.Format("200601021504") // 快照key同一个as_of页内复用 dest := f.rediscache.Key("hot:video:merge:1m:%s", asOf.Format("200601021504")) // 快照key同一个as_of页内复用
opCtx, cancel := context.WithTimeout(ctx, 80*time.Millisecond) opCtx, cancel := context.WithTimeout(ctx, 80*time.Millisecond)
defer cancel() defer cancel()

View File

@@ -3,7 +3,6 @@ package jwt
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"net/http" "net/http"
"strings" "strings"
@@ -69,7 +68,7 @@ func SoftJWTAuth(accountRepo *account.AccountRepository, cache *rediscache.Clien
} }
func check(c *gin.Context, claims *auth.Claims, tokenString string, accountRepo *account.AccountRepository, cache *rediscache.Client) { func check(c *gin.Context, claims *auth.Claims, tokenString string, accountRepo *account.AccountRepository, cache *rediscache.Client) {
key := fmt.Sprintf("account:%d", claims.AccountID) key := cache.Key("account:%d", claims.AccountID)
// 先查 Redis // 先查 Redis
if cache != nil { if cache != nil {

View File

@@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"feedsystem_video_go/internal/config" "feedsystem_video_go/internal/config"
"fmt"
"strconv" "strconv"
"time" "time"
@@ -12,16 +13,19 @@ import (
) )
type Client struct { type Client struct {
rdb *redis.Client rdb *redis.Client
keyPrefix string
} }
const defaultKeyPrefix = "v1:"
func NewFromEnv(cfg *config.RedisConfig) (*Client, error) { func NewFromEnv(cfg *config.RedisConfig) (*Client, error) {
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: cfg.Host + ":" + strconv.Itoa(cfg.Port), Addr: cfg.Host + ":" + strconv.Itoa(cfg.Port),
Password: cfg.Password, Password: cfg.Password,
DB: cfg.DB, DB: cfg.DB,
}) })
return &Client{rdb: rdb}, nil return &Client{rdb: rdb, keyPrefix: defaultKeyPrefix}, nil
} }
func (c *Client) Close() error { func (c *Client) Close() error {
@@ -42,6 +46,14 @@ func IsMiss(err error) bool {
return err == redis.Nil return err == redis.Nil
} }
func (c *Client) Key(format string, args ...any) string {
prefix := ""
if c != nil {
prefix = c.keyPrefix
}
return prefix + fmt.Sprintf(format, args...)
}
func randToken(n int) (string, error) { func randToken(n int) (string, error) {
b := make([]byte, n) b := make([]byte, n)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {

View File

@@ -2,7 +2,6 @@ package video
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"time" "time"
@@ -15,10 +14,10 @@ func UpdatePopularityCache(ctx context.Context, cache *rediscache.Client, id uin
return return
} }
_ = cache.Del(context.Background(), fmt.Sprintf("video:detail:id=%d", id)) _ = cache.Del(context.Background(), cache.Key("video:detail:id=%d", id))
now := time.Now().UTC().Truncate(time.Minute) now := time.Now().UTC().Truncate(time.Minute)
windowKey := "hot:video:1m:" + now.Format("200601021504") windowKey := cache.Key("hot:video:1m:%s", now.Format("200601021504"))
member := strconv.FormatUint(uint64(id), 10) member := strconv.FormatUint(uint64(id), 10)
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -83,7 +82,7 @@ func (vs *VideoService) Delete(ctx context.Context, id uint, authorID uint) erro
return err return err
} }
if vs.cache != nil { if vs.cache != nil {
cacheKey := fmt.Sprintf("video:detail:id=%d", id) cacheKey := vs.cache.Key("video:detail:id=%d", id)
_ = vs.cache.Del(context.Background(), cacheKey) _ = vs.cache.Del(context.Background(), cacheKey)
} }
return nil return nil
@@ -98,7 +97,7 @@ func (vs *VideoService) ListByAuthorID(ctx context.Context, authorID uint) ([]Vi
} }
func (vs *VideoService) GetDetail(ctx context.Context, id uint) (*Video, error) { func (vs *VideoService) GetDetail(ctx context.Context, id uint) (*Video, error) {
cacheKey := fmt.Sprintf("video:detail:id=%d", id) cacheKey := vs.cache.Key("video:detail:id=%d", id)
getCached := func() (*Video, bool) { getCached := func() (*Video, bool) {
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
@@ -204,11 +203,11 @@ func (vs *VideoService) UpdatePopularity(ctx context.Context, id uint, change in
if vs.cache != nil { if vs.cache != nil {
// 1) 详情缓存:直接失效(最简单靠谱) // 1) 详情缓存:直接失效(最简单靠谱)
_ = vs.cache.Del(context.Background(), fmt.Sprintf("video:detail:id=%d", id)) _ = vs.cache.Del(context.Background(), vs.cache.Key("video:detail:id=%d", id))
// 2) 热榜写到“时间窗ZSET”不要用 detail key // 2) 热榜写到“时间窗ZSET”不要用 detail key
now := time.Now().UTC().Truncate(time.Minute) now := time.Now().UTC().Truncate(time.Minute)
windowKey := "hot:video:1m:" + now.Format("200601021504") windowKey := vs.cache.Key("hot:video:1m:%s", now.Format("200601021504"))
member := strconv.FormatUint(uint64(id), 10) member := strconv.FormatUint(uint64(id), 10)
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)

View File

@@ -67,7 +67,7 @@ func StartConsumer(tmq *rabbitmq.TimelineMQ, queueName string, redisClient *redi
} }
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
timelineKey := "feed:global_timeline" timelineKey := redisClient.Key("feed:global_timeline")
err = redisClient.ZAdd(ctx, timelineKey, oredis.Z{ err = redisClient.ZAdd(ctx, timelineKey, oredis.Z{
Score: float64(event.CreateTime), Score: float64(event.CreateTime),
Member: fmt.Sprintf("%d", event.VideoID), Member: fmt.Sprintf("%d", event.VideoID),