添加扩展名匹配器和缓存机制,优化302跳转规则选择逻辑,增强缓存统计功能,确保配置更新时清理缓存以提高性能和准确性。

This commit is contained in:
wood chen 2025-06-02 06:33:50 +08:00
parent 8dd410fad4
commit 9e45b3e38a
5 changed files with 295 additions and 131 deletions

View File

@ -2,6 +2,7 @@ package config
import ( import (
"strings" "strings"
"sync"
) )
type Config struct { type Config struct {
@ -14,6 +15,11 @@ type PathConfig struct {
ExtensionMap []ExtRuleConfig `json:"ExtensionMap"` // 扩展名映射规则 ExtensionMap []ExtRuleConfig `json:"ExtensionMap"` // 扩展名映射规则
ExtRules []ExtensionRule `json:"-"` // 内部使用,存储处理后的扩展名规则 ExtRules []ExtensionRule `json:"-"` // 内部使用,存储处理后的扩展名规则
RedirectMode bool `json:"RedirectMode"` // 是否使用302跳转模式 RedirectMode bool `json:"RedirectMode"` // 是否使用302跳转模式
// 缓存相关字段不参与JSON序列化
matcherCache interface{} `json:"-"` // 缓存的ExtensionMatcher使用interface{}避免循环导入
matcherCacheMux sync.RWMutex `json:"-"` // 缓存读写锁
cacheValid bool `json:"-"` // 缓存是否有效
} }
// ExtensionRule 表示一个扩展名映射规则(内部使用) // ExtensionRule 表示一个扩展名映射规则(内部使用)
@ -73,6 +79,32 @@ func (p *PathConfig) ProcessExtensionMap() {
p.ExtRules = append(p.ExtRules, extRule) p.ExtRules = append(p.ExtRules, extRule)
} }
} }
// 清除缓存,因为规则已经改变
p.InvalidateMatcherCache()
}
// InvalidateMatcherCache 清除ExtensionMatcher缓存
func (p *PathConfig) InvalidateMatcherCache() {
p.matcherCacheMux.Lock()
defer p.matcherCacheMux.Unlock()
p.matcherCache = nil
p.cacheValid = false
}
// GetMatcherCache 获取缓存的ExtensionMatcher
func (p *PathConfig) GetMatcherCache() (interface{}, bool) {
p.matcherCacheMux.RLock()
defer p.matcherCacheMux.RUnlock()
return p.matcherCache, p.cacheValid
}
// SetMatcherCache 设置ExtensionMatcher缓存
func (p *PathConfig) SetMatcherCache(matcher interface{}) {
p.matcherCacheMux.Lock()
defer p.matcherCacheMux.Unlock()
p.matcherCache = matcher
p.cacheValid = true
} }
// GetProcessedExtTarget 快速获取扩展名对应的目标URL如果存在返回true // GetProcessedExtTarget 快速获取扩展名对应的目标URL如果存在返回true

View File

@ -182,6 +182,10 @@ func NewProxyHandler(cfg *config.Config) *ProxyHandler {
// 注册配置更新回调 // 注册配置更新回调
config.RegisterUpdateCallback(func(newCfg *config.Config) { config.RegisterUpdateCallback(func(newCfg *config.Config) {
// 注意config包已经在回调触发前处理了所有ExtRules这里无需再次处理 // 注意config包已经在回调触发前处理了所有ExtRules这里无需再次处理
// 清理所有ExtensionMatcher缓存因为配置已更新
utils.ClearAllMatcherCaches(newCfg.MAP)
handler.pathMap = newCfg.MAP handler.pathMap = newCfg.MAP
handler.prefixTree.update(newCfg.MAP) // 更新前缀匹配树 handler.prefixTree.update(newCfg.MAP) // 更新前缀匹配树
handler.config = newCfg handler.config = newCfg

View File

@ -4,7 +4,6 @@ import (
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath"
"proxy-go/internal/config" "proxy-go/internal/config"
"proxy-go/internal/utils" "proxy-go/internal/utils"
"strings" "strings"
@ -32,43 +31,21 @@ func (rh *RedirectHandler) HandleRedirect(w http.ResponseWriter, r *http.Request
return true return true
} }
// shouldRedirect 判断是否应该进行302跳转并返回目标URL // shouldRedirect 判断是否应该进行302跳转并返回目标URL(优化版本)
func (rh *RedirectHandler) shouldRedirect(r *http.Request, pathConfig config.PathConfig, targetPath string, client *http.Client) (bool, string) { func (rh *RedirectHandler) shouldRedirect(r *http.Request, pathConfig config.PathConfig, targetPath string, client *http.Client) (bool, string) {
// 获取文件扩展名 // 使用优化的规则选择函数
ext := strings.ToLower(filepath.Ext(targetPath)) result := utils.SelectRuleForRedirect(client, pathConfig, targetPath)
if ext != "" {
ext = ext[1:] // 去掉点号
}
// 使用统一的规则选择逻辑,考虑文件大小 if result.ShouldRedirect {
if rule, found, _ := utils.SelectBestRule(client, pathConfig, targetPath); found && rule != nil && rule.RedirectMode { // 构建完整的目标URL
// 使用选中规则的目标URL进行302跳转 targetURL := rh.buildTargetURL(result.TargetURL, targetPath, r.URL.RawQuery)
targetURL := rh.buildTargetURL(rule.Target, targetPath, r.URL.RawQuery)
log.Printf("[Redirect] %s -> 使用选中规则进行302跳转: %s", targetPath, targetURL)
return true, targetURL
}
// 检查默认目标是否配置为302跳转 if result.Rule != nil {
if pathConfig.RedirectMode { log.Printf("[Redirect] %s -> 使用选中规则进行302跳转: %s", targetPath, targetURL)
// 使用默认目标URL进行302跳转 } else {
targetURL := rh.buildTargetURL(pathConfig.DefaultTarget, targetPath, r.URL.RawQuery) log.Printf("[Redirect] %s -> 使用默认目标进行302跳转: %s", targetPath, targetURL)
log.Printf("[Redirect] %s -> 使用默认目标进行302跳转: %s", targetPath, targetURL) }
return true, targetURL
}
// 如果默认目标没有配置302跳转检查是否有简单的扩展名匹配向后兼容
if rule, found := pathConfig.GetProcessedExtRule(ext); found && rule.RedirectMode {
// 使用扩展名规则的目标URL进行302跳转
targetURL := rh.buildTargetURL(rule.Target, targetPath, r.URL.RawQuery)
log.Printf("[Redirect] %s -> 使用扩展名规则进行302跳转: %s", targetPath, targetURL)
return true, targetURL
}
// 检查通配符规则
if rule, found := pathConfig.GetProcessedExtRule("*"); found && rule.RedirectMode {
// 使用通配符规则的目标URL进行302跳转
targetURL := rh.buildTargetURL(rule.Target, targetPath, r.URL.RawQuery)
log.Printf("[Redirect] %s -> 使用通配符规则进行302跳转: %s", targetPath, targetURL)
return true, targetURL return true, targetURL
} }

View File

@ -13,12 +13,14 @@ type OldPathConfig struct {
SizeThreshold int64 `json:"SizeThreshold,omitempty"` SizeThreshold int64 `json:"SizeThreshold,omitempty"`
MaxSize int64 `json:"MaxSize,omitempty"` MaxSize int64 `json:"MaxSize,omitempty"`
Path string `json:"Path,omitempty"` Path string `json:"Path,omitempty"`
RedirectMode bool `json:"RedirectMode,omitempty"`
} }
// 新配置结构 // 新配置结构
type NewPathConfig struct { type NewPathConfig struct {
DefaultTarget string `json:"DefaultTarget"` DefaultTarget string `json:"DefaultTarget"`
ExtensionMap []ExtRuleConfig `json:"ExtensionMap"` ExtensionMap []ExtRuleConfig `json:"ExtensionMap"`
RedirectMode bool `json:"RedirectMode"`
} }
type ExtRuleConfig struct { type ExtRuleConfig struct {
@ -26,6 +28,7 @@ type ExtRuleConfig struct {
Target string `json:"Target"` Target string `json:"Target"`
SizeThreshold int64 `json:"SizeThreshold"` SizeThreshold int64 `json:"SizeThreshold"`
MaxSize int64 `json:"MaxSize"` MaxSize int64 `json:"MaxSize"`
RedirectMode bool `json:"RedirectMode"`
} }
type CompressionConfig struct { type CompressionConfig struct {
@ -79,6 +82,7 @@ func MigrateConfig(configPath string) error {
newPathConfig := NewPathConfig{ newPathConfig := NewPathConfig{
DefaultTarget: oldPathConfig.DefaultTarget, DefaultTarget: oldPathConfig.DefaultTarget,
ExtensionMap: []ExtRuleConfig{}, ExtensionMap: []ExtRuleConfig{},
RedirectMode: oldPathConfig.RedirectMode,
} }
// 检查ExtensionMap类型 // 检查ExtensionMap类型
@ -94,6 +98,7 @@ func MigrateConfig(configPath string) error {
Target: target, Target: target,
SizeThreshold: oldPathConfig.SizeThreshold, SizeThreshold: oldPathConfig.SizeThreshold,
MaxSize: oldPathConfig.MaxSize, MaxSize: oldPathConfig.MaxSize,
RedirectMode: oldPathConfig.RedirectMode,
} }
newPathConfig.ExtensionMap = append(newPathConfig.ExtensionMap, rule) newPathConfig.ExtensionMap = append(newPathConfig.ExtensionMap, rule)
} }

View File

@ -14,6 +14,7 @@ import (
"sort" "sort"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@ -37,6 +38,10 @@ var (
cacheTTL = 5 * time.Minute cacheTTL = 5 * time.Minute
accessTTL = 30 * time.Second accessTTL = 30 * time.Second
maxCacheSize = 10000 // 最大缓存条目数 maxCacheSize = 10000 // 最大缓存条目数
// 缓存统计
matcherCacheHits int64
matcherCacheMisses int64
) )
// 清理过期缓存 // 清理过期缓存
@ -81,6 +86,19 @@ func init() {
}) })
} }
}() }()
// 定期打印缓存统计信息
go func() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
hits, misses := GetMatcherCacheStats()
if hits+misses > 0 {
hitRate := float64(hits) / float64(hits+misses) * 100
log.Printf("[Cache] ExtensionMatcher stats: hits=%d, misses=%d, hit_rate=%.2f%%",
hits, misses, hitRate)
}
}
}()
} }
// GenerateRequestID 生成唯一的请求ID // GenerateRequestID 生成唯一的请求ID
@ -183,104 +201,168 @@ func GetFileSize(client *http.Client, url string) (int64, error) {
return resp.ContentLength, nil return resp.ContentLength, nil
} }
// SelectBestRule 根据文件大小和扩展名选择最合适的规则 // RuleSelectionResult 规则选择结果,用于缓存和传递结果
// 返回值: (选中的规则, 是否找到匹配的规则, 是否使用了备用目标) type RuleSelectionResult struct {
func SelectBestRule(client *http.Client, pathConfig config.PathConfig, path string) (*config.ExtensionRule, bool, bool) { Rule *config.ExtensionRule
// 获取文件扩展名(使用优化的字符串处理) Found bool
ext := "" UsedAltTarget bool
lastDotIndex := strings.LastIndex(path, ".") TargetURL string
if lastDotIndex > 0 && lastDotIndex < len(path)-1 { ShouldRedirect bool
ext = strings.ToLower(path[lastDotIndex+1:]) }
// ExtensionMatcher 扩展名匹配器,用于优化扩展名匹配性能
type ExtensionMatcher struct {
exactMatches map[string][]*config.ExtensionRule // 精确匹配的扩展名
wildcardRules []*config.ExtensionRule // 通配符规则
hasRedirectRule bool // 是否有任何302跳转规则
}
// NewExtensionMatcher 创建扩展名匹配器
func NewExtensionMatcher(rules []config.ExtensionRule) *ExtensionMatcher {
matcher := &ExtensionMatcher{
exactMatches: make(map[string][]*config.ExtensionRule),
wildcardRules: make([]*config.ExtensionRule, 0),
} }
for i := range rules {
rule := &rules[i]
// 处理阈值默认值
if rule.SizeThreshold < 0 {
rule.SizeThreshold = 0
}
if rule.MaxSize <= 0 {
rule.MaxSize = 1<<63 - 1
}
// 检查是否有302跳转规则
if rule.RedirectMode {
matcher.hasRedirectRule = true
}
// 分类存储规则
for _, ext := range rule.Extensions {
if ext == "*" {
matcher.wildcardRules = append(matcher.wildcardRules, rule)
} else {
if matcher.exactMatches[ext] == nil {
matcher.exactMatches[ext] = make([]*config.ExtensionRule, 0, 1)
}
matcher.exactMatches[ext] = append(matcher.exactMatches[ext], rule)
}
}
}
// 预排序所有规则组
for ext := range matcher.exactMatches {
sortRulesByThreshold(matcher.exactMatches[ext])
}
sortRulesByThreshold(matcher.wildcardRules)
return matcher
}
// sortRulesByThreshold 按阈值排序规则
func sortRulesByThreshold(rules []*config.ExtensionRule) {
sort.Slice(rules, func(i, j int) bool {
if rules[i].SizeThreshold == rules[j].SizeThreshold {
return rules[i].MaxSize > rules[j].MaxSize
}
return rules[i].SizeThreshold < rules[j].SizeThreshold
})
}
// GetMatchingRules 获取匹配的规则
func (em *ExtensionMatcher) GetMatchingRules(ext string) []*config.ExtensionRule {
// 先查找精确匹配
if rules, exists := em.exactMatches[ext]; exists {
return rules
}
// 返回通配符规则
return em.wildcardRules
}
// HasRedirectRule 检查是否有任何302跳转规则
func (em *ExtensionMatcher) HasRedirectRule() bool {
return em.hasRedirectRule
}
// extractExtension 提取文件扩展名(优化版本)
func extractExtension(path string) string {
lastDotIndex := strings.LastIndex(path, ".")
if lastDotIndex > 0 && lastDotIndex < len(path)-1 {
return strings.ToLower(path[lastDotIndex+1:])
}
return ""
}
// GetMatcherCacheStats 获取缓存统计信息
func GetMatcherCacheStats() (hits, misses int64) {
return atomic.LoadInt64(&matcherCacheHits), atomic.LoadInt64(&matcherCacheMisses)
}
// ResetMatcherCacheStats 重置缓存统计
func ResetMatcherCacheStats() {
atomic.StoreInt64(&matcherCacheHits, 0)
atomic.StoreInt64(&matcherCacheMisses, 0)
}
// getOrCreateExtensionMatcher 获取或创建ExtensionMatcher带缓存和统计
func getOrCreateExtensionMatcher(pathConfig *config.PathConfig) *ExtensionMatcher {
// 尝试从缓存获取
if cached, valid := pathConfig.GetMatcherCache(); valid && cached != nil {
if matcher, ok := cached.(*ExtensionMatcher); ok {
atomic.AddInt64(&matcherCacheHits, 1)
return matcher
}
}
// 缓存未命中,创建新的匹配器
atomic.AddInt64(&matcherCacheMisses, 1)
matcher := NewExtensionMatcher(pathConfig.ExtRules)
// 存储到缓存
pathConfig.SetMatcherCache(matcher)
log.Printf("[Cache] ExtensionMatcher created and cached for %d rules", len(pathConfig.ExtRules))
return matcher
}
// SelectBestRule 根据文件大小和扩展名选择最合适的规则(优化版本,带缓存)
// 返回值: (选中的规则, 是否找到匹配的规则, 是否使用了备用目标)
func SelectBestRule(client *http.Client, pathConfig config.PathConfig, path string) (*config.ExtensionRule, bool, bool) {
// 如果没有扩展名规则返回nil // 如果没有扩展名规则返回nil
if len(pathConfig.ExtRules) == 0 { if len(pathConfig.ExtRules) == 0 {
return nil, false, false return nil, false, false
} }
// 提取扩展名
ext := extractExtension(path)
// 获取或创建缓存的扩展名匹配器
matcher := getOrCreateExtensionMatcher(&pathConfig)
// 获取匹配的规则
matchingRules := matcher.GetMatchingRules(ext)
if len(matchingRules) == 0 {
return nil, false, false
}
// 获取文件大小 // 获取文件大小
contentLength, err := GetFileSize(client, pathConfig.DefaultTarget+path) contentLength, err := GetFileSize(client, pathConfig.DefaultTarget+path)
if err != nil { if err != nil {
log.Printf("[SelectRule] %s -> 获取文件大小出错: %v", path, err) log.Printf("[SelectRule] %s -> 获取文件大小出错: %v", path, err)
// 如果无法获取文件大小,返回第一个匹配的规则
// 如果无法获取文件大小,尝试使用扩展名直接匹配 if len(matchingRules) > 0 {
for _, rule := range pathConfig.ExtRules { log.Printf("[SelectRule] %s -> 基于扩展名直接匹配规则", path)
// 检查具体扩展名匹配 return matchingRules[0], true, true
for _, e := range rule.Extensions {
if e == ext {
log.Printf("[SelectRule] %s -> 基于扩展名直接匹配规则", path)
return &rule, true, true
}
}
} }
// 尝试使用通配符规则
for _, rule := range pathConfig.ExtRules {
for _, e := range rule.Extensions {
if e == "*" {
log.Printf("[SelectRule] %s -> 基于通配符匹配规则", path)
return &rule, true, true
}
}
}
return nil, false, false return nil, false, false
} }
// 获取匹配的扩展名规则 // 根据文件大小找出最匹配的规则(规则已经预排序)
matchingRules := []config.ExtensionRule{} for _, rule := range matchingRules {
wildcardRules := []config.ExtensionRule{} // 存储通配符规则
// 找出所有匹配当前扩展名的规则
for _, rule := range pathConfig.ExtRules {
// 处理阈值默认值
if rule.SizeThreshold < 0 {
rule.SizeThreshold = 0 // 默认不限制
}
if rule.MaxSize <= 0 {
rule.MaxSize = 1<<63 - 1 // 设置为最大值表示不限制
}
// 检查是否包含通配符
for _, e := range rule.Extensions {
if e == "*" {
wildcardRules = append(wildcardRules, rule)
break
}
}
// 检查具体扩展名匹配
for _, e := range rule.Extensions {
if e == ext {
matchingRules = append(matchingRules, rule)
break
}
}
}
// 如果没有找到匹配的具体扩展名规则,使用通配符规则
if len(matchingRules) == 0 {
if len(wildcardRules) > 0 {
log.Printf("[SelectRule] %s -> 使用通配符规则", path)
matchingRules = wildcardRules
} else {
return nil, false, false
}
}
// 按阈值排序规则(优化点:使用更高效的排序)
sort.Slice(matchingRules, func(i, j int) bool {
if matchingRules[i].SizeThreshold == matchingRules[j].SizeThreshold {
return matchingRules[i].MaxSize > matchingRules[j].MaxSize
}
return matchingRules[i].SizeThreshold < matchingRules[j].SizeThreshold
})
// 根据文件大小找出最匹配的规则
for i := range matchingRules {
rule := &matchingRules[i]
// 检查文件大小是否在阈值范围内 // 检查文件大小是否在阈值范围内
if contentLength >= rule.SizeThreshold && contentLength <= rule.MaxSize { if contentLength >= rule.SizeThreshold && contentLength <= rule.MaxSize {
// 找到匹配的规则 // 找到匹配的规则
@ -303,33 +385,84 @@ func SelectBestRule(client *http.Client, pathConfig config.PathConfig, path stri
return nil, false, false return nil, false, false
} }
// GetTargetURL 根据路径和配置决定目标URL // SelectRuleForRedirect 专门为302跳转优化的规则选择函数带缓存
func SelectRuleForRedirect(client *http.Client, pathConfig config.PathConfig, path string) *RuleSelectionResult {
result := &RuleSelectionResult{}
// 快速检查如果没有任何302跳转配置直接返回
if !pathConfig.RedirectMode && len(pathConfig.ExtRules) == 0 {
return result
}
// 如果默认目标配置了302跳转优先使用
if pathConfig.RedirectMode {
result.Found = true
result.ShouldRedirect = true
result.TargetURL = pathConfig.DefaultTarget
return result
}
// 检查扩展名规则
if len(pathConfig.ExtRules) > 0 {
ext := extractExtension(path)
// 获取或创建缓存的扩展名匹配器
matcher := getOrCreateExtensionMatcher(&pathConfig)
// 快速检查如果没有任何302跳转规则跳过复杂逻辑
if !matcher.HasRedirectRule() {
return result
}
// 尝试选择最佳规则
if rule, found, usedAlt := SelectBestRule(client, pathConfig, path); found && rule != nil && rule.RedirectMode {
result.Rule = rule
result.Found = found
result.UsedAltTarget = usedAlt
result.ShouldRedirect = true
result.TargetURL = rule.Target
return result
}
// 回退到简单的扩展名匹配
if rule, found := pathConfig.GetProcessedExtRule(ext); found && rule.RedirectMode {
result.Rule = rule
result.Found = found
result.UsedAltTarget = true
result.ShouldRedirect = true
result.TargetURL = rule.Target
return result
}
// 检查通配符规则
if rule, found := pathConfig.GetProcessedExtRule("*"); found && rule.RedirectMode {
result.Rule = rule
result.Found = found
result.UsedAltTarget = true
result.ShouldRedirect = true
result.TargetURL = rule.Target
return result
}
}
return result
}
// GetTargetURL 根据路径和配置决定目标URL优化版本
func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathConfig, path string) (string, bool) { func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathConfig, path string) (string, bool) {
// 默认使用默认目标 // 默认使用默认目标
targetBase := pathConfig.DefaultTarget targetBase := pathConfig.DefaultTarget
usedAltTarget := false usedAltTarget := false
// 获取文件扩展名(使用优化的字符串处理)
ext := ""
lastDotIndex := strings.LastIndex(path, ".")
if lastDotIndex > 0 && lastDotIndex < len(path)-1 {
ext = strings.ToLower(path[lastDotIndex+1:])
}
// 如果没有扩展名规则,直接返回默认目标 // 如果没有扩展名规则,直接返回默认目标
if len(pathConfig.ExtRules) == 0 { if len(pathConfig.ExtRules) == 0 {
ext := extractExtension(path)
if ext == "" { if ext == "" {
log.Printf("[Route] %s -> %s (无扩展名)", path, targetBase) log.Printf("[Route] %s -> %s (无扩展名)", path, targetBase)
} }
return targetBase, false return targetBase, false
} }
// 确保有扩展名规则
if ext == "" {
log.Printf("[Route] %s -> %s (无扩展名)", path, targetBase)
// 即使没有扩展名,也要尝试匹配 * 通配符规则
}
// 使用新的统一规则选择逻辑 // 使用新的统一规则选择逻辑
rule, found, usedAlt := SelectBestRule(client, pathConfig, path) rule, found, usedAlt := SelectBestRule(client, pathConfig, path)
if found && rule != nil { if found && rule != nil {
@ -338,6 +471,7 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo
log.Printf("[Route] %s -> %s (使用选中的规则)", path, targetBase) log.Printf("[Route] %s -> %s (使用选中的规则)", path, targetBase)
} else { } else {
// 如果无法获取文件大小,尝试使用扩展名直接匹配(优化点) // 如果无法获取文件大小,尝试使用扩展名直接匹配(优化点)
ext := extractExtension(path)
if altTarget, exists := pathConfig.GetProcessedExtTarget(ext); exists { if altTarget, exists := pathConfig.GetProcessedExtTarget(ext); exists {
usedAltTarget = true usedAltTarget = true
targetBase = altTarget targetBase = altTarget
@ -457,3 +591,15 @@ func ParseInt(s string, defaultValue int) int {
} }
return result return result
} }
// ClearAllMatcherCaches 清理所有ExtensionMatcher缓存用于配置更新时
func ClearAllMatcherCaches(configMap map[string]config.PathConfig) {
cleared := 0
for path := range configMap {
pathConfig := configMap[path]
pathConfig.InvalidateMatcherCache()
configMap[path] = pathConfig
cleared++
}
log.Printf("[Cache] Cleared %d ExtensionMatcher caches due to config update", cleared)
}