- Removed Gorse-related configurations, handlers, and dependencies from the codebase. - Introduced HotRank feature with configuration options for ranking posts based on recent activity. - Updated application structure to support HotRank processing, including new caching mechanisms and database interactions. - Cleaned up related DTOs and repository methods to reflect the removal of Gorse and the addition of HotRank functionality.
487 lines
10 KiB
Go
487 lines
10 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
redislib "github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
|
|
redisPkg "carrot_bbs/internal/pkg/redis"
|
|
)
|
|
|
|
type LayeredCache struct {
|
|
local *LRUStriped
|
|
redis *RedisCache
|
|
mu sync.RWMutex
|
|
stats CacheStats
|
|
enabled bool
|
|
keyPrefix string
|
|
}
|
|
|
|
type CacheStats struct {
|
|
LocalHits int64
|
|
RedisHits int64
|
|
Misses int64
|
|
Sets int64
|
|
Invalidates int64
|
|
}
|
|
|
|
type LayeredCacheOptions struct {
|
|
LocalSize int
|
|
LocalBuckets int
|
|
LocalDefaultTTL time.Duration
|
|
KeyPrefix string
|
|
Enabled bool
|
|
}
|
|
|
|
func NewLayeredCache(redisClient *redisPkg.Client, opts *LayeredCacheOptions) *LayeredCache {
|
|
if opts == nil {
|
|
opts = &LayeredCacheOptions{
|
|
LocalSize: 10000,
|
|
LocalBuckets: 0,
|
|
LocalDefaultTTL: 5 * time.Minute,
|
|
KeyPrefix: "",
|
|
Enabled: true,
|
|
}
|
|
}
|
|
|
|
if opts.LocalSize <= 0 {
|
|
opts.LocalSize = 10000
|
|
}
|
|
if opts.LocalDefaultTTL <= 0 {
|
|
opts.LocalDefaultTTL = 5 * time.Minute
|
|
}
|
|
|
|
localCache := NewLRUStriped(&LRUStripedOptions{
|
|
Size: opts.LocalSize,
|
|
DefaultExpiry: opts.LocalDefaultTTL,
|
|
Buckets: opts.LocalBuckets,
|
|
Name: "layered_local",
|
|
})
|
|
|
|
var redisCache *RedisCache
|
|
if redisClient != nil {
|
|
redisCache = NewRedisCache(redisClient)
|
|
}
|
|
|
|
return &LayeredCache{
|
|
local: localCache,
|
|
redis: redisCache,
|
|
enabled: opts.Enabled,
|
|
keyPrefix: opts.KeyPrefix,
|
|
}
|
|
}
|
|
|
|
func (c *LayeredCache) Set(key string, value any, ttl time.Duration) {
|
|
if !c.enabled {
|
|
return
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.stats.Sets++
|
|
c.mu.Unlock()
|
|
|
|
c.local.Set(key, value, ttl)
|
|
|
|
if c.redis != nil {
|
|
c.redis.Set(key, value, ttl)
|
|
}
|
|
}
|
|
|
|
func (c *LayeredCache) Get(key string) (any, bool) {
|
|
if !c.enabled {
|
|
return nil, false
|
|
}
|
|
|
|
if val, ok := c.local.Get(key); ok {
|
|
c.mu.Lock()
|
|
c.stats.LocalHits++
|
|
c.mu.Unlock()
|
|
return val, true
|
|
}
|
|
|
|
if c.redis != nil {
|
|
if val, ok := c.redis.Get(key); ok {
|
|
c.mu.Lock()
|
|
c.stats.RedisHits++
|
|
c.mu.Unlock()
|
|
|
|
if ttl := c.getLocalTTL(); ttl > 0 {
|
|
c.local.Set(key, val, ttl)
|
|
}
|
|
return val, true
|
|
}
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.stats.Misses++
|
|
c.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
|
|
func (c *LayeredCache) getLocalTTL() time.Duration {
|
|
return 30 * time.Second
|
|
}
|
|
|
|
func (c *LayeredCache) Delete(key string) {
|
|
if !c.enabled {
|
|
return
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.stats.Invalidates++
|
|
c.mu.Unlock()
|
|
|
|
c.local.Delete(key)
|
|
if c.redis != nil {
|
|
c.redis.Delete(key)
|
|
}
|
|
}
|
|
|
|
func (c *LayeredCache) DeleteByPrefix(prefix string) {
|
|
if !c.enabled {
|
|
return
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.stats.Invalidates++
|
|
c.mu.Unlock()
|
|
|
|
c.local.DeleteByPrefix(prefix)
|
|
if c.redis != nil {
|
|
c.redis.DeleteByPrefix(prefix)
|
|
}
|
|
}
|
|
|
|
func (c *LayeredCache) Clear() {
|
|
c.local.Clear()
|
|
if c.redis != nil {
|
|
c.redis.Clear()
|
|
}
|
|
c.mu.Lock()
|
|
c.stats = CacheStats{}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *LayeredCache) Exists(key string) bool {
|
|
if !c.enabled {
|
|
return false
|
|
}
|
|
|
|
if c.local.Exists(key) {
|
|
return true
|
|
}
|
|
|
|
if c.redis != nil {
|
|
return c.redis.Exists(key)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *LayeredCache) Increment(key string) int64 {
|
|
if !c.enabled {
|
|
return 0
|
|
}
|
|
|
|
if c.redis != nil {
|
|
val := c.redis.Increment(key)
|
|
c.local.Delete(key)
|
|
return val
|
|
}
|
|
|
|
return c.local.Increment(key)
|
|
}
|
|
|
|
func (c *LayeredCache) IncrementBy(key string, value int64) int64 {
|
|
if !c.enabled {
|
|
return 0
|
|
}
|
|
|
|
if c.redis != nil {
|
|
val := c.redis.IncrementBy(key, value)
|
|
c.local.Delete(key)
|
|
return val
|
|
}
|
|
|
|
return c.local.IncrementBy(key, value)
|
|
}
|
|
|
|
func (c *LayeredCache) GetStats() CacheStats {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.stats
|
|
}
|
|
|
|
func (c *LayeredCache) GetHitRate() float64 {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
total := c.stats.LocalHits + c.stats.RedisHits + c.stats.Misses
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
return float64(c.stats.LocalHits+c.stats.RedisHits) / float64(total)
|
|
}
|
|
|
|
func (c *LayeredCache) HSet(ctx context.Context, key string, field string, value any) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.HSet(ctx, key, field, value)
|
|
}
|
|
|
|
func (c *LayeredCache) HMSet(ctx context.Context, key string, values map[string]any) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.HMSet(ctx, key, values)
|
|
}
|
|
|
|
func (c *LayeredCache) HGet(ctx context.Context, key string, field string) (string, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return "", ErrKeyNotFound
|
|
}
|
|
return c.redis.HGet(ctx, key, field)
|
|
}
|
|
|
|
func (c *LayeredCache) HMGet(ctx context.Context, key string, fields ...string) ([]any, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
return c.redis.HMGet(ctx, key, fields...)
|
|
}
|
|
|
|
func (c *LayeredCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
return c.redis.HGetAll(ctx, key)
|
|
}
|
|
|
|
func (c *LayeredCache) HDel(ctx context.Context, key string, fields ...string) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.HDel(ctx, key, fields...)
|
|
}
|
|
|
|
func (c *LayeredCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.ZAdd(ctx, key, score, member)
|
|
}
|
|
|
|
func (c *LayeredCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
return c.redis.ZRangeByScore(ctx, key, min, max, offset, count)
|
|
}
|
|
|
|
func (c *LayeredCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
return c.redis.ZRevRangeByScore(ctx, key, max, min, offset, count)
|
|
}
|
|
|
|
func (c *LayeredCache) ZRevRange(ctx context.Context, key string, start, stop int64) ([]string, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
return c.redis.ZRevRange(ctx, key, start, stop)
|
|
}
|
|
|
|
func (c *LayeredCache) ZReplaceSortedSet(ctx context.Context, key string, members []redislib.Z) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return ErrKeyNotFound
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.ZReplaceSortedSet(ctx, key, members)
|
|
}
|
|
|
|
func (c *LayeredCache) ZRem(ctx context.Context, key string, members ...any) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.ZRem(ctx, key, members...)
|
|
}
|
|
|
|
func (c *LayeredCache) ZCard(ctx context.Context, key string) (int64, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return 0, ErrKeyNotFound
|
|
}
|
|
return c.redis.ZCard(ctx, key)
|
|
}
|
|
|
|
func (c *LayeredCache) Incr(ctx context.Context, key string) (int64, error) {
|
|
if !c.enabled || c.redis == nil {
|
|
return 0, ErrKeyNotFound
|
|
}
|
|
c.local.Delete(key)
|
|
return c.redis.Incr(ctx, key)
|
|
}
|
|
|
|
func (c *LayeredCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil
|
|
}
|
|
return c.redis.Expire(ctx, key, ttl)
|
|
}
|
|
|
|
func (c *LayeredCache) SetLocalOnly(key string, value any, ttl time.Duration) {
|
|
if !c.enabled {
|
|
return
|
|
}
|
|
c.local.Set(key, value, ttl)
|
|
}
|
|
|
|
func (c *LayeredCache) GetLocalOnly(key string) (any, bool) {
|
|
if !c.enabled {
|
|
return nil, false
|
|
}
|
|
return c.local.Get(key)
|
|
}
|
|
|
|
func (c *LayeredCache) SetRedisOnly(key string, value any, ttl time.Duration) {
|
|
if !c.enabled || c.redis == nil {
|
|
return
|
|
}
|
|
c.redis.Set(key, value, ttl)
|
|
}
|
|
|
|
func (c *LayeredCache) GetRedisOnly(key string) (any, bool) {
|
|
if !c.enabled || c.redis == nil {
|
|
return nil, false
|
|
}
|
|
return c.redis.Get(key)
|
|
}
|
|
|
|
func (c *LayeredCache) GetRedisClient() *redisPkg.Client {
|
|
if c.redis == nil {
|
|
return nil
|
|
}
|
|
return c.redis.GetRedisClient()
|
|
}
|
|
|
|
func GetTypedLayered[T any](c *LayeredCache, key string) (T, bool) {
|
|
var zero T
|
|
if !c.enabled {
|
|
return zero, false
|
|
}
|
|
|
|
raw, ok := c.Get(key)
|
|
if !ok {
|
|
return zero, false
|
|
}
|
|
|
|
if typed, ok := raw.(T); ok {
|
|
return typed, true
|
|
}
|
|
|
|
if str, ok := raw.(string); ok {
|
|
var out T
|
|
if err := json.Unmarshal([]byte(str), &out); err != nil {
|
|
return zero, false
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
if data, err := json.Marshal(raw); err == nil {
|
|
var out T
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return zero, false
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
return zero, false
|
|
}
|
|
|
|
func GetOrLoadLayered[T any](
|
|
c *LayeredCache,
|
|
key string,
|
|
ttl time.Duration,
|
|
loader func() (T, error),
|
|
) (T, error) {
|
|
if cached, ok := GetTypedLayered[T](c, key); ok {
|
|
return cached, nil
|
|
}
|
|
|
|
loaded, err := loader()
|
|
if err != nil {
|
|
var zero T
|
|
return zero, err
|
|
}
|
|
|
|
c.Set(key, loaded, ttl)
|
|
return loaded, nil
|
|
}
|
|
|
|
func (c *LayeredCache) InvalidateUserCache(userID uint) {
|
|
c.DeleteByPrefix(cacheKeyPrefixUser + ":")
|
|
c.Delete(buildUserDetailKey(userID))
|
|
}
|
|
|
|
func (c *LayeredCache) InvalidatePostCache(postID uint) {
|
|
c.Delete(buildPostDetailKey(postID))
|
|
c.DeleteByPrefix(cacheKeyPrefixPostList)
|
|
}
|
|
|
|
func (c *LayeredCache) InvalidateGroupCache(groupID uint) {
|
|
c.Delete(buildGroupDetailKey(groupID))
|
|
c.Delete(buildGroupMembersKey(groupID))
|
|
}
|
|
|
|
func (c *LayeredCache) InvalidateAllLists() {
|
|
c.DeleteByPrefix(cacheKeyPrefixPostList)
|
|
c.DeleteByPrefix(cacheKeyPrefixUserActivity)
|
|
}
|
|
|
|
var (
|
|
cacheKeyPrefixUser = "user"
|
|
cacheKeyPrefixPostList = "post_list"
|
|
cacheKeyPrefixUserActivity = "user_activity"
|
|
)
|
|
|
|
func buildUserDetailKey(userID uint) string {
|
|
return "user:detail:" + strconv.FormatUint(uint64(userID), 10)
|
|
}
|
|
|
|
func buildPostDetailKey(postID uint) string {
|
|
return "post:detail:" + strconv.FormatUint(uint64(postID), 10)
|
|
}
|
|
|
|
func buildGroupDetailKey(groupID uint) string {
|
|
return "group:detail:" + strconv.FormatUint(uint64(groupID), 10)
|
|
}
|
|
|
|
func buildGroupMembersKey(groupID uint) string {
|
|
return "group:members:" + strconv.FormatUint(uint64(groupID), 10)
|
|
}
|
|
|
|
var _ Cache = (*LayeredCache)(nil)
|
|
|
|
func (c *LayeredCache) logStats() {
|
|
stats := c.GetStats()
|
|
zap.L().Debug("LayeredCache stats",
|
|
zap.Int64("local_hits", stats.LocalHits),
|
|
zap.Int64("redis_hits", stats.RedisHits),
|
|
zap.Int64("misses", stats.Misses),
|
|
zap.Int64("sets", stats.Sets),
|
|
zap.Int64("invalidates", stats.Invalidates),
|
|
zap.Float64("hit_rate", c.GetHitRate()),
|
|
)
|
|
}
|