27 KiB
PanSou 缓存系统设计详解
1. 缓存系统概述
缓存系统是PanSou性能优化的核心组件,通过两级缓存(内存+磁盘)机制,大幅提升重复查询的响应速度。该系统采用分层设计,实现了高效的缓存存取和智能的缓存策略。
PanSou的缓存系统包括两个主要部分:
- 通用缓存系统:用于API响应和常规搜索结果缓存
- 异步插件缓存系统:专为异步插件设计的高级缓存机制
2. 目录结构
pansou/util/cache/
├── cache_key.go # 优化的缓存键生成
├── cache_key_test.go # 缓存键单元测试
├── disk_cache.go # 磁盘缓存实现
├── two_level_cache.go # 两级缓存实现
├── utils.go # 缓存工具函数
└── utils_test.go # 缓存工具测试
pansou/plugin/
├── baseasyncplugin.go # 异步插件缓存实现
pansou/util/json/
└── json.go # 基于sonic的高性能JSON处理封装
3. 缓存架构设计
3.1 两级缓存架构
PanSou采用两级缓存架构,包括内存缓存和磁盘缓存:
┌─────────────────────────┐
│ 搜索请求 │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 缓存键生成 │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 内存缓存查询 │
└───────────┬─────────────┘
│ (未命中)
┌───────────▼─────────────┐
│ 磁盘缓存查询 │
└───────────┬─────────────┘
│ (未命中)
┌───────────▼─────────────┐
│ 执行搜索 │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 更新内存缓存 │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 异步更新磁盘缓存 │
└─────────────────────────┘
3.2 缓存层次职责
-
内存缓存:
- 提供快速访问
- 存储热点数据
- 减少磁盘I/O
-
磁盘缓存:
- 提供持久存储
- 存储更多数据
- 在服务重启后保留缓存
4. 缓存键设计
4.1 缓存键生成(cache_key.go)
缓存键生成是缓存系统的基础,决定了缓存的命中率和有效性。
// GenerateCacheKey 根据所有影响搜索结果的参数生成缓存键
func GenerateCacheKey(keyword string, channels []string, sourceType string, plugins []string) string {
// 关键词标准化
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword))
// 获取频道列表哈希
channelsHash := getChannelsHash(channels)
// 源类型处理
if sourceType == "" {
sourceType = "all"
}
// 插件参数规范化处理
var pluginsHash string
if sourceType == "tg" {
// 对于只搜索Telegram的请求,忽略插件参数
pluginsHash = "none"
} else {
// 获取插件列表哈希
pluginsHash = getPluginsHash(plugins)
}
// 生成最终缓存键
keyStr := fmt.Sprintf("%s:%s:%s:%s", normalizedKeyword, channelsHash, sourceType, pluginsHash)
hash := md5.Sum([]byte(keyStr))
return hex.EncodeToString(hash[:])
}
4.2 缓存键设计思想
- 标准化处理:对关键词进行标准化,确保相同语义的查询使用相同的缓存键
- 参数敏感:缓存键包含影响结果的参数(如搜索频道、来源类型、插件列表),避免错误的缓存命中
- 排序处理:对数组参数进行排序,确保参数顺序不同但内容相同的查询使用相同的缓存键
- 哈希处理:对大型列表使用哈希处理,减小缓存键长度,提高性能
- 参数规范化:统一处理不同形式但语义相同的参数,提高缓存命中率
4.3 列表参数处理
// 获取或计算插件哈希
func getPluginsHash(plugins []string) string {
// 检查是否为空列表
if plugins == nil || len(plugins) == 0 {
// 使用预计算的所有插件哈希
if hash, ok := precomputedHashes.Load("all_plugins"); ok {
return hash.(string)
}
return allPluginsHash
}
// 检查是否有空字符串元素
hasNonEmptyPlugin := false
for _, p := range plugins {
if p != "" {
hasNonEmptyPlugin = true
break
}
}
// 如果全是空字符串,也视为空列表
if !hasNonEmptyPlugin {
if hash, ok := precomputedHashes.Load("all_plugins"); ok {
return hash.(string)
}
return allPluginsHash
}
// 对于小型列表,直接使用字符串连接
if len(plugins) < 5 {
pluginsCopy := make([]string, 0, len(plugins))
for _, p := range plugins {
if p != "" { // 忽略空字符串
pluginsCopy = append(pluginsCopy, p)
}
}
sort.Strings(pluginsCopy)
// 检查是否有预计算的哈希
key := strings.Join(pluginsCopy, ",")
if hash, ok := precomputedHashes.Load("plugins:"+key); ok {
return hash.(string)
}
return strings.Join(pluginsCopy, ",")
}
// 生成排序后的字符串用作键,忽略空字符串
pluginsCopy := make([]string, 0, len(plugins))
for _, p := range plugins {
if p != "" { // 忽略空字符串
pluginsCopy = append(pluginsCopy, p)
}
}
sort.Strings(pluginsCopy)
key := strings.Join(pluginsCopy, ",")
// 尝试从缓存获取
if hash, ok := pluginHashCache.Load(key); ok {
return hash.(string)
}
// 计算哈希
hash := calculateListHash(pluginsCopy)
// 存入缓存
pluginHashCache.Store(key, hash)
return hash
}
4.4 预计算哈希优化
// 初始化预计算的哈希值
func init() {
// 预计算空列表的哈希值
precomputedHashes.Store("empty_channels", "all")
// 预计算常用的频道组合哈希值
commonChannels := [][]string{
{"dongman", "anime"},
{"movie", "film"},
{"music", "audio"},
{"book", "ebook"},
}
for _, channels := range commonChannels {
key := strings.Join(channels, ",")
hash := calculateListHash(channels)
precomputedHashes.Store("channels:"+key, hash)
}
// 预计算常用的插件组合哈希值
commonPlugins := [][]string{
{"pan666", "panta"},
{"aliyun", "baidu"},
}
for _, plugins := range commonPlugins {
key := strings.Join(plugins, ",")
hash := calculateListHash(plugins)
precomputedHashes.Store("plugins:"+key, hash)
}
// 预计算所有插件的哈希值
allPlugins := plugin.GetRegisteredPlugins()
allPluginNames := make([]string, 0, len(allPlugins))
for _, p := range allPlugins {
allPluginNames = append(allPluginNames, p.Name())
}
sort.Strings(allPluginNames)
allPluginsHash = calculateListHash(allPluginNames)
precomputedHashes.Store("all_plugins", allPluginsHash)
}
5. 缓存一致性优化
5.1 参数规范化处理
为确保不同形式但语义相同的参数生成相同的缓存键,系统实现了以下规范化处理:
-
插件参数规范化:
- 不传plugins参数
- 传空plugins数组
- 传只包含空字符串的plugins数组
- 传所有插件名称
这四种情况都被统一处理,生成相同的缓存键。
-
搜索类型规范化:
- 对于
sourceType=tg的请求,忽略插件参数,使用固定值"none" - 对于
sourceType=all或sourceType=plugin的请求,根据插件参数内容决定缓存键
- 对于
-
参数预处理:
- 在
Search函数中添加参数预处理逻辑,确保不同形式的参数产生相同的搜索结果 - 对于包含所有注册插件的请求,统一设为nil,与不指定插件的请求使用相同的缓存键
- 在
5.2 缓存键测试
func TestPluginParameterNormalization(t *testing.T) {
// 获取所有插件名称
allPlugins := plugin.GetRegisteredPlugins()
allPluginNames := make([]string, 0, len(allPlugins))
for _, p := range allPlugins {
allPluginNames = append(allPluginNames, p.Name())
}
// 测试不传插件参数
key1 := GenerateCacheKey("movie", nil, "all", nil)
// 测试传空插件数组
key2 := GenerateCacheKey("movie", nil, "all", []string{})
// 测试传只包含空字符串的插件数组
key3 := GenerateCacheKey("movie", nil, "all", []string{""})
// 测试传所有插件
key4 := GenerateCacheKey("movie", nil, "all", allPluginNames)
// 所有情况应该生成相同的缓存键
if key1 != key2 || key1 != key3 || key1 != key4 {
t.Errorf("Different plugin parameter forms should generate the same cache key:\nnil: %s\nempty: %s\nempty string: %s\nall plugins: %s",
key1, key2, key3, key4)
}
// 测试sourceType=tg时忽略插件参数
key5 := GenerateCacheKey("movie", nil, "tg", nil)
key6 := GenerateCacheKey("movie", nil, "tg", allPluginNames)
if key5 != key6 {
t.Errorf("With sourceType=tg, plugin parameters should be ignored: %s != %s", key5, key6)
}
}
6. 内存缓存设计
6.1 内存缓存实现(memory_cache.go)
内存缓存提供快速访问,减少磁盘I/O,适合存储热点数据。
// MemoryCache 内存缓存
type MemoryCache struct {
cache map[string]cacheItem
mutex sync.RWMutex
maxSize int64
currSize int64
}
// cacheItem 缓存项
type cacheItem struct {
data []byte
expireAt time.Time
size int64
}
// NewMemoryCache 创建新的内存缓存
func NewMemoryCache(maxSizeMB int) *MemoryCache {
maxSize := int64(maxSizeMB) * 1024 * 1024
return &MemoryCache{
cache: make(map[string]cacheItem),
maxSize: maxSize,
}
}
// Get 从内存缓存获取数据
func (c *MemoryCache) Get(key string) ([]byte, bool, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
item, ok := c.cache[key]
if !ok {
return nil, false, nil
}
// 检查是否过期
if time.Now().After(item.expireAt) {
return nil, false, nil
}
return item.data, true, nil
}
// Set 将数据存入内存缓存
func (c *MemoryCache) Set(key string, data []byte, ttl time.Duration) error {
c.mutex.Lock()
defer c.mutex.Unlock()
size := int64(len(data))
// 如果数据太大,超过最大缓存大小,不缓存
if size > c.maxSize {
return nil
}
// 检查是否需要腾出空间
if c.currSize+size > c.maxSize {
c.evict(c.currSize + size - c.maxSize)
}
// 存储数据
c.cache[key] = cacheItem{
data: data,
expireAt: time.Now().Add(ttl),
size: size,
}
c.currSize += size
return nil
}
// 腾出空间
func (c *MemoryCache) evict(sizeToFree int64) {
// 按过期时间排序
type keyExpire struct {
key string
expireAt time.Time
}
items := make([]keyExpire, 0, len(c.cache))
for k, v := range c.cache {
items = append(items, keyExpire{k, v.expireAt})
}
// 按过期时间排序,先过期的先删除
sort.Slice(items, func(i, j int) bool {
return items[i].expireAt.Before(items[j].expireAt)
})
// 删除足够的项目以腾出空间
freed := int64(0)
for _, item := range items {
if freed >= sizeToFree {
break
}
cacheItem := c.cache[item.key]
freed += cacheItem.size
c.currSize -= cacheItem.size
delete(c.cache, item.key)
}
}
7. 两级缓存实现
7.1 两级缓存(two_level_cache.go)
两级缓存整合内存缓存和磁盘缓存,提供统一的接口。
// TwoLevelCache 两级缓存
type TwoLevelCache struct {
memCache *MemoryCache
diskCache *DiskCache
}
// NewTwoLevelCache 创建新的两级缓存
func NewTwoLevelCache() (*TwoLevelCache, error) {
// 获取配置
maxSizeMB := 100 // 默认100MB
if sizeStr := os.Getenv("CACHE_MAX_SIZE"); sizeStr != "" {
if size, err := strconv.Atoi(sizeStr); err == nil {
maxSizeMB = size
}
}
// 创建内存缓存
memCache := NewMemoryCache(maxSizeMB)
// 创建磁盘缓存
diskCache, err := NewDiskCache()
if err != nil {
return nil, err
}
return &TwoLevelCache{
memCache: memCache,
diskCache: diskCache,
}, nil
}
// Get 从缓存获取数据
func (c *TwoLevelCache) Get(key string) ([]byte, bool, error) {
// 先查内存缓存
data, hit, err := c.memCache.Get(key)
if hit || err != nil {
return data, hit, err
}
// 内存未命中,查磁盘缓存
data, hit, err = c.diskCache.Get(key)
if err != nil {
return nil, false, err
}
// 如果磁盘命中,更新内存缓存
if hit {
// 使用较短的TTL,因为这只是内存缓存
c.memCache.Set(key, data, 10*time.Minute)
}
return data, hit, nil
}
// Set 将数据存入缓存
func (c *TwoLevelCache) Set(key string, data []byte, ttl time.Duration) error {
// 更新内存缓存
if err := c.memCache.Set(key, data, ttl); err != nil {
return err
}
// 异步更新磁盘缓存
go c.diskCache.Set(key, data, ttl)
return nil
}
8. 序列化优化
8.1 高性能JSON处理(util/json包)
为提高序列化和反序列化性能,系统封装了bytedance/sonic库,提供高性能的JSON处理功能:
// pansou/util/json/json.go
package json
import (
"github.com/bytedance/sonic"
)
// API是sonic的全局配置实例
var API = sonic.ConfigDefault
// 初始化sonic配置
func init() {
// 根据需要配置sonic选项
API = sonic.Config{
UseNumber: true,
EscapeHTML: true,
SortMapKeys: false, // 生产环境设为false提高性能
}.Froze()
}
// Marshal 使用sonic序列化对象到JSON
func Marshal(v interface{}) ([]byte, error) {
return API.Marshal(v)
}
// Unmarshal 使用sonic反序列化JSON到对象
func Unmarshal(data []byte, v interface{}) error {
return API.Unmarshal(data, v)
}
// MarshalString 序列化对象到JSON字符串
func MarshalString(v interface{}) (string, error) {
bytes, err := API.Marshal(v)
if err != nil {
return "", err
}
return string(bytes), nil
}
// UnmarshalString 反序列化JSON字符串到对象
func UnmarshalString(str string, v interface{}) error {
return API.Unmarshal([]byte(str), v)
}
该包的主要特点:
- 高性能:基于bytedance/sonic库,比标准库encoding/json快5-10倍
- 统一接口:提供与标准库兼容的接口,便于系统内统一使用
- 优化配置:预配置了适合生产环境的sonic选项
- 字符串处理:额外提供字符串序列化/反序列化方法,减少内存分配
8.2 序列化工具(utils.go)
为提高序列化和反序列化性能,系统使用高性能JSON库并实现对象池化。
var (
// 缓冲区对象池
bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
)
// SerializeWithPool 使用对象池序列化数据
func SerializeWithPool(v interface{}) ([]byte, error) {
// 从对象池获取缓冲区
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufferPool.Put(buf)
// 使用高性能JSON库序列化
if err := sonic.ConfigDefault.NewEncoder(buf).Encode(v); err != nil {
return nil, err
}
// 复制数据,因为缓冲区会被重用
data := make([]byte, buf.Len())
copy(data, buf.Bytes())
return data, nil
}
// DeserializeWithPool 使用对象池反序列化数据
func DeserializeWithPool(data []byte, v interface{}) error {
return sonic.ConfigDefault.Unmarshal(data, v)
}
9. 缓存系统优化历程
9.1 第一阶段:缓存键生成优化
-
实现新的
GenerateCacheKey函数:- 使用哈希处理大型列表
- 实现参数排序确保顺序不变性
- 统一空值处理方式
-
添加缓存键单元测试:
- 验证参数顺序不变性
- 验证空值处理一致性
-
优化哈希计算:
- 对大型列表使用MD5哈希处理
- 添加哈希缓存映射避免重复计算
9.2 第二阶段:JSON序列化优化
-
高性能JSON库:
- 使用
github.com/bytedance/sonic高性能JSON库
- 使用
-
缓冲区对象池:
- 实现缓冲区对象池,减少内存分配
- 创建
SerializeWithPool和DeserializeWithPool函数
-
性能测试:
- 对比优化前后的序列化性能
- 验证对象池化方法的效果
9.3 第三阶段:缓存写入优化
-
异步缓存写入:
- 内存缓存在主线程执行
- 磁盘缓存移至goroutine异步执行
-
预计算哈希缓存:
- 缓存频道和插件组合的哈希值
- 提前计算常用组合的哈希值
9.4 第四阶段:缓存键一致性优化
-
插件参数规范化处理:
- 统一处理不传plugins参数、传空plugins数组、传只包含空字符串的plugins数组、传所有插件名称这几种情况
- 对于
sourceType=tg的请求,忽略插件参数,使用固定值"none"
-
Search函数优化:
- 添加参数预处理逻辑,确保不同形式的插件参数产生相同的搜索结果
- 对于包含所有注册插件的请求,统一设为nil,与不指定插件的请求使用相同的缓存键
-
HTTP请求处理优化:
- 区分"不传plugins参数"和"传空plugins值"这两种情况
- 对于
sourceType=all的请求,如果plugins为空或不存在,统一设为nil
-
单元测试:
- 添加
TestPluginParameterNormalization测试用例,验证不同形式的插件参数生成相同的缓存键
- 添加
10. 性能指标
10.1 缓存命中率
- 内存缓存命中率:约85%(热点查询)
- 磁盘缓存命中率:约10%(非热点查询)
- 总体命中率:约95%
10.2 响应时间
- 缓存命中:平均响应时间 < 50ms
- 缓存未命中:平均响应时间约6-12秒(取决于查询复杂度和网络状况)
- 性能提升:缓存命中时响应时间减少约99%
10.3 资源消耗
- 内存占用:约100MB(可配置)
- 磁盘占用:约1GB(取决于查询量和缓存TTL)
- CPU使用率:缓存命中时几乎为0,缓存未命中时约20-30%
11. 异步插件缓存系统
异步插件缓存系统是为解决慢速插件响应问题而设计的专门缓存机制,实现了"尽快响应,持续处理"的异步模式。
11.1 异步缓存架构
┌─────────────────────────┐
│ 搜索请求 │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ 异步缓存查询 │
└───────────┬─────────────┘
│ (命中)
├───────────────────┐
│ │
┌───────────▼─────────────┐ │
│ 返回缓存结果 │ │
└───────────┬─────────────┘ │
│ │
│ (接近过期) │
│ │
┌───────────▼─────────────┐ │
│ 后台刷新缓存 │ │
└─────────────────────────┘ │
│
│ (未命中) │
│ │
┌───────────▼─────────────┐ │
│ 启动双通道处理 │ │
└───────────┬─────────────┘ │
│ │
┌──────┴──────┐ │
│ │ │
┌────▼────┐ ┌────▼────┐ │
│快速响应 │ │后台处理│ │
│(短超时) │ │(长超时) │ │
└────┬────┘ └────┬────┘ │
│ │ │
│ │ │
┌────▼────┐ ┌────▼────┐ │
│返回结果 │ │更新缓存│ │
└─────────┘ └────┬────┘ │
│ │
▼ │
┌─────────────────────────┐ │
│ 持久化到磁盘 │◄───┘
└─────────────────────────┘
11.2 异步缓存机制设计
11.2.1 缓存结构
// 缓存响应结构
type cachedResponse struct {
Results []model.SearchResult `json:"results"`
Timestamp time.Time `json:"timestamp"`
Complete bool `json:"complete"`
LastAccess time.Time `json:"last_access"`
AccessCount int `json:"access_count"`
}
// 可序列化的缓存结构,用于持久化
type persistentCache struct {
Entries map[string]cachedResponse
}
11.2.2 缓存键设计
异步插件缓存使用插件特定的缓存键,确保不同插件的缓存不会相互干扰:
// 生成插件特定的缓存键
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, cacheKey)
11.2.3 双级超时控制
异步缓存系统实现了双级超时控制:
- 响应超时(默认2秒):确保快速响应用户请求
- 处理超时(默认30秒):允许后台处理有足够时间完成
// 默认配置值
defaultAsyncResponseTimeout = 2 * time.Second
defaultPluginTimeout = 30 * time.Second
11.3 缓存持久化
11.3.1 定期保存
缓存系统会定期将内存中的缓存保存到磁盘:
// 缓存保存间隔 (2分钟)
cacheSaveInterval = 2 * time.Minute
// 启动定期保存
func startCachePersistence() {
ticker := time.NewTicker(cacheSaveInterval)
defer ticker.Stop()
for range ticker.C {
if hasCacheItems() {
saveCacheToDisk()
}
}
}
11.3.2 即时保存
当缓存更新时,系统会触发即时保存:
// 更新缓存后立即触发保存
go saveCacheToDisk()
11.3.3 优雅关闭
系统实现了优雅关闭机制,确保在程序退出前保存缓存:
// 在main.go中
// 创建通道来接收操作系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 等待中断信号
<-quit
// 保存异步插件缓存
plugin.SaveCacheToDisk()
// 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("服务器关闭异常: %v", err)
}
11.4 智能缓存管理
11.4.1 基于得分的缓存淘汰
系统实现了基于多因素的缓存淘汰策略:
// 计算得分:访问次数 / (空闲时间的平方 * 年龄)
// 这样:
// - 访问频率高的得分高
// - 最近访问的得分高
// - 较新的缓存得分高
score := float64(item.AccessCount) / (idleTime.Seconds() * idleTime.Seconds() * age.Seconds())
11.4.2 访问统计
系统记录每个缓存项的访问情况:
// 记录缓存访问次数,用于智能缓存策略
func recordCacheAccess(key string) {
// 更新缓存项的访问时间和计数
if cached, ok := apiResponseCache.Load(key); ok {
cachedItem := cached.(cachedResponse)
cachedItem.LastAccess = time.Now()
cachedItem.AccessCount++
apiResponseCache.Store(key, cachedItem)
}
}
11.4.3 增量缓存更新
系统实现了新旧结果的智能合并:
// 创建合并结果集
mergedResults := make([]model.SearchResult, 0, len(results) + len(oldCachedResult.Results))
// 创建已有结果ID的映射
existingIDs := make(map[string]bool)
for _, r := range results {
existingIDs[r.UniqueID] = true
mergedResults = append(mergedResults, r)
}
// 添加旧结果中不存在的项
for _, r := range oldCachedResult.Results {
if !existingIDs[r.UniqueID] {
mergedResults = append(mergedResults, r)
}
}
11.4.4 后台自动刷新
对于接近过期的缓存,系统会在后台自动刷新:
// 如果缓存接近过期(已用时间超过TTL的80%),在后台刷新缓存
if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) {
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult)
}
11.5 资源管理
11.5.1 工作池控制
系统实现了工作池机制,限制并发任务数量:
// 工作池相关变量
backgroundWorkerPool chan struct{}
backgroundTasksCount int32 = 0
// 默认配置值
defaultMaxBackgroundWorkers = 20
defaultMaxBackgroundTasks = 100
// 尝试获取工作槽
func acquireWorkerSlot() bool {
// 获取最大任务数
maxTasks := int32(defaultMaxBackgroundTasks)
if config.AppConfig != nil {
maxTasks = int32(config.AppConfig.AsyncMaxBackgroundTasks)
}
// 检查总任务数
if atomic.LoadInt32(&backgroundTasksCount) >= maxTasks {
return false
}
// 尝试获取工作槽
select {
case backgroundWorkerPool <- struct{}{}:
atomic.AddInt32(&backgroundTasksCount, 1)
return true
default:
return false
}
}
11.5.2 统计监控
系统记录各种缓存操作的统计数据:
// 统计数据 (仅用于内部监控)
cacheHits int64 = 0
cacheMisses int64 = 0
asyncCompletions int64 = 0
11.6 配置选项
异步缓存系统提供了丰富的配置选项:
// 异步插件相关配置
AsyncPluginEnabled bool // 是否启用异步插件
AsyncResponseTimeout int // 响应超时时间(秒)
AsyncResponseTimeoutDur time.Duration // 响应超时时间(Duration)
AsyncMaxBackgroundWorkers int // 最大后台工作者数量
AsyncMaxBackgroundTasks int // 最大后台任务数量
AsyncCacheTTLHours int // 异步缓存有效期(小时)
11.7 性能指标
- 缓存命中时响应时间:< 50ms
- 缓存未命中时响应时间:约4秒(响应超时时间)
- 后台处理时间:最长30秒(处理超时时间)
- 缓存命中率:约90%(经过一段时间运行后)