优化搜索tg频道匹配网盘链接

This commit is contained in:
www.xueximeng.com
2025-07-21 18:51:58 +08:00
parent 997dc0cf4f
commit 42fbcb06aa
15 changed files with 682 additions and 584 deletions

View File

@@ -10,31 +10,14 @@ PanSou是一个高性能的网盘资源搜索API服务支持TG搜索和自定
- **异步插件系统**:支持通过插件扩展搜索来源,已内置多个网盘搜索插件,详情参考[插件开发指南.md](docs/插件开发指南.md);支持"尽快响应,持续处理"的异步搜索模,解决了某些搜索源响应时间长的问题 - **异步插件系统**:支持通过插件扩展搜索来源,已内置多个网盘搜索插件,详情参考[插件开发指南.md](docs/插件开发指南.md);支持"尽快响应,持续处理"的异步搜索模,解决了某些搜索源响应时间长的问题
- **双级超时控制**:短超时(4秒)确保快速响应,长超时(30秒)允许完整处理 - **双级超时控制**:短超时(4秒)确保快速响应,长超时(30秒)允许完整处理
- **持久化缓存**:缓存自动保存到磁盘,系统重启后自动恢复 - **持久化缓存**:缓存自动保存到磁盘,系统重启后自动恢复
- **即时保存**:缓存更新后立即触发保存,不再等待定时器
- **优雅关闭**:在程序退出前保存缓存,确保数据不丢失 - **优雅关闭**:在程序退出前保存缓存,确保数据不丢失
- **增量更新**:智能合并新旧结果,保留有价值的数据 - **增量更新**:智能合并新旧结果,保留有价值的数据
- **后台自动刷新**:对于接近过期的缓存,在后台自动刷新 - **主动更新**:异步插件在缓存异步更新后会主动更新主缓存(内存+磁盘),使用户在不强制刷新的情况下也能获取最新数据
- **异步二级缓存**:内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能,即使在不强制刷新的情况下也能获取异步插件更新的最新缓存数据 - **二级缓存**:内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能
1. **分片磁盘缓存** - **分片磁盘缓存**:将缓存数据分散到多个子目录,减少锁竞争,通过哈希算法将缓存键均匀分布到不同分片,提高高并发场景下的性能
- 将缓存数据分散到多个子目录,减少锁竞争 - **序列化器接口**Gob序列化提供更高性能和更小的结果大小
- 通过哈希算法将缓存键均匀分布到不同分片 - **分离的缓存键**TG搜索和插件搜索使用独立的缓存键实现独立更新互不影响提高缓存命中率和更新效率
- 提高高并发场景下的性能 - **优化的缓存读取策略**:优先使用内存缓存,其次从磁盘读取缓存数据
2. **序列化器接口**
- 统一的序列化和反序列化操作
- 支持Gob和JSON双序列化方式
- Gob序列化提供更高性能和更小的结果大小
3. **分离的缓存键**
- TG搜索和插件搜索使用独立的缓存键
- 实现独立更新,互不影响
- 提高缓存命中率和更新效率
4. **优化的缓存读取策略**
- 优先从磁盘读取数据,而不是优先使用内存缓存
- 每次查询前强制删除内存缓存,确保从磁盘读取最新数据
- 确保即使在不强制刷新的情况下也能获取异步插件更新的最新缓存数据
## 支持的网盘类型 ## 支持的网盘类型

View File

@@ -12,7 +12,6 @@ PanSou是一个高性能的网盘资源搜索API服务支持Telegram搜索和
- **网盘类型分类**:自动识别多种网盘链接,按类型归类展示 - **网盘类型分类**:自动识别多种网盘链接,按类型归类展示
- **增强版两级缓存**:内存+分片磁盘缓存机制,大幅提升重复查询速度 - **增强版两级缓存**:内存+分片磁盘缓存机制,大幅提升重复查询速度
- **插件系统**:支持通过插件扩展搜索来源 - **插件系统**:支持通过插件扩展搜索来源
- **缓存键一致性**:优化的缓存键生成算法,确保相同语义查询的缓存命中
- **异步插件缓存更新**:支持在不强制刷新的情况下获取异步插件的最新缓存数据 - **异步插件缓存更新**:支持在不强制刷新的情况下获取异步插件的最新缓存数据
### 1.2 技术栈 ### 1.2 技术栈
@@ -20,10 +19,8 @@ PanSou是一个高性能的网盘资源搜索API服务支持Telegram搜索和
- **编程语言**Go - **编程语言**Go
- **Web框架**Gin - **Web框架**Gin
- **缓存**:自定义增强版两级缓存(内存+分片磁盘) - **缓存**:自定义增强版两级缓存(内存+分片磁盘)
- **序列化**Gob和JSON双序列化支持
- **JSON处理**sonic高性能JSON库 - **JSON处理**sonic高性能JSON库
- **并发控制**:工作池模式 - **并发控制**:工作池模式
- **HTTP客户端**自定义优化的HTTP客户端
## 2. 系统架构 ## 2. 系统架构
@@ -34,22 +31,22 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ API 层 │ │ API 层 │
│ (路由、处理器、中间件) │ │ (路由、处理器、中间件)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 服务层 │ │ 服务层
│ (搜索服务、缓存) │ │ (搜索服务、缓存)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌─────────────────┐ ┌───────────▼─────────────┐ ┌─────────────────┐
│ 插件系统 │◄───┤ 插件注册表 │ │ 插件系统 │◄───┤ 插件注册表 │
│ (搜索插件、插件管理器) │ └─────────────────┘ │ (搜索插件、插件管理器) │ └─────────────────┘
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 工具层 │ │ 工具层
│ (缓存、HTTP客户端、工作池)│ │ (缓存、HTTP客户端、工作池)
└─────────────────────────┘ └─────────────────────────┘
``` ```
@@ -69,7 +66,6 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
- **结果处理**:过滤、排序和分类搜索结果 - **结果处理**:过滤、排序和分类搜索结果
- **缓存管理**管理搜索结果的缓存策略支持TG和插件搜索的独立缓存 - **缓存管理**管理搜索结果的缓存策略支持TG和插件搜索的独立缓存
- **缓存键生成**:基于所有影响结果的参数生成一致的缓存键 - **缓存键生成**:基于所有影响结果的参数生成一致的缓存键
- **缓存读取优化**:优先从磁盘读取数据,确保获取异步插件更新的最新缓存数据
- **主缓存注入**:将主缓存系统注入到异步插件中,实现统一的缓存更新 - **主缓存注入**:将主缓存系统注入到异步插件中,实现统一的缓存更新
#### 2.2.3 插件系统 #### 2.2.3 插件系统
@@ -79,7 +75,6 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
- **自动注册**通过init函数实现插件自动注册 - **自动注册**通过init函数实现插件自动注册
- **高性能JSON处理**使用sonic库优化JSON序列化/反序列化 - **高性能JSON处理**使用sonic库优化JSON序列化/反序列化
- **异步插件**:支持"尽快响应,持续处理"的异步搜索模式 - **异步插件**:支持"尽快响应,持续处理"的异步搜索模式
- **缓存键一致性**:确保异步插件和主缓存系统使用相同格式的缓存键
#### 2.2.4 工具层 #### 2.2.4 工具层
@@ -95,7 +90,7 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
2. **参数处理**:解析、验证和规范化请求参数 2. **参数处理**:解析、验证和规范化请求参数
3. **缓存键生成**分别为TG搜索和插件搜索生成独立的缓存键 3. **缓存键生成**分别为TG搜索和插件搜索生成独立的缓存键
4. **缓存检查**:检查是否有缓存结果 4. **缓存检查**:检查是否有缓存结果
5. **缓存读取优化**:优先从磁盘读取数据,确保获取最新的缓存数据 5. **缓存读取优化**:优先从内存读取数据
6. **并发搜索**:如无缓存,并发执行搜索任务 6. **并发搜索**:如无缓存,并发执行搜索任务
7. **结果处理**:过滤、排序和分类搜索结果 7. **结果处理**:过滤、排序和分类搜索结果
8. **缓存存储**:将结果存入缓存 8. **缓存存储**:将结果存入缓存
@@ -106,10 +101,10 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
### 3.1 高性能设计 ### 3.1 高性能设计
- **并发搜索**:使用工作池模式实现高效并发 - **并发搜索**:使用工作池模式实现高效并发
- **增强版两级缓存**:内存缓存提供快速访问,分片磁盘缓存提供持久存储和高并发支持 - **两级缓存**:内存缓存提供快速访问,分片磁盘缓存提供持久存储和高并发支持
- **异步操作**:非关键路径使用异步处理 - **异步操作**:非关键路径使用异步处理
- **内存优化**预分配策略、对象池化、GC参数优化 - **内存优化**预分配策略、对象池化、GC参数优化
- **高效序列化**使用Gob序列化提高性能保留JSON序列化兼容性 - **高效序列化**使用Gob序列化提高性能
- **高效JSON处理**使用sonic库替代标准库提升序列化性能 - **高效JSON处理**使用sonic库替代标准库提升序列化性能
### 3.2 可扩展性设计 ### 3.2 可扩展性设计
@@ -125,7 +120,6 @@ PanSou采用模块化的分层架构设计主要包括以下几个层次
- **错误处理**:全面的错误捕获和处理 - **错误处理**:全面的错误捕获和处理
- **优雅降级**:单个搜索源失败不影响整体结果 - **优雅降级**:单个搜索源失败不影响整体结果
- **缓存一致性**:确保相同语义的查询使用相同的缓存键 - **缓存一致性**:确保相同语义的查询使用相同的缓存键
- **双缓存机制**:优先使用增强版缓存,失败时回退到原始缓存
## 4. 代码组织结构 ## 4. 代码组织结构
@@ -135,16 +129,9 @@ pansou/
│ ├── handler.go # 请求处理器 │ ├── handler.go # 请求处理器
│ ├── middleware.go # 中间件 │ ├── middleware.go # 中间件
│ └── router.go # 路由定义 │ └── router.go # 路由定义
├── cache/ # 缓存数据存储目录
├── config/ # 配置管理 ├── config/ # 配置管理
│ └── config.go # 配置定义和加载 │ └── config.go # 配置定义和加载
├── docs/ # 文档 ├── docs/ # 文档
│ ├── 1-项目总体架构设计.md # 总体架构文档
│ ├── 2-API层设计.md # API层设计文档
│ ├── 3-服务层设计.md # 服务层设计文档
│ ├── 4-插件系统设计.md # 插件系统设计文档
│ ├── 5-缓存系统设计.md # 缓存系统设计文档
│ └── 插件开发指南.md # 插件开发指南
├── model/ # 数据模型 ├── model/ # 数据模型
│ ├── request.go # 请求模型 │ ├── request.go # 请求模型
│ └── response.go # 响应模型 │ └── response.go # 响应模型

View File

@@ -26,7 +26,7 @@ var (
) )
// 优先关键词列表 // 优先关键词列表
var priorityKeywords = []string{"全", "合集", "系列", "完", "最新", "附", "花园墙外"} var priorityKeywords = []string{"全", "合集", "系列", "完", "最新", "附"}
// 初始化缓存 // 初始化缓存
func init() { func init() {
@@ -381,9 +381,6 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
// 优先使用增强版缓存 // 优先使用增强版缓存
if enhancedTwoLevelCache != nil { if enhancedTwoLevelCache != nil {
// 强制删除内存缓存,确保每次都从磁盘读取最新数据
enhancedTwoLevelCache.Delete(cacheKey)
// 使用Get方法它会优先从磁盘读取数据 // 使用Get方法它会优先从磁盘读取数据
data, hit, err = enhancedTwoLevelCache.Get(cacheKey) data, hit, err = enhancedTwoLevelCache.Get(cacheKey)
@@ -505,16 +502,6 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
} }
``` ```
### 3.6 优化的缓存读取策略
插件搜索方法中实现了优化的缓存读取策略,确保即使在不强制刷新的情况下也能获取最新的缓存数据:
1. **强制删除内存缓存**:每次查询前先删除内存缓存,确保从磁盘读取最新数据
2. **优先从磁盘读取**`EnhancedTwoLevelCache.Get` 方法优先从磁盘读取数据,而不是优先使用内存缓存
3. **缓存键一致性**:确保异步插件和主缓存系统使用相同格式的缓存键
这种策略特别适用于异步插件场景,因为异步插件会在后台持续更新缓存数据。通过优先从磁盘读取数据,系统能够确保用户总是获取到最新的搜索结果,同时保持良好的性能。
## 4. 结果处理 ## 4. 结果处理
### 4.1 过滤和排序 ### 4.1 过滤和排序

View File

@@ -143,12 +143,13 @@ func (p *JikePanPlugin) Priority() int {
```go ```go
// BaseAsyncPlugin 基础异步插件结构 // BaseAsyncPlugin 基础异步插件结构
type BaseAsyncPlugin struct { type BaseAsyncPlugin struct {
name string name string // 插件名称
priority int priority int // 优先级
client *http.Client // 用于短超时客户端 client *http.Client // 短超时客户端
backgroundClient *http.Client // 用于长超时客户端 backgroundClient *http.Client // 长超时客户端
cacheTTL time.Duration // 缓存有效期 cacheTTL time.Duration // 缓存有效期
mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数
MainCacheKey string // 主缓存键,导出字段,由主程序设置
} }
// NewBaseAsyncPlugin 创建基础异步插件 // NewBaseAsyncPlugin 创建基础异步插件
@@ -193,28 +194,44 @@ func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.
// AsyncSearch 异步搜索基础方法 // AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch( func (p *BaseAsyncPlugin) AsyncSearch(
keyword string, keyword string,
cacheKey string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error), searchFunc func(*http.Client, string) ([]model.SearchResult, error),
mainCacheKey string, // 主缓存key参数
) ([]model.SearchResult, error) { ) ([]model.SearchResult, error) {
now := time.Now() now := time.Now()
// 生成标准缓存键,用于主缓存系统
// 使用与cache.GeneratePluginCacheKey完全相同的格式
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword))
// 使用nil作为plugins参数表示使用所有插件
// 这与cache.GeneratePluginCacheKey(keyword, nil)的行为相同
keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword)
hash := md5.Sum([]byte(keyStr))
mainCacheKey := hex.EncodeToString(hash[:])
// 修改缓存键,确保包含插件名称 // 修改缓存键,确保包含插件名称
// 使用原始关键词而不是cacheKey参数确保与主缓存系统使用相同的键格式
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword) pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword)
// 检查缓存 // 检查缓存
if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok { if cachedItems, ok := apiResponseCache.Load(pluginSpecificCacheKey); ok {
// 处理缓存命中逻辑... cachedResult := cachedItems.(cachedResponse)
// 缓存完全有效(未过期且完整)
if time.Since(cachedResult.Timestamp) < p.cacheTTL && cachedResult.Complete {
recordCacheHit()
recordCacheAccess(pluginSpecificCacheKey)
// 如果缓存接近过期已用时间超过TTL的80%),在后台刷新缓存
if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) {
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey)
}
return cachedResult.Results, nil
}
// 缓存已过期但有结果,启动后台刷新,同时返回旧结果
if len(cachedResult.Results) > 0 {
recordCacheHit()
recordCacheAccess(pluginSpecificCacheKey)
// 标记为部分过期
if time.Since(cachedResult.Timestamp) >= p.cacheTTL {
// 在后台刷新缓存
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey)
}
return cachedResult.Results, nil
}
} }
// 创建通道 // 创建通道
@@ -229,6 +246,10 @@ func (p *BaseAsyncPlugin) AsyncSearch(
// 工作池已满,使用快速响应客户端直接处理 // 工作池已满,使用快速响应客户端直接处理
results, err := searchFunc(p.client, keyword) results, err := searchFunc(p.client, keyword)
// 处理结果... // 处理结果...
// 更新主缓存系统
p.updateMainCache(mainCacheKey, results)
return return
} }
defer releaseWorkerSlot() defer releaseWorkerSlot()
@@ -241,9 +262,15 @@ func (p *BaseAsyncPlugin) AsyncSearch(
case <-doneChan: case <-doneChan:
// 已经响应,只更新缓存 // 已经响应,只更新缓存
// ... // ...
// 更新主缓存系统
p.updateMainCache(mainCacheKey, results)
default: default:
// 尚未响应,发送结果 // 尚未响应,发送结果
// ... // ...
// 更新主缓存系统
p.updateMainCache(mainCacheKey, results)
} }
}() }()
@@ -263,72 +290,50 @@ func (p *BaseAsyncPlugin) AsyncSearch(
} }
``` ```
### 5.3 缓存键生成优化 ### 5.3 缓存键传递机制
为了确保异步插件和主缓存系统使用相同的缓存键格式,我们对`AsyncSearch`方法进行了优化 为了确保异步插件和主缓存系统使用相同的缓存键,系统实现了缓存键传递机制
```go ```go
// AsyncSearch 异步搜索基础方法 // BaseAsyncPlugin 基础异步插件结构
func (p *BaseAsyncPlugin) AsyncSearch( type BaseAsyncPlugin struct {
keyword string, name string // 插件名称
cacheKey string, priority int // 优先级
searchFunc func(*http.Client, string) ([]model.SearchResult, error), client *http.Client // 短超时客户端
) ([]model.SearchResult, error) { backgroundClient *http.Client // 长超时客户端
now := time.Now() cacheTTL time.Duration // 缓存有效期
mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数
// 生成标准缓存键,用于主缓存系统 MainCacheKey string // 主缓存键,导出字段,由主程序设置
// 使用与cache.GeneratePluginCacheKey完全相同的格式 }
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword))
// SetMainCacheKey 设置主缓存键
// 使用nil作为plugins参数表示使用所有插件 func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
// 这与cache.GeneratePluginCacheKey(keyword, nil)的行为相同 p.MainCacheKey = key
keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword)
hash := md5.Sum([]byte(keyStr))
mainCacheKey := hex.EncodeToString(hash[:])
// 修改缓存键,确保包含插件名称
// 使用原始关键词而不是cacheKey参数确保与主缓存系统使用相同的键格式
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword)
// ... 其余代码 ...
} }
``` ```
关键的优化点是: 关键的优化点是:
1. **统一缓存键格式**确保异步插件和主缓存系统使用相同的缓存键格式 1. **缓存键传递**主程序通过`SetMainCacheKey`方法将缓存键传递给插件
2. **使用原始关键词**:在生成`pluginSpecificCacheKey`时使用原始关键词而不是`cacheKey`参数 2. **统一缓存键**:插件直接使用传递的缓存键,而不是自己重新生成
3. **主缓存更新**:在更新缓存时,同时更新内部缓存和主缓存系统 3. **避免缓存不一致**:确保异步插件和主缓存系统使用完全相同的缓存键
```go ```go
// updateMainCache 更新主缓存系统 // AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.SearchResult) { func (p *BaseAsyncPlugin) AsyncSearch(
// 如果主缓存更新函数为空,直接返回 keyword string,
if p.mainCacheUpdater == nil { searchFunc func(*http.Client, string) ([]model.SearchResult, error),
return mainCacheKey string, // 主缓存key参数
} ) ([]model.SearchResult, error) {
// ...
// 直接使用传入的cacheKey作为主缓存的键 // 更新主缓存系统
mainCacheKey := cacheKey p.updateMainCache(mainCacheKey, results)
// 序列化结果 // ...
data, err := json.Marshal(results)
if err != nil {
fmt.Printf("[%s] 序列化结果失败: %v\n", p.name, err)
return
}
// 调用主缓存更新函数
if err := p.mainCacheUpdater(mainCacheKey, data, p.cacheTTL); err != nil {
fmt.Printf("[%s] 更新主缓存失败: %v\n", p.name, err)
} else {
fmt.Printf("[%s] 成功更新主缓存: %s\n", p.name, mainCacheKey)
}
} }
``` ```
这些优化确保了异步插件和主缓存系统之间的一致性,解决了缓存键不匹配的问题,使系统能够在不强制刷新的情况下获取异步插件更新的最新缓存数据。
### 5.4 异步插件缓存机制 ### 5.4 异步插件缓存机制
异步插件系统实现了高级缓存机制: 异步插件系统实现了高级缓存机制:
@@ -336,7 +341,6 @@ func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.Searc
1. **持久化存储**:缓存定期保存到磁盘,服务重启时自动加载 1. **持久化存储**:缓存定期保存到磁盘,服务重启时自动加载
2. **智能缓存管理**:基于访问频率、时间和热度的缓存淘汰策略 2. **智能缓存管理**:基于访问频率、时间和热度的缓存淘汰策略
3. **增量更新**:新旧结果智能合并,保留唯一标识符不同的结果 3. **增量更新**:新旧结果智能合并,保留唯一标识符不同的结果
4. **后台刷新**:接近过期的缓存会在后台自动刷新
```go ```go
// 缓存响应结构 // 缓存响应结构
@@ -364,69 +368,41 @@ func loadCacheFromDisk() {
### 5.5 异步插件实现示例 ### 5.5 异步插件实现示例
```go ```go
// JikePanPlugin 即刻盘异步搜索插件 // MyAsyncPlugin 自定义异步搜索插件
type JikePanPlugin struct { type MyAsyncPlugin struct {
*plugin.BaseAsyncPlugin *plugin.BaseAsyncPlugin
} }
// NewJikePanPlugin 创建即刻盘插件 // NewMyAsyncPlugin 创建自定义插件
func NewJikePanPlugin() *JikePanPlugin { func NewMyAsyncPlugin() *MyAsyncPlugin {
return &JikePanPlugin{ return &MyAsyncPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("jikepan", 5), BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myasyncplugin", 5),
} }
} }
// Search 实现搜索接口 // Search 实现搜索接口
func (p *JikePanPlugin) Search(keyword string) ([]model.SearchResult, error) { func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用原始关键词作为缓存键 // 使用保存的主缓存键
// 注意不需要额外处理AsyncSearch方法会自动生成标准格式的缓存键 return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
return p.BaseAsyncPlugin.AsyncSearch(keyword, keyword, p.doSearch)
} }
// doSearch 执行实际搜索 // doSearch 执行实际搜索
func (p *JikePanPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) { func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
// 实现搜索逻辑 // 实现搜索逻辑
// ... // ...
return results, nil return results, nil
} }
``` ```
### 5.6 优化的缓存读取策略 ### 5.6 异步插件与主程序缓存协同
为了确保即使在不强制刷新的情况下也能获取最新的缓存数据,系统实现了优化的缓存读取策略:
```go
// 在searchPlugins方法中
// 强制删除内存缓存,确保每次都从磁盘读取最新数据
enhancedTwoLevelCache.Delete(cacheKey)
// 使用Get方法它会优先从磁盘读取数据
data, hit, err = enhancedTwoLevelCache.Get(cacheKey)
if err == nil && hit {
var results []model.SearchResult
if err := enhancedTwoLevelCache.GetSerializer().Deserialize(data, &results); err == nil {
// 返回缓存数据
return results, nil
}
}
```
这种策略通过以下方式确保获取最新的缓存数据:
1. **强制删除内存缓存**:每次查询前先删除内存缓存,确保从磁盘读取最新数据
2. **优先从磁盘读取**`EnhancedTwoLevelCache.Get` 方法优先从磁盘读取数据,而不是优先使用内存缓存
这种策略特别适用于异步插件场景,因为异步插件会在后台持续更新缓存数据。通过优先从磁盘读取数据,系统能够确保用户总是获取到最新的搜索结果,同时保持良好的性能。
### 5.7 异步插件与主程序缓存协同
异步插件系统与主程序的缓存系统协同工作,实现了完整的缓存更新流程: 异步插件系统与主程序的缓存系统协同工作,实现了完整的缓存更新流程:
1. **缓存键一致性**异步插件和主缓存系统使用相同格式的缓存键 1. **缓存键传递**主程序在调用异步插件时传递主缓存键
2. **缓存注入**将主缓存系统注入到异步插件中,实现统一的缓存更新 2. **缓存键保存**主程序通过`SetMainCacheKey`方法将缓存键保存到插件的`MainCacheKey`字段
3. **优先从磁盘读取**:每次查询前删除内存缓存,强制从磁盘读取最新数据 3. **直接使用**:插件在`Search`方法中直接使用`p.MainCacheKey`
4. **异步缓存更新**:后台处理完成后更新缓存,供后续查询使用 4. **缓存更新**异步插件在后台处理完成后,使用保存的主缓存键更新缓存
5. **缓存一致性**:确保异步插件缓存和主缓存保持一致
```go ```go
// 主缓存注入示例 // 主缓存注入示例
@@ -589,11 +565,8 @@ func NewMyAsyncPlugin() *MyAsyncPlugin {
// Search 实现搜索接口 // Search 实现搜索接口
func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 生成缓存键 // 使用保存的主缓存键
cacheKey := generateCacheKey(keyword) return p.BaseAsyncPlugin.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
// 使用异步搜索
return p.BaseAsyncPlugin.AsyncSearch(keyword, cacheKey, p.doSearch)
} }
// doSearch 执行实际搜索 // doSearch 执行实际搜索
@@ -602,4 +575,4 @@ func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.S
// ... // ...
return results, nil return results, nil
} }
``` ```

View File

@@ -47,31 +47,31 @@ PanSou采用增强版两级缓存架构包括内存缓存和分片磁盘缓
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 搜索请求 │ │ 搜索请求
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 缓存键生成 │ │ 缓存键生成
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 内存缓存查询 │ │ 内存缓存查询
└───────────┬─────────────┘ └───────────┬─────────────┘
│ (未命中) │ (未命中)
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 分片磁盘缓存查询 │ │ 分片磁盘缓存查询
└───────────┬─────────────┘ └───────────┬─────────────┘
│ (未命中) │ (未命中)
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 执行搜索 │ │ 执行搜索
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 更新内存缓存 │ │ 更新内存缓存
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 异步更新分片磁盘缓存 │ │ 异步更新分片磁盘缓存
└─────────────────────────┘ └─────────────────────────┘
``` ```
@@ -134,31 +134,31 @@ PanSou采用的增强版两级缓存架构结合了内存缓存和磁盘缓存
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 搜索请求 │ │ 搜索请求
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 缓存键生成 │ │ 缓存键生成
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 缓存检查 │ │ 缓存检查
└───────────┬─────────────┘ └───────────┬─────────────┘
│ (未命中) │ (未命中)
┌───────────┬─────────────┐ ┌───────────┬─────────────┐
快速响应通道 │ 后台处理通道 │ │ 快速响应通道│ 后台处理通道 │
└───────┬────────────┬───┘ └───────┬────────────┬───┘
│ │ │ │
┌───────▼───────┐ ┌───▼───────────┐ ┌───────▼───────┐ ┌───▼───────────┐
│ 响应超时返回 │ │ 完整处理 │ │ 响应超时返回 │ │ 完整处理 │
└───────┬───────┘ └───┬───────────┘ └───────┬───────┘ └───┬───────────┘
│ │ │ │
┌───────▼───────┐ ┌───▼───────────┐ ┌───────▼───────┐ ┌───▼───────────┐
│ 返回部分结果 │ │ 更新插件缓存 │ │ 返回部分结果 │ │ 更新插件缓存
└───────────────┘ └───┬───────────┘ └───────────────┘ └───┬───────────┘
┌───▼───────────┐ ┌───▼───────────┐
│ 更新主缓存 │ │ 更新主缓存
└───────────────┘ └───────────────┘
``` ```
@@ -191,7 +191,7 @@ type EnhancedTwoLevelCache struct {
} }
``` ```
增强版两级缓存是整个缓存系统的核心,它整合了内存缓存和分片磁盘缓存,提供了统一的缓存接口。其特点是优先从磁盘读取数据,确保获取最新的缓存数据。 增强版两级缓存是整个缓存系统的核心,它整合了内存缓存和分片磁盘缓存,提供了统一的缓存接口。
#### 3.2.2 内存缓存 #### 3.2.2 内存缓存
@@ -281,6 +281,25 @@ type JSONSerializer struct {
#### 3.2.6 异步插件缓存 #### 3.2.6 异步插件缓存
```go ```go
// 缓存相关变量
var (
// API响应缓存键为关键词值为缓存的响应
apiResponseCache = sync.Map{}
// 最后一次清理缓存的时间
lastCacheCleanTime = time.Now()
// 最后一次保存缓存的时间
lastCacheSaveTime = time.Now()
// 缓存保存锁,防止并发保存导致的竞态条件
saveCacheLock sync.Mutex
// 工作池相关变量
backgroundWorkerPool chan struct{}
backgroundTasksCount int32 = 0
)
// 缓存响应结构 // 缓存响应结构
type cachedResponse struct { type cachedResponse struct {
Results []model.SearchResult `json:"results"` // 搜索结果 Results []model.SearchResult `json:"results"` // 搜索结果
@@ -298,10 +317,11 @@ type BaseAsyncPlugin struct {
backgroundClient *http.Client // 长超时客户端 backgroundClient *http.Client // 长超时客户端
cacheTTL time.Duration // 缓存有效期 cacheTTL time.Duration // 缓存有效期
mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数
MainCacheKey string // 主缓存键,导出字段,由主程序设置
} }
``` ```
异步插件缓存是为异步插件设计的专用缓存系统,它实现了"尽快响应,持续处理"的异步模式。通过`mainCacheUpdater`函数,异步插件可以直接更新主缓存系统,确保缓存一致性。 异步插件缓存是为异步插件设计的专用缓存系统,它实现了"尽快响应,持续处理"的异步模式。通过`mainCacheUpdater`函数,异步插件可以直接更新主缓存系统,确保缓存一致性。主程序通过`SetMainCacheKey`方法将缓存键传递给插件并存储在`MainCacheKey`字段中,插件直接使用这个字段而不再重新生成缓存键,避免了缓存键不一致的问题。
### 3.3 分片策略设计 ### 3.3 分片策略设计
@@ -334,22 +354,83 @@ func (c *ShardedDiskCache) getShard(key string) *DiskCache {
2. **均匀分布**:缓存键均匀分布到各个分片 2. **均匀分布**:缓存键均匀分布到各个分片
3. **隔离故障**:单个分片的问题不会影响其他分片 3. **隔离故障**:单个分片的问题不会影响其他分片
### 3.4 关键数据流转 ### 3.4 并发控制与竞态条件处理
#### 3.4.1 缓存写入流程 #### 3.4.1 互斥锁保护缓存保存
为了防止多个goroutine同时保存缓存导致的竞态条件系统使用互斥锁保护`saveCacheToDisk`函数:
```go
// 缓存保存锁,防止并发保存导致的竞态条件
saveCacheLock sync.Mutex
// saveCacheToDisk 将缓存保存到磁盘
func saveCacheToDisk() {
// 使用互斥锁确保同一时间只有一个goroutine可以执行
saveCacheLock.Lock()
defer saveCacheLock.Unlock()
// ... 缓存保存逻辑 ...
}
```
这种设计确保:
1. 同一时间只有一个goroutine可以执行缓存保存操作
2. 避免多个goroutine同时创建和重命名临时文件导致的冲突
3. 防止缓存文件损坏或不一致
#### 3.4.2 随机延迟减少冲突
在后台刷新缓存时,系统添加了随机延迟,进一步减少并发冲突的可能性:
```go
// refreshCacheInBackground 在后台刷新缓存
func (p *BaseAsyncPlugin) refreshCacheInBackground(...) {
// ... 缓存刷新逻辑 ...
// 添加随机延迟避免多个goroutine同时调用saveCacheToDisk
time.Sleep(time.Duration(100+rand.Intn(500)) * time.Millisecond)
// 更新缓存后立即触发保存
go saveCacheToDisk()
}
```
这种随机延迟机制可以:
1. 错开多个goroutine的缓存保存时间
2. 减少对互斥锁的竞争
3. 提高系统在高并发场景下的稳定性
#### 3.4.3 sync.Map的无锁设计
异步插件缓存使用`sync.Map`存储缓存项而不是普通的map加锁
```go
// API响应缓存键为关键词值为缓存的响应
apiResponseCache = sync.Map{}
```
`sync.Map`的优势:
1. 针对读多写少的场景优化
2. 无需显式加锁,减少锁竞争
3. 支持并发读取和更新操作
### 3.5 关键数据流转
#### 3.5.1 缓存写入流程
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 搜索结果生成 │ │ 搜索结果生成
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 序列化数据 │ │ 序列化数据
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────┬─────────────┐ ┌───────────┬─────────────
│ 更新内存缓存异步更新磁盘缓存 │ 更新内存缓存异步更新磁盘缓存│
└─────────────┘└─────────────┘ └─────────────────────────┘
``` ```
1. **搜索结果生成**:系统生成搜索结果 1. **搜索结果生成**:系统生成搜索结果
@@ -357,36 +438,32 @@ func (c *ShardedDiskCache) getShard(key string) *DiskCache {
3. **更新内存缓存**:立即更新内存缓存 3. **更新内存缓存**:立即更新内存缓存
4. **异步更新磁盘缓存**:在后台异步更新磁盘缓存,不阻塞主流程 4. **异步更新磁盘缓存**:在后台异步更新磁盘缓存,不阻塞主流程
#### 3.4.2 缓存读取流程(优化版) #### 3.5.2 缓存读取流程
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 缓存键生成 │ │ 缓存键生成
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
强制删除内存缓存 检查内存缓存
└───────────┬─────────────┘
│ (未命中)
┌───────────▼─────────────┐
│ 使用磁盘数据更新内存 │
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
检查磁盘缓存 反序列化数据
└───────────┬─────────────┘
│ (命中)
┌───────────▼─────────────┐
│ 使用磁盘数据更新内存 │
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
反序列化数据 返回缓存结果
└───────────┬─────────────┘
┌───────────▼─────────────┐
│ 返回缓存结果 │
└─────────────────────────┘ └─────────────────────────┘
``` ```
1. **缓存键生成**:根据搜索参数生成缓存键 1. **缓存键生成**:根据搜索参数生成缓存键
2. **强制删除内存缓存**确保从磁盘读取最新数据 2. **检查内存缓存**查询内存缓存是否命中,命中则直接返回缓存结果
3. **检查磁盘缓存**:查询磁盘缓存是否命中 3. **检查磁盘缓存**:查询磁盘缓存是否命中
4. **更新内存缓存**:使用磁盘数据更新内存缓存 4. **更新内存缓存**:使用磁盘数据更新内存缓存
5. **反序列化数据**:将字节数组转换回原始数据结构 5. **反序列化数据**:将字节数组转换回原始数据结构
@@ -621,23 +698,24 @@ func NewEnhancedTwoLevelCache() (*EnhancedTwoLevelCache, error) {
```go ```go
// Get 获取缓存 // Get 获取缓存
func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) { func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) {
// 首先尝试从磁盘读取数据
diskData, diskHit, diskErr := c.disk.Get(key) // 检查内存缓存
if diskErr == nil && diskHit { data, _, memHit := c.memory.GetWithTimestamp(key)
// 磁盘缓存命中,更新内存缓存 if memHit {
diskLastModified, _ := c.disk.GetLastModified(key) return data, true, nil
ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute }
c.memory.SetWithTimestamp(key, diskData, ttl, diskLastModified)
return diskData, true, nil // 尝试从磁盘读取数据
} diskData, diskHit, diskErr := c.disk.Get(key)
if diskErr == nil && diskHit {
// 磁盘命中,检查内存缓存 // 磁盘缓存命中,更新内存缓存
data, _, memHit := c.memory.GetWithTimestamp(key) diskLastModified, _ := c.disk.GetLastModified(key)
if memHit { ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute
return data, true, nil c.memory.SetWithTimestamp(key, diskData, ttl, diskLastModified)
} return diskData, true, nil
}
return nil, false, nil
return nil, false, nil
} }
``` ```
@@ -990,64 +1068,63 @@ func (c *DiskCache) getFilename(key string) string {
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 用户搜索请求 │ │ 用户搜索请求
│ (关键词:"电影") │ │ (关键词:"电影")
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 生成缓存键 │ │ 生成缓存键
│ "plugin:电影:all" │
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 检查内存缓存(未命中) │ │ 检查内存缓存(未命中)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 检查磁盘缓存(未命中) │ │ 检查磁盘缓存(未命中)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 并行执行搜索 │ │ 并行执行搜索
│ (TG搜索 + 插件搜索) │ │ (TG搜索 + 插件搜索)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────┬─────────────┐ ┌───────────┬─────────────┐
│ 常规插件 异步插件 │ │ 常规插件 异步插件 │
└─────┬─────┘┌────┬───────┘ └─────┬─────┘┌────┬───────┘
│ │ │ │ │ │
│ │ │ 响应超时 │ │ │ 响应超时
│ │ ▼ │ │ ▼
│ │┌──────────┐ │ │┌──────────┐
│ ││返回部分 │ │ ││ 返回部分 │
│ ││结果或空 │ │ ││ 结果或空 │
│ │└────┬─────┘ │ │└────┬─────┘
│ │ │ 后台继续处理 │ │ │ 后台继续处理
│ │ ▼ │ │ ▼
│ │┌──────────┐ │ │┌──────────┐
│ ││完成搜索 │ │ ││ 完成搜索 │
│ │└────┬─────┘ │ │└────┬─────┘
│ │ │ │ │ │
│ ▼ ▼ │ ▼ ▼
┌─────▼────────────────────┐ ┌─────▼────────────────────┐
│ 合并所有结果 │ │ 合并所有结果
└───────────┬──────────────┘ └───────────┬──────────────┘
┌───────────▼──────────────┐ ┌───────────▼──────────────┐
│ 过滤和排序结果 │ │ 过滤和排序结果
└───────────┬──────────────┘ └───────────┬──────────────┘
┌───────────▼──────────────┐ ┌───────────▼──────────────┐
│ 更新内存缓存 │ │ 更新内存缓存
└───────────┬──────────────┘ └───────────┬──────────────┘
┌───────────▼──────────────┐ ┌───────────▼──────────────┐
│ 异步更新磁盘缓存 │ │ 异步更新磁盘缓存
└───────────┬──────────────┘ └───────────┬──────────────┘
┌───────────▼──────────────┐ ┌───────────▼──────────────┐
│ 返回结果给用户 │ 返回结果给用户 (100条)
└────────────────────────┘ └──────────────────────────
``` ```
**详细说明:** **详细说明:**
@@ -1111,30 +1188,20 @@ func (c *DiskCache) getFilename(key string) string {
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 用户搜索请求 │ │ 用户搜索请求
│ (关键词:"电影") │ │ (关键词:"电影")
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 生成缓存键 │ │ 生成缓存键
│ "plugin:电影:all" │
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 检查内存缓存(命中) │ │ 检查内存缓存(命中)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 返回缓存结果 │ 返回缓存结果 150条
└───────────┬─────────────┘
┌───────────▼─────────────┐
│ 检查缓存新鲜度 │
└───────────┬─────────────┘
│ 如果缓存接近过期(>80% TTL)
┌─────────────────────────┐
│ 后台异步刷新缓存 │
└─────────────────────────┘ └─────────────────────────┘
``` ```
@@ -1142,7 +1209,7 @@ func (c *DiskCache) getFilename(key string) string {
1. **缓存键生成**:与首次搜索相同,系统生成唯一的缓存键 1. **缓存键生成**:与首次搜索相同,系统生成唯一的缓存键
2. **内存缓存命中**:系统检查内存缓存,发现命中 2. **内存缓存命中**:系统检查内存缓存,发现命中。此时异步插件在陆续后台更新缓存,可能还没更新完毕。
```go ```go
data, _, memHit := c.memory.GetWithTimestamp(key) data, _, memHit := c.memory.GetWithTimestamp(key)
if memHit { if memHit {
@@ -1152,43 +1219,26 @@ func (c *DiskCache) getFilename(key string) string {
3. **返回缓存结果**:系统直接返回缓存结果,无需执行搜索 3. **返回缓存结果**:系统直接返回缓存结果,无需执行搜索
4. **缓存新鲜度检查**:系统检查缓存是否接近过期,如果是,则在后台刷新缓存
```go
// 如果缓存接近过期已用时间超过TTL的80%),在后台刷新缓存
if time.Since(cachedResult.Timestamp) > (p.cacheTTL * 4 / 5) {
go p.refreshCacheInBackground(keyword, pluginSpecificCacheKey, searchFunc, cachedResult, mainCacheKey)
}
```
### 7.3 第三次搜索(异步插件已更新缓存) ### 7.3 第三次搜索(异步插件已更新缓存)
当异步插件在后台完成处理并更新了缓存后,用户再次执行相同搜索时,系统会经历以下步骤: 当异步插件在后台完成处理并更新了缓存后,用户再次执行相同搜索时,系统会经历以下步骤:
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 用户搜索请求 │ │ 用户搜索请求
│ (关键词:"电影") │ │ (关键词:"电影")
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 生成缓存键 │ │ 生成缓存键
│ "plugin:电影:all" │
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
强制删除内存缓存 检查内存缓存(命中)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
检查磁盘缓存(命中) 返回更新后的结果 (500条)
└───────────┬─────────────┘
┌───────────▼─────────────┐
│ 使用磁盘数据更新内存 │
└───────────┬─────────────┘
┌───────────▼─────────────┐
│ 返回更新后的结果 │
└─────────────────────────┘ └─────────────────────────┘
``` ```
@@ -1196,27 +1246,7 @@ func (c *DiskCache) getFilename(key string) string {
1. **缓存键生成**:与之前相同,系统生成唯一的缓存键 1. **缓存键生成**:与之前相同,系统生成唯一的缓存键
2. **强制删除内存缓存**:系统强制删除内存缓存,确保从磁盘读取最新数据 2. **内存缓存命中**:系统检查内存缓存,发现命中。假设此时异步插件各自的插件缓存都更新完毕了,插件会主动更新主缓存,所以此时内存中的缓存数据也被更新过了
```go
// 强制删除内存缓存,确保每次都从磁盘读取最新数据
enhancedTwoLevelCache.Delete(cacheKey)
```
3. **磁盘缓存命中**:系统检查磁盘缓存,发现命中
```go
diskData, diskHit, diskErr := c.disk.Get(key)
if diskErr == nil && diskHit {
// 磁盘缓存命中
}
```
4. **更新内存缓存**:系统使用磁盘数据更新内存缓存
```go
// 磁盘缓存命中,更新内存缓存
diskLastModified, _ := c.disk.GetLastModified(key)
ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute
c.memory.SetWithTimestamp(key, diskData, ttl, diskLastModified)
```
5. **返回结果**:系统返回更新后的结果 5. **返回结果**:系统返回更新后的结果
@@ -1245,14 +1275,24 @@ func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, mainCach
### 8.2 缓存键一致性 ### 8.2 缓存键一致性
异步插件和主缓存系统使用相同格式的缓存键,确保缓存更新能够被正确识别 为了确保异步插件和主缓存系统使用相同的缓存键,系统实现了缓存键传递机制
```go ```go
// AsyncSearch方法中生成标准缓存键 // BaseAsyncPlugin 基础异步插件结构
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) type BaseAsyncPlugin struct {
keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword) name string // 插件名称
hash := md5.Sum([]byte(keyStr)) priority int // 优先级
mainCacheKey := hex.EncodeToString(hash[:]) client *http.Client // 短超时客户端
backgroundClient *http.Client // 长超时客户端
cacheTTL time.Duration // 缓存有效期
mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数
MainCacheKey string // 主缓存键,导出字段,由主程序设置
}
// SetMainCacheKey 设置主缓存键
func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
p.MainCacheKey = key
}
``` ```
### 8.3 异步更新机制 ### 8.3 异步更新机制
@@ -1306,11 +1346,10 @@ PanSou缓存系统是一套精心设计的多层次缓存解决方案通过
1. **增强版两级缓存**:结合内存缓存和分片磁盘缓存的优势,提供快速访问和持久存储 1. **增强版两级缓存**:结合内存缓存和分片磁盘缓存的优势,提供快速访问和持久存储
2. **分片磁盘缓存**:通过分片减少锁竞争,提高并发性能 2. **分片磁盘缓存**:通过分片减少锁竞争,提高并发性能
3. **优化的缓存读取策略**:优先从磁盘读取数据,确保获取最新的缓存数据 3. **异步写入机制**:缓存写入异步执行,不阻塞主流程
4. **异步写入机制**:缓存写入异步执行,不阻塞主流程 4. **智能缓存管理**:基于访问频率、时间和热度的缓存淘汰策略
5. **智能缓存管理**:基于访问频率、时间和热度的缓存淘汰策略 5. **异步插件缓存系统**:实现"尽快响应,持续处理"的异步模式
6. **异步插件缓存系统**:实现"尽快响应,持续处理"的异步模式
7. **主缓存与异步插件协同**:通过缓存注入和一致的缓存键格式,确保缓存一致性
### 9.2 性能优化亮点 ### 9.2 性能优化亮点

View File

@@ -209,7 +209,7 @@ type Link struct {
6. **URL** 6. **URL**
- 网盘链接的完整URL - 网盘链接的完整URL
- 必须包含协议部分如http://https:// - 必须包含协议部分(如 http://https://
- 例如:`https://pan.baidu.com/s/1abcdefg` - 例如:`https://pan.baidu.com/s/1abcdefg`
7. **Type** 7. **Type**
@@ -474,29 +474,48 @@ func NewMyPlugin() *MyPlugin {
超时时间通过环境变量 `PLUGIN_TIMEOUT` 配置,默认为 30 秒。 超时时间通过环境变量 `PLUGIN_TIMEOUT` 配置,默认为 30 秒。
## 异步插件开发 ## 6. 异步插件开发
对于响应时间较长的搜索源,建议使用异步插件模式,实现"尽快响应,持续处理"的搜索体验 异步插件是PanSou系统的高级功能实现"尽快响应,持续处理"的异步模式特别适合处理响应时间不稳定或较长的API
### 1. 异步插件架构 ### 1. 异步插件基础
异步插件基于 `BaseAsyncPlugin` 基类实现,具有以下特点 #### 1.1 异步插件特点
- **双级超时控制**:短超时确保快速响应,长超时允许完整处理 异步插件具有以下特点:
- **缓存机制**:自动缓存搜索结果,避免重复请求
- **后台处理**:响应超时后继续在后台处理请求
- **增量更新**:智能合并新旧结果,保留有价值的数据
- **资源管理**:通过工作池控制并发任务数量,避免资源耗尽
### 2. 创建异步插件 1. **快速响应**即使API响应较慢也能在超时时间内返回结果
2. **后台处理**:在返回初步结果后,继续在后台处理完整请求
3. **缓存更新**:后台处理完成后自动更新缓存
4. **智能缓存**:支持缓存新鲜度检查和自动刷新
5. **主缓存协同**:与主程序缓存系统协同工作,保持一致性
#### 2.1 基本结构 #### 1.2 异步插件结构
```go ```go
package myplugin_async // MyAsyncPlugin 自定义异步插件结构体
type MyAsyncPlugin struct {
*plugin.BaseAsyncPlugin
}
// NewMyAsyncPlugin 创建新的异步插件实例
func NewMyAsyncPlugin() *MyAsyncPlugin {
return &MyAsyncPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3),
}
}
```
### 2. 异步插件实现
#### 2.1 创建异步插件
```go
package myplugin
import ( import (
"net/http" "net/http"
"time"
"pansou/model" "pansou/model"
"pansou/plugin" "pansou/plugin"
@@ -525,9 +544,8 @@ func init() {
```go ```go
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果
func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { func (p *MyAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用原始关键词作为缓存键 // 使用保存的主缓存键
// 注意不需要额外处理AsyncSearch方法会自动生成标准格式的缓存键 return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
return p.AsyncSearch(keyword, keyword, p.doSearch)
} }
// doSearch 实际的搜索实现 // doSearch 实际的搜索实现
@@ -536,7 +554,16 @@ func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.S
// 注意client已经配置了适当的超时时间 // 注意client已经配置了适当的超时时间
// ... // ...
return results, nil // 获取搜索结果
results, err := actualSearch(client, keyword)
if err != nil {
return nil, err
}
// 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
} }
``` ```
@@ -558,18 +585,14 @@ func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.S
```go ```go
// 在AsyncSearch方法中 // 在AsyncSearch方法中
// 生成标准缓存键,用于主缓存系统 // 插件特定的缓存键
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword))
keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword)
hash := md5.Sum([]byte(keyStr))
mainCacheKey := hex.EncodeToString(hash[:])
// 修改缓存键,确保包含插件名称
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword) pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword)
// 主缓存键由主程序传递通过BaseAsyncPlugin的MainCacheKey字段直接使用
``` ```
这种设计确保了: 这种设计确保了:
1. **一致性**:异步插件和主缓存系统使用相同格式的缓存 1. **一致性**:异步插件直接使用主程序传递的缓存键,避免重复生成不一致的
2. **隔离性**:每个插件有自己的缓存命名空间 2. **隔离性**:每个插件有自己的缓存命名空间
3. **可追踪性**:缓存键包含插件名称,便于调试和监控 3. **可追踪性**:缓存键包含插件名称,便于调试和监控
@@ -620,15 +643,14 @@ type HunhepanAsyncPlugin struct {
// NewHunhepanAsyncPlugin 创建新的混合盘搜索异步插件 // NewHunhepanAsyncPlugin 创建新的混合盘搜索异步插件
func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin { func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin {
return &HunhepanAsyncPlugin{ return &HunhepanAsyncPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("hunhepan_async", 3), BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("hunhepan", 3),
} }
} }
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果
func (p *HunhepanAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { func (p *HunhepanAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用原始关键词作为缓存键 // 使用保存的主缓存键
// 注意不需要额外处理AsyncSearch方法会自动生成标准格式的缓存键 return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
return p.AsyncSearch(keyword, keyword, p.doSearch)
} }
// doSearch 实际的搜索实现 // doSearch 实际的搜索实现
@@ -686,7 +708,10 @@ func (p *HunhepanAsyncPlugin) doSearch(client *http.Client, keyword string) ([]m
// 转换为标准格式 // 转换为标准格式
results := p.convertResults(uniqueItems) results := p.convertResults(uniqueItems)
return results, nil // 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
} }
``` ```
@@ -694,54 +719,127 @@ func (p *HunhepanAsyncPlugin) doSearch(client *http.Client, keyword string) ([]m
异步插件系统与主程序的缓存系统协同工作,实现了完整的缓存更新流程: 异步插件系统与主程序的缓存系统协同工作,实现了完整的缓存更新流程:
1. **异步插件缓存更新**:异步插件在后台持续更新自己的缓存 1. **主缓存键传递**:主程序在调用异步插件时传递主缓存
2. **主程序缓存检查**:主程序在获取缓存数据时检查时间戳 2. **缓存键保存**:主程序通过`SetMainCacheKey`方法将缓存键保存到插件的`MainCacheKey`字段
3. **缓存更新判断**:如果缓存数据不是最新的,重新执行搜索 3. **直接使用**:插件在`Search`方法中直接使用`p.MainCacheKey`,不再重新生成缓存键
4. **缓存写入**搜索结果异步写入主程序缓存 4. **缓存更新**异步插件在后台处理完成后,使用保存的主缓存键更新主缓存
5. **缓存一致性**:确保异步插件缓存和主缓存保持一致
#### 5.6.1 缓存键一致性 ### 5.7 并发保护机制
为确保异步插件和主缓存系统之间的一致性,`BaseAsyncPlugin.AsyncSearch` 方法使用与 `cache.GeneratePluginCacheKey` 完全相同的格式生成缓存键 异步插件系统实现了多种并发保护机制,确保在高并发场景下的稳定性
1. **互斥锁保护**:使用`saveCacheLock`互斥锁保护缓存保存操作
2. **随机延迟**:在触发缓存保存前添加随机延迟,减少冲突
3. **无锁数据结构**:使用`sync.Map`存储缓存项,减少锁竞争
## 7. 结果过滤功能
PanSou插件系统提供了结果过滤功能可以根据搜索关键词过滤搜索结果提高结果的相关性。
### 7.1 过滤功能概述
过滤功能的主要目的是:
1. **提高相关性**:确保返回的结果与搜索关键词相关
2. **减少无关结果**:过滤掉与关键词无关的结果
3. **支持多关键词**:支持按空格分割的多个关键词过滤
### 7.2 过滤方法实现
BaseAsyncPlugin提供了`FilterResultsByKeyword`方法,用于过滤搜索结果:
```go ```go
// 生成标准缓存键,用于主缓存系统 // FilterResultsByKeyword 根据关键词过滤搜索结果
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword)) func (p *BaseAsyncPlugin) FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult {
keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword) if keyword == "" {
hash := md5.Sum([]byte(keyStr)) return results
mainCacheKey := hex.EncodeToString(hash[:]) }
// 预估过滤后会保留80%的结果
filteredResults := make([]model.SearchResult, 0, len(results)*8/10)
// 将关键词转为小写,用于不区分大小写的比较
lowerKeyword := strings.ToLower(keyword)
// 将关键词按空格分割,用于支持多关键词搜索
keywords := strings.Fields(lowerKeyword)
for _, result := range results {
// 将标题和内容转为小写
lowerTitle := strings.ToLower(result.Title)
lowerContent := strings.ToLower(result.Content)
// 检查每个关键词是否在标题或内容中
matched := true
for _, kw := range keywords {
// 对于所有关键词,检查是否在标题或内容中
if !strings.Contains(lowerTitle, kw) && !strings.Contains(lowerContent, kw) {
matched = false
break
}
}
if matched {
filteredResults = append(filteredResults, result)
}
}
return filteredResults
}
``` ```
#### 5.6.2 优化的缓存读取策略 ### 7.3 在插件中使用过滤功能
为了确保即使在不强制刷新的情况下也能获取异步插件更新的最新缓存数据,系统对缓存读取策略进行了优化 在异步插件的`doSearch`方法中,可以使用过滤功能
1. **优先从磁盘读取**`EnhancedTwoLevelCache.Get` 方法优先从磁盘读取数据,而不是优先使用内存缓存
2. **强制删除内存缓存**:在 `searchPlugins` 方法中,每次查询前先删除内存缓存,强制从磁盘读取最新数据
```go ```go
// 在searchPlugins方法中 // doSearch 实际的搜索实现
// 强制删除内存缓存,确保每次都从磁盘读取最新数据 func (p *MyAsyncPlugin) doSearch(client *http.Client, keyword string) ([]model.SearchResult, error) {
enhancedTwoLevelCache.Delete(cacheKey) // ... 执行搜索逻辑 ...
// 使用Get方法它会优先从磁盘读取数据 // 获取搜索结果
data, hit, err = enhancedTwoLevelCache.Get(cacheKey) results, err := actualSearch(client, keyword)
if err != nil {
return nil, err
}
// 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
}
``` ```
这种组合策略确保了系统能够在保持高性能的同时,始终提供最新的搜索结果,特别适合异步插件场景,因为异步插件会在后台持续更新缓存数据。 对于非异步插件,可以使用全局过滤函数:
### 6. 配置选项 ```go
// Search 执行搜索并返回结果
异步插件系统提供了多种配置选项,可通过环境变量设置: func (p *MyPlugin) Search(keyword string) ([]model.SearchResult, error) {
// ... 执行搜索逻辑 ...
```
ASYNC_PLUGIN_ENABLED=true # 是否启用异步插件 // 获取结果
ASYNC_RESPONSE_TIMEOUT=4 # 响应超时时间(秒) results, err := someSearchFunction(keyword)
ASYNC_MAX_BACKGROUND_WORKERS=20 # 最大后台工作者数量 if err != nil {
ASYNC_MAX_BACKGROUND_TASKS=100 # 最大后台任务数量 return nil, err
ASYNC_CACHE_TTL_HOURS=1 # 异步缓存有效期(小时) }
// 使用全局过滤函数过滤结果
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
}
``` ```
## 最佳实践 ### 7.4 过滤功能的性能考虑
过滤操作可能会消耗一定的CPU资源特别是当结果数量很大时。如果性能成为问题可以考虑以下优化
1. **提前过滤**在API返回大量结果时先进行初步过滤
2. **限制结果数量**:对于特别大的结果集,可以先限制数量再过滤
3. **优化字符串处理**:使用更高效的字符串匹配算法
4. **并行处理**:对大量结果进行并行过滤
## 8. 最佳实践
### 1. 错误处理 ### 1. 错误处理
@@ -1045,21 +1143,21 @@ type ApiItem struct {
``` ```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 插件缓存系统 │ │ 插件缓存系统
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 缓存存储层 │ │ 缓存存储层
│ (sync.Map实现的内存缓存) │ │ (sync.Map实现的内存缓存) │
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 缓存管理层 │ │ 缓存管理层
│ (缓存清理、过期策略、统计) │ │ (缓存清理、过期策略、统计)
└───────────┬─────────────┘ └───────────┬─────────────┘
┌───────────▼─────────────┐ ┌───────────▼─────────────┐
│ 缓存接口层 │ │ 缓存接口层
│ (Load/Store操作封装) │ │ (Load/Store操作封装) │
└─────────────────────────┘ └─────────────────────────┘
``` ```
@@ -1370,54 +1468,3 @@ func estimateCacheSize() int64 {
6. **并发安全**:使用线程安全的数据结构和原子操作 6. **并发安全**:使用线程安全的数据结构和原子操作
7. **定期清理**:实现自动清理机制,避免内存泄漏 7. **定期清理**:实现自动清理机制,避免内存泄漏
## 常见问题
### 1. 插件注册失败
**问题**:插件未被系统识别和加载
**解决方案**
- 确保在 `init()` 函数中调用了 `plugin.RegisterGlobalPlugin()`
- 确保在 `main.go` 中导入了插件包(使用空导入)
- 检查插件名称是否为空或重复
### 2. 搜索超时
**问题**:插件搜索经常超时
**解决方案**
- 调整插件的默认超时时间
- 使用并发请求减少总体响应时间
- 实现请求重试机制
- 优化请求逻辑,减少不必要的请求
### 3. 结果格式错误
**问题**:插件返回的结果格式不正确
**解决方案**
- 严格按照数据结构标准构造返回值
- 确保链接类型使用标准值
- 正确处理时间格式
- 清理HTML标签和特殊字符
### 4. 内存泄漏
**问题**:插件导致内存使用量持续增长
**解决方案**
- 确保所有goroutine都能正确退出
- 关闭HTTP响应体
- 避免无限循环
- 限制结果集大小
### 5. 错误处理不当
**问题**:插件错误影响了整个系统
**解决方案**
- 捕获并记录所有可能的错误
- 使用超时控制避免长时间阻塞
- 在返回错误前进行必要的资源清理
- 对于非致命错误,返回部分结果而不是完全失败

View File

@@ -2,9 +2,7 @@ package plugin
import ( import (
"compress/gzip" "compress/gzip"
"crypto/md5"
"encoding/gob" "encoding/gob"
"encoding/hex"
"pansou/util/json" "pansou/util/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -15,6 +13,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"math/rand"
"pansou/config" "pansou/config"
"pansou/model" "pansou/model"
@@ -44,6 +43,9 @@ var (
initialized bool = false initialized bool = false
initLock sync.Mutex initLock sync.Mutex
// 缓存保存锁,防止并发保存导致的竞态条件
saveCacheLock sync.Mutex
// 默认配置值 // 默认配置值
defaultAsyncResponseTimeout = 4 * time.Second defaultAsyncResponseTimeout = 4 * time.Second
defaultPluginTimeout = 30 * time.Second defaultPluginTimeout = 30 * time.Second
@@ -258,6 +260,10 @@ func getCachePath() string {
// saveCacheToDisk 将缓存保存到磁盘 // saveCacheToDisk 将缓存保存到磁盘
func saveCacheToDisk() { func saveCacheToDisk() {
// 使用互斥锁确保同一时间只有一个goroutine可以执行
saveCacheLock.Lock()
defer saveCacheLock.Unlock()
cacheFile := getCachePath() cacheFile := getCachePath()
lastCacheSaveTime = time.Now() lastCacheSaveTime = time.Now()
@@ -462,6 +468,7 @@ type BaseAsyncPlugin struct {
backgroundClient *http.Client // 用于长超时的客户端 backgroundClient *http.Client // 用于长超时的客户端
cacheTTL time.Duration // 缓存有效期 cacheTTL time.Duration // 缓存有效期
mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数 mainCacheUpdater func(string, []byte, time.Duration) error // 主缓存更新函数
MainCacheKey string // 主缓存键,导出字段
} }
// NewBaseAsyncPlugin 创建基础异步插件 // NewBaseAsyncPlugin 创建基础异步插件
@@ -496,6 +503,11 @@ func NewBaseAsyncPlugin(name string, priority int) *BaseAsyncPlugin {
} }
} }
// SetMainCacheKey 设置主缓存键
func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
p.MainCacheKey = key
}
// SetMainCacheUpdater 设置主缓存更新函数 // SetMainCacheUpdater 设置主缓存更新函数
func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.Duration) error) { func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []byte, time.Duration) error) {
p.mainCacheUpdater = updater p.mainCacheUpdater = updater
@@ -514,21 +526,11 @@ func (p *BaseAsyncPlugin) Priority() int {
// AsyncSearch 异步搜索基础方法 // AsyncSearch 异步搜索基础方法
func (p *BaseAsyncPlugin) AsyncSearch( func (p *BaseAsyncPlugin) AsyncSearch(
keyword string, keyword string,
cacheKey string,
searchFunc func(*http.Client, string) ([]model.SearchResult, error), searchFunc func(*http.Client, string) ([]model.SearchResult, error),
mainCacheKey string, // 主缓存key参数
) ([]model.SearchResult, error) { ) ([]model.SearchResult, error) {
now := time.Now() now := time.Now()
// 生成标准缓存键,用于主缓存系统
// 使用与cache.GeneratePluginCacheKey完全相同的格式
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword))
// 使用nil作为plugins参数表示使用所有插件
// 这与cache.GeneratePluginCacheKey(keyword, nil)的行为相同
keyStr := fmt.Sprintf("plugin:%s:all", normalizedKeyword)
hash := md5.Sum([]byte(keyStr))
mainCacheKey := hex.EncodeToString(hash[:])
// 修改缓存键,确保包含插件名称 // 修改缓存键,确保包含插件名称
pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword) pluginSpecificCacheKey := fmt.Sprintf("%s:%s", p.name, keyword)
@@ -648,9 +650,6 @@ func (p *BaseAsyncPlugin) AsyncSearch(
// 使用合并结果 // 使用合并结果
results = mergedResults results = mergedResults
// 日志记录
// fmt.Printf("[%s] 增量更新缓存: %s (新项目: %d, 合并项目: %d)\n", p.name, pluginSpecificCacheKey, len(existingIDs), len(mergedResults))
} }
} }
@@ -824,7 +823,7 @@ func (p *BaseAsyncPlugin) refreshCacheInBackground(
}) })
// 更新主缓存系统 // 更新主缓存系统
// 使用传入的原始缓存键而不是尝试处理cacheKey // 使用传入的originalCacheKey直接传递给updateMainCache
p.updateMainCache(originalCacheKey, mergedResults) p.updateMainCache(originalCacheKey, mergedResults)
// 记录刷新时间 // 记录刷新时间
@@ -832,20 +831,20 @@ func (p *BaseAsyncPlugin) refreshCacheInBackground(
fmt.Printf("[%s] 后台刷新完成: %s (耗时: %v, 新项目: %d, 合并项目: %d)\n", fmt.Printf("[%s] 后台刷新完成: %s (耗时: %v, 新项目: %d, 合并项目: %d)\n",
p.name, cacheKey, refreshTime, len(results), len(mergedResults)) p.name, cacheKey, refreshTime, len(results), len(mergedResults))
// 添加随机延迟避免多个goroutine同时调用saveCacheToDisk
time.Sleep(time.Duration(100+rand.Intn(500)) * time.Millisecond)
// 更新缓存后立即触发保存 // 更新缓存后立即触发保存
go saveCacheToDisk() go saveCacheToDisk()
} }
// updateMainCache 更新主缓存系统 // updateMainCache 更新主缓存系统
func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.SearchResult) { func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.SearchResult) {
// 如果主缓存更新函数为空,直接返回 // 如果主缓存更新函数为空或缓存键为空,直接返回
if p.mainCacheUpdater == nil { if p.mainCacheUpdater == nil || cacheKey == "" {
return return
} }
// 直接使用传入的cacheKey作为主缓存的键
mainCacheKey := cacheKey
// 序列化结果 // 序列化结果
data, err := json.Marshal(results) data, err := json.Marshal(results)
if err != nil { if err != nil {
@@ -854,9 +853,52 @@ func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []model.Searc
} }
// 调用主缓存更新函数 // 调用主缓存更新函数
if err := p.mainCacheUpdater(mainCacheKey, data, p.cacheTTL); err != nil { if err := p.mainCacheUpdater(cacheKey, data, p.cacheTTL); err != nil {
fmt.Printf("[%s] 更新主缓存失败: %v\n", p.name, err) fmt.Printf("[%s] 更新主缓存失败: %v\n", p.name, err)
} else { } else {
fmt.Printf("[%s] 成功更新主缓存: %s\n", p.name, mainCacheKey) fmt.Printf("[%s] 成功更新主缓存: %s\n", p.name, cacheKey)
} }
}
// FilterResultsByKeyword 根据关键词过滤搜索结果
func (p *BaseAsyncPlugin) FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult {
if keyword == "" {
return results
}
// 预估过滤后会保留80%的结果
filteredResults := make([]model.SearchResult, 0, len(results)*8/10)
// 将关键词转为小写,用于不区分大小写的比较
lowerKeyword := strings.ToLower(keyword)
// 将关键词按空格分割,用于支持多关键词搜索
keywords := strings.Fields(lowerKeyword)
for _, result := range results {
// 将标题和内容转为小写
lowerTitle := strings.ToLower(result.Title)
lowerContent := strings.ToLower(result.Content)
// 检查每个关键词是否在标题或内容中
matched := true
for _, kw := range keywords {
// 对于所有关键词,检查是否在标题或内容中
if !strings.Contains(lowerTitle, kw) && !strings.Contains(lowerContent, kw) {
matched = false
break
}
}
if matched {
filteredResults = append(filteredResults, result)
}
}
return filteredResults
}
// GetClient 返回短超时客户端
func (p *BaseAsyncPlugin) GetClient() *http.Client {
return p.client
} }

View File

@@ -44,11 +44,8 @@ func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin {
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果
func (p *HunhepanAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { func (p *HunhepanAsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用关键词作为缓存键让BaseAsyncPlugin统一处理缓存键生成 // 使用保存的主缓存键
cacheKey := keyword return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
// 使用异步搜索基础方法
return p.AsyncSearch(keyword, cacheKey, p.doSearch)
} }
// doSearch 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -38,11 +38,8 @@ func NewJikepanAsyncV2Plugin() *JikepanAsyncV2Plugin {
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果
func (p *JikepanAsyncV2Plugin) Search(keyword string) ([]model.SearchResult, error) { func (p *JikepanAsyncV2Plugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用关键词作为缓存键让BaseAsyncPlugin统一处理缓存键生成 // 使用保存的主缓存键
cacheKey := keyword return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
// 使用异步搜索基础方法
return p.AsyncSearch(keyword, cacheKey, p.doSearch)
} }
// doSearch 实际的搜索实现 // doSearch 实际的搜索实现

View File

@@ -57,11 +57,8 @@ func NewPan666AsyncPlugin() *Pan666AsyncPlugin {
// Search 执行搜索并返回结果 // Search 执行搜索并返回结果
func (p *Pan666AsyncPlugin) Search(keyword string) ([]model.SearchResult, error) { func (p *Pan666AsyncPlugin) Search(keyword string) ([]model.SearchResult, error) {
// 使用关键词作为缓存键让BaseAsyncPlugin统一处理缓存键生成 // 使用保存的主缓存键
cacheKey := keyword return p.AsyncSearch(keyword, p.doSearch, p.MainCacheKey)
// 使用异步搜索基础方法
return p.AsyncSearch(keyword, cacheKey, p.doSearch)
} }
// doSearch 实际的搜索实现 // doSearch 实际的搜索实现
@@ -78,7 +75,10 @@ func (p *Pan666AsyncPlugin) doSearch(client *http.Client, keyword string) ([]mod
// 去重 // 去重
uniqueResults := p.deduplicateResults(allResults) uniqueResults := p.deduplicateResults(allResults)
return uniqueResults, nil // 使用过滤功能过滤结果
filteredResults := p.FilterResultsByKeyword(uniqueResults, keyword)
return filteredResults, nil
} }
// fetchBatch 获取一批页面的数据 // fetchBatch 获取一批页面的数据

View File

@@ -297,7 +297,15 @@ func (p *PantaPlugin) Search(keyword string) ([]model.SearchResult, error) {
} }
// 解析搜索结果 // 解析搜索结果
return p.parseSearchResults(doc) results, err := p.parseSearchResults(doc)
if err != nil {
return nil, err
}
// 使用过滤功能过滤结果
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
} }
// parseSearchResults 使用goquery解析搜索结果 // parseSearchResults 使用goquery解析搜索结果

View File

@@ -1,8 +1,10 @@
package plugin package plugin
import ( import (
"pansou/model" "strings"
"sync" "sync"
"pansou/model"
) )
// 全局插件注册表 // 全局插件注册表
@@ -81,4 +83,43 @@ func (pm *PluginManager) RegisterAllGlobalPlugins() {
// GetPlugins 获取所有注册的插件 // GetPlugins 获取所有注册的插件
func (pm *PluginManager) GetPlugins() []SearchPlugin { func (pm *PluginManager) GetPlugins() []SearchPlugin {
return pm.plugins return pm.plugins
}
// FilterResultsByKeyword 根据关键词过滤搜索结果的全局辅助函数
// 供非BaseAsyncPlugin类型的插件使用
func FilterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult {
if keyword == "" {
return results
}
// 预估过滤后会保留80%的结果
filteredResults := make([]model.SearchResult, 0, len(results)*8/10)
// 将关键词转为小写,用于不区分大小写的比较
lowerKeyword := strings.ToLower(keyword)
// 将关键词按空格分割,用于支持多关键词搜索
keywords := strings.Fields(lowerKeyword)
for _, result := range results {
// 将标题和内容转为小写
lowerTitle := strings.ToLower(result.Title)
lowerContent := strings.ToLower(result.Content)
// 检查每个关键词是否在标题或内容中
matched := true
for _, kw := range keywords {
// 对于所有关键词,检查是否在标题或内容中
if !strings.Contains(lowerTitle, kw) && !strings.Contains(lowerContent, kw) {
matched = false
break
}
}
if matched {
filteredResults = append(filteredResults, result)
}
}
return filteredResults
} }

View File

@@ -1,9 +1,9 @@
package service package service
import ( import (
"context" // Added for context.WithTimeout "context"
"io/ioutil" "io/ioutil"
"net/http" // Added for http.Client "net/http"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -14,7 +14,7 @@ import (
"pansou/util" "pansou/util"
"pansou/util/cache" "pansou/util/cache"
"pansou/util/pool" "pansou/util/pool"
"sync" // Added for sync.WaitGroup "sync"
) )
// 优先关键词列表 // 优先关键词列表
@@ -188,7 +188,6 @@ func (s *SearchService) Search(keyword string, channels []string, concurrency in
tgResults, tgErr = s.searchTG(keyword, channels, forceRefresh) tgResults, tgErr = s.searchTG(keyword, channels, forceRefresh)
}() }()
} }
// 如果需要搜索插件 // 如果需要搜索插件
if sourceType == "all" || sourceType == "plugin" { if sourceType == "all" || sourceType == "plugin" {
wg.Add(1) wg.Add(1)
@@ -214,15 +213,12 @@ func (s *SearchService) Search(keyword string, channels []string, concurrency in
// 合并结果 // 合并结果
allResults := mergeSearchResults(tgResults, pluginResults) allResults := mergeSearchResults(tgResults, pluginResults)
// 过滤结果,确保标题包含搜索关键词
filteredResults := filterResultsByKeyword(allResults, keyword)
// 按照优化后的规则排序结果 // 按照优化后的规则排序结果
sortResultsByTimeAndKeywords(filteredResults) sortResultsByTimeAndKeywords(allResults)
// 过滤结果只保留有时间的结果或包含优先关键词的结果到Results中 // 过滤结果只保留有时间的结果或包含优先关键词的结果到Results中
filteredForResults := make([]model.SearchResult, 0, len(filteredResults)) filteredForResults := make([]model.SearchResult, 0, len(allResults))
for _, result := range filteredResults { for _, result := range allResults {
// 有时间的结果或包含优先关键词的结果保留在Results中 // 有时间的结果或包含优先关键词的结果保留在Results中
if !result.Datetime.IsZero() || getKeywordPriority(result.Title) > 0 { if !result.Datetime.IsZero() || getKeywordPriority(result.Title) > 0 {
filteredForResults = append(filteredForResults, result) filteredForResults = append(filteredForResults, result)
@@ -230,7 +226,7 @@ func (s *SearchService) Search(keyword string, channels []string, concurrency in
} }
// 合并链接按网盘类型分组(使用所有过滤后的结果) // 合并链接按网盘类型分组(使用所有过滤后的结果)
mergedLinks := mergeResultsByType(filteredResults) mergedLinks := mergeResultsByType(allResults)
// 构建响应 // 构建响应
var total int var total int
@@ -284,64 +280,6 @@ func filterResponseByType(response model.SearchResponse, resultType string) mode
} }
} }
// 过滤结果,确保标题包含搜索关键词
func filterResultsByKeyword(results []model.SearchResult, keyword string) []model.SearchResult {
// 预估过滤后会保留80%的结果
filteredResults := make([]model.SearchResult, 0, len(results)*8/10)
// 将关键词转为小写,用于不区分大小写的比较
lowerKeyword := strings.ToLower(keyword)
// 将关键词按空格分割,用于支持多关键词搜索
keywords := strings.Fields(lowerKeyword)
for _, result := range results {
// 将标题和内容转为小写
lowerTitle := strings.ToLower(result.Title)
lowerContent := strings.ToLower(result.Content)
// 检查每个关键词是否在标题或内容中
matched := true
for _, kw := range keywords {
// 如果关键词是"pwd",特殊处理,只要标题、内容或链接中包含即可
if kw == "pwd" {
// 检查标题、内容
pwdInTitle := strings.Contains(lowerTitle, kw)
pwdInContent := strings.Contains(lowerContent, kw)
// 检查链接中是否包含pwd参数
pwdInLinks := false
for _, link := range result.Links {
if strings.Contains(strings.ToLower(link.URL), "pwd=") {
pwdInLinks = true
break
}
}
// 只要有一个包含pwd就算匹配
if pwdInTitle || pwdInContent || pwdInLinks {
continue // 匹配成功,检查下一个关键词
} else {
matched = false
break
}
} else {
// 对于其他关键词,检查是否同时在标题和内容中
if !strings.Contains(lowerTitle, kw) && !strings.Contains(lowerContent, kw) {
matched = false
break
}
}
}
if matched {
filteredResults = append(filteredResults, result)
}
}
return filteredResults
}
// 根据时间和关键词排序结果 // 根据时间和关键词排序结果
func sortResultsByTimeAndKeywords(results []model.SearchResult) { func sortResultsByTimeAndKeywords(results []model.SearchResult) {
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
@@ -631,8 +569,6 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
// 优先使用增强版缓存 // 优先使用增强版缓存
if enhancedTwoLevelCache != nil { if enhancedTwoLevelCache != nil {
// 强制删除内存缓存,确保每次都从磁盘读取最新数据
enhancedTwoLevelCache.Delete(cacheKey)
// 使用Get方法它会检查磁盘缓存是否有更新 // 使用Get方法它会检查磁盘缓存是否有更新
// 如果磁盘缓存比内存缓存更新,会自动更新内存缓存并返回最新数据 // 如果磁盘缓存比内存缓存更新,会自动更新内存缓存并返回最新数据
@@ -710,11 +646,32 @@ func (s *SearchService) searchPlugins(keyword string, plugins []string, forceRef
for _, p := range availablePlugins { for _, p := range availablePlugins {
plugin := p // 创建副本,避免闭包问题 plugin := p // 创建副本,避免闭包问题
tasks = append(tasks, func() interface{} { tasks = append(tasks, func() interface{} {
results, err := plugin.Search(keyword) // 检查插件是否为异步插件
if err != nil { if asyncPlugin, ok := plugin.(interface {
return nil AsyncSearch(keyword string, searchFunc func(*http.Client, string) ([]model.SearchResult, error), mainCacheKey string) ([]model.SearchResult, error)
SetMainCacheKey(string)
}); ok {
// 先设置主缓存键
asyncPlugin.SetMainCacheKey(cacheKey)
// 是异步插件调用AsyncSearch方法并传递主缓存键
results, err := asyncPlugin.AsyncSearch(keyword, func(client *http.Client, kw string) ([]model.SearchResult, error) {
// 这里使用插件的Search方法作为搜索函数
return plugin.Search(kw)
}, cacheKey)
if err != nil {
return nil
}
return results
} else {
// 不是异步插件直接调用Search方法
results, err := plugin.Search(keyword)
if err != nil {
return nil
}
return results
} }
return results
}) })
} }

View File

@@ -60,7 +60,14 @@ func (c *EnhancedTwoLevelCache) Set(key string, data []byte, ttl time.Duration)
// Get 获取缓存 // Get 获取缓存
func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) { func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) {
// 首先尝试从磁盘读取数据
// 检查内存缓存
data, _, memHit := c.memory.GetWithTimestamp(key)
if memHit {
return data, true, nil
}
// 尝试从磁盘读取数据
diskData, diskHit, diskErr := c.disk.Get(key) diskData, diskHit, diskErr := c.disk.Get(key)
if diskErr == nil && diskHit { if diskErr == nil && diskHit {
// 磁盘缓存命中,更新内存缓存 // 磁盘缓存命中,更新内存缓存
@@ -70,12 +77,6 @@ func (c *EnhancedTwoLevelCache) Get(key string) ([]byte, bool, error) {
return diskData, true, nil return diskData, true, nil
} }
// 磁盘未命中,检查内存缓存
data, _, memHit := c.memory.GetWithTimestamp(key)
if memHit {
return data, true, nil
}
return nil, false, nil return nil, false, nil
} }

View File

@@ -560,6 +560,10 @@ func ExtractNetDiskLinks(text string) []string {
for _, match := range baiduMatches { for _, match := range baiduMatches {
// 清理并添加百度网盘链接 // 清理并添加百度网盘链接
cleanURL := CleanBaiduPanURL(match) cleanURL := CleanBaiduPanURL(match)
// 确保链接末尾不包含https
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
if cleanURL != "" { if cleanURL != "" {
links = append(links, cleanURL) links = append(links, cleanURL)
} }
@@ -570,6 +574,10 @@ func ExtractNetDiskLinks(text string) []string {
for _, match := range tianyiMatches { for _, match := range tianyiMatches {
// 清理并添加天翼云盘链接 // 清理并添加天翼云盘链接
cleanURL := CleanTianyiPanURL(match) cleanURL := CleanTianyiPanURL(match)
// 确保链接末尾不包含https
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
if cleanURL != "" { if cleanURL != "" {
links = append(links, cleanURL) links = append(links, cleanURL)
} }
@@ -580,6 +588,10 @@ func ExtractNetDiskLinks(text string) []string {
for _, match := range ucMatches { for _, match := range ucMatches {
// 清理并添加UC网盘链接 // 清理并添加UC网盘链接
cleanURL := CleanUCPanURL(match) cleanURL := CleanUCPanURL(match)
// 确保链接末尾不包含https
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
if cleanURL != "" { if cleanURL != "" {
links = append(links, cleanURL) links = append(links, cleanURL)
} }
@@ -590,6 +602,10 @@ func ExtractNetDiskLinks(text string) []string {
for _, match := range pan123Matches { for _, match := range pan123Matches {
// 清理并添加123网盘链接 // 清理并添加123网盘链接
cleanURL := Clean123PanURL(match) cleanURL := Clean123PanURL(match)
// 确保链接末尾不包含https
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
if cleanURL != "" { if cleanURL != "" {
// 检查是否已经存在相同的链接比较完整URL // 检查是否已经存在相同的链接比较完整URL
isDuplicate := false isDuplicate := false
@@ -615,6 +631,10 @@ func ExtractNetDiskLinks(text string) []string {
for _, match := range pan115Matches { for _, match := range pan115Matches {
// 清理并添加115网盘链接 // 清理并添加115网盘链接
cleanURL := Clean115PanURL(match) // 115网盘链接的清理逻辑与123网盘类似 cleanURL := Clean115PanURL(match) // 115网盘链接的清理逻辑与123网盘类似
// 确保链接末尾不包含https
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
if cleanURL != "" { if cleanURL != "" {
// 检查是否已经存在相同的链接比较完整URL // 检查是否已经存在相同的链接比较完整URL
isDuplicate := false isDuplicate := false
@@ -640,6 +660,10 @@ func ExtractNetDiskLinks(text string) []string {
for _, match := range aliyunMatches { for _, match := range aliyunMatches {
// 清理并添加阿里云盘链接 // 清理并添加阿里云盘链接
cleanURL := CleanAliyunPanURL(match) cleanURL := CleanAliyunPanURL(match)
// 确保链接末尾不包含https
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
if cleanURL != "" { if cleanURL != "" {
// 检查是否已经存在相同的链接 // 检查是否已经存在相同的链接
isDuplicate := false isDuplicate := false
@@ -664,17 +688,22 @@ func ExtractNetDiskLinks(text string) []string {
quarkLinks := QuarkPanPattern.FindAllString(text, -1) quarkLinks := QuarkPanPattern.FindAllString(text, -1)
if quarkLinks != nil { if quarkLinks != nil {
for _, match := range quarkLinks { for _, match := range quarkLinks {
// 确保链接末尾不包含https
cleanURL := match
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
// 检查是否已经存在相同的链接 // 检查是否已经存在相同的链接
isDuplicate := false isDuplicate := false
for _, existingLink := range links { for _, existingLink := range links {
if strings.Contains(existingLink, match) || strings.Contains(match, existingLink) { if strings.Contains(existingLink, cleanURL) || strings.Contains(cleanURL, existingLink) {
isDuplicate = true isDuplicate = true
break break
} }
} }
if !isDuplicate { if !isDuplicate {
links = append(links, match) links = append(links, cleanURL)
} }
} }
} }
@@ -683,17 +712,22 @@ func ExtractNetDiskLinks(text string) []string {
xunleiLinks := XunleiPanPattern.FindAllString(text, -1) xunleiLinks := XunleiPanPattern.FindAllString(text, -1)
if xunleiLinks != nil { if xunleiLinks != nil {
for _, match := range xunleiLinks { for _, match := range xunleiLinks {
// 确保链接末尾不包含https
cleanURL := match
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
// 检查是否已经存在相同的链接 // 检查是否已经存在相同的链接
isDuplicate := false isDuplicate := false
for _, existingLink := range links { for _, existingLink := range links {
if strings.Contains(existingLink, match) || strings.Contains(match, existingLink) { if strings.Contains(existingLink, cleanURL) || strings.Contains(cleanURL, existingLink) {
isDuplicate = true isDuplicate = true
break break
} }
} }
if !isDuplicate { if !isDuplicate {
links = append(links, match) links = append(links, cleanURL)
} }
} }
} }
@@ -703,25 +737,30 @@ func ExtractNetDiskLinks(text string) []string {
if otherLinks != nil { if otherLinks != nil {
// 过滤掉已经添加过的链接 // 过滤掉已经添加过的链接
for _, link := range otherLinks { for _, link := range otherLinks {
// 确保链接末尾不包含https
cleanURL := link
if strings.HasSuffix(cleanURL, "https") {
cleanURL = cleanURL[:len(cleanURL)-5]
}
// 跳过百度、夸克、迅雷、天翼、UC和123网盘链接因为已经单独处理过 // 跳过百度、夸克、迅雷、天翼、UC和123网盘链接因为已经单独处理过
if strings.Contains(link, "pan.baidu.com") || if strings.Contains(cleanURL, "pan.baidu.com") ||
strings.Contains(link, "pan.quark.cn") || strings.Contains(cleanURL, "pan.quark.cn") ||
strings.Contains(link, "pan.xunlei.com") || strings.Contains(cleanURL, "pan.xunlei.com") ||
strings.Contains(link, "cloud.189.cn") || strings.Contains(cleanURL, "cloud.189.cn") ||
strings.Contains(link, "drive.uc.cn") || strings.Contains(cleanURL, "drive.uc.cn") ||
strings.Contains(link, "123684.com") || strings.Contains(cleanURL, "123684.com") ||
strings.Contains(link, "123685.com") || strings.Contains(cleanURL, "123685.com") ||
strings.Contains(link, "123912.com") || strings.Contains(cleanURL, "123912.com") ||
strings.Contains(link, "123pan.com") || strings.Contains(cleanURL, "123pan.com") ||
strings.Contains(link, "123pan.cn") || strings.Contains(cleanURL, "123pan.cn") ||
strings.Contains(link, "123592.com") { strings.Contains(cleanURL, "123592.com") {
continue continue
} }
isDuplicate := false isDuplicate := false
for _, existingLink := range links { for _, existingLink := range links {
normalizedExisting := normalizeURLForComparison(existingLink) normalizedExisting := normalizeURLForComparison(existingLink)
normalizedNew := normalizeURLForComparison(link) normalizedNew := normalizeURLForComparison(cleanURL)
// 使用完整URL比较包括www.前缀 // 使用完整URL比较包括www.前缀
if normalizedExisting == normalizedNew || if normalizedExisting == normalizedNew ||
@@ -733,7 +772,7 @@ func ExtractNetDiskLinks(text string) []string {
} }
if !isDuplicate { if !isDuplicate {
links = append(links, link) links = append(links, cleanURL)
} }
} }
} }