主要变更: - WebSocket 代理支持 (internal/proxy/websocket.go) - OCSP stapling 实现 (internal/ssl/ocsp.go) - 监控状态端点 (internal/server/status.go) - 新增 nginx 模块文档 (19-24) - UDP 代理超时配置支持 - 多模块代码注释完善和功能增强 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
494 lines
12 KiB
Go
494 lines
12 KiB
Go
// Package cache 提供文件缓存和代理缓存功能,支持 LRU 淘汰和缓存锁防击穿。
|
||
//
|
||
// 该文件包含缓存相关的核心逻辑,包括:
|
||
// - 文件缓存实现,支持 LRU 淘汰策略
|
||
// - 代理响应缓存,支持缓存锁防止缓存击穿
|
||
// - 缓存统计和生命周期管理
|
||
//
|
||
// 主要用途:
|
||
// 用于缓存静态文件内容和代理响应,减少磁盘 I/O 和上游请求,提升服务性能。
|
||
//
|
||
// 注意事项:
|
||
// - 文件缓存支持按条目数和内存大小双重限制
|
||
// - 代理缓存支持过期缓存复用(stale)机制
|
||
// - 所有公开方法均为并发安全
|
||
//
|
||
// 作者:xfy
|
||
package cache
|
||
|
||
import (
|
||
"container/list"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// FileEntry 文件缓存条目,存储单个文件的缓存信息。
|
||
type FileEntry struct {
|
||
// Path 文件路径,作为缓存键
|
||
Path string
|
||
|
||
// Size 文件大小,单位为字节
|
||
Size int64
|
||
|
||
// ModTime 文件最后修改时间,用于检测文件变更
|
||
ModTime time.Time
|
||
|
||
// LastAccess 最后访问时间,用于 LRU 淘汰策略
|
||
LastAccess time.Time
|
||
|
||
// Data 文件内容字节
|
||
Data []byte
|
||
|
||
// element LRU 链表元素,用于快速更新链表位置
|
||
element *list.Element
|
||
}
|
||
|
||
// FileCache 文件缓存,支持 LRU 淘汰策略。
|
||
//
|
||
// 该结构体实现了基于内存的文件缓存,支持按条目数和内存大小限制进行淘汰。
|
||
// 使用 LRU(最近最少使用)算法决定淘汰顺序。
|
||
//
|
||
// 注意事项:
|
||
// - 所有方法均为并发安全
|
||
// - 支持过期时间自动淘汰
|
||
type FileCache struct {
|
||
// maxEntries 最大缓存条目数,超过时触发 LRU 淘汰
|
||
maxEntries int64
|
||
|
||
// maxSize 内存使用上限,单位为字节,超过时触发 LRU 淘汰
|
||
maxSize int64
|
||
|
||
// inactive 未访问淘汰时间,超过此时间未访问的条目将被淘汰
|
||
inactive time.Duration
|
||
|
||
// entries 缓存条目映射,以文件路径为键
|
||
entries map[string]*FileEntry
|
||
|
||
// lruList LRU 链表,头部为最近访问,尾部为最久未访问
|
||
lruList *list.List
|
||
|
||
// mu 读写锁,保护并发访问
|
||
mu sync.RWMutex
|
||
|
||
// currentSize 当前内存使用量,单位为字节
|
||
currentSize int64
|
||
}
|
||
|
||
// NewFileCache 创建文件缓存实例。
|
||
//
|
||
// 根据指定的条目数限制、内存大小限制和过期时间创建缓存。
|
||
//
|
||
// 参数:
|
||
// - maxEntries: 最大缓存条目数,设为 0 表示不限制
|
||
// - maxSize: 内存使用上限(字节),设为 0 表示不限制
|
||
// - inactive: 未访问淘汰时间,超过此时间未访问的条目将被淘汰
|
||
//
|
||
// 返回值:
|
||
// - *FileCache: 创建的文件缓存实例
|
||
func NewFileCache(maxEntries, maxSize int64, inactive time.Duration) *FileCache {
|
||
return &FileCache{
|
||
maxEntries: maxEntries,
|
||
maxSize: maxSize,
|
||
inactive: inactive,
|
||
entries: make(map[string]*FileEntry),
|
||
lruList: list.New(),
|
||
}
|
||
}
|
||
|
||
// Get 获取缓存的文件。
|
||
//
|
||
// 根据文件路径查找缓存条目,如果找到且未过期则返回。
|
||
// 访问时会更新条目的访问时间并移动到 LRU 链表头部。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
//
|
||
// 返回值:
|
||
// - *FileEntry: 缓存条目,包含文件内容和元数据
|
||
// - bool: 是否找到有效缓存
|
||
func (c *FileCache) Get(path string) (*FileEntry, bool) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
entry, ok := c.entries[path]
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
|
||
// 检查是否过期
|
||
if time.Since(entry.LastAccess) > c.inactive {
|
||
c.removeEntry(entry)
|
||
return nil, false
|
||
}
|
||
|
||
// 更新访问时间并移到 LRU 链表头部
|
||
entry.LastAccess = time.Now()
|
||
c.lruList.MoveToFront(entry.element)
|
||
|
||
return entry, true
|
||
}
|
||
|
||
// Set 设置缓存条目。
|
||
//
|
||
// 将文件内容存入缓存,如果缓存已存在则更新。
|
||
// 存入后检查是否需要触发 LRU 淘汰。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
// - data: 文件内容字节
|
||
// - size: 文件大小(字节)
|
||
// - modTime: 文件最后修改时间
|
||
//
|
||
// 返回值:
|
||
// - error: 当前实现始终返回 nil
|
||
func (c *FileCache) Set(path string, data []byte, size int64, modTime time.Time) error {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
// 检查是否已存在
|
||
if entry, ok := c.entries[path]; ok {
|
||
c.currentSize -= entry.Size
|
||
entry.Data = data
|
||
entry.Size = size
|
||
entry.ModTime = modTime
|
||
entry.LastAccess = time.Now()
|
||
c.currentSize += size
|
||
c.lruList.MoveToFront(entry.element)
|
||
c.evictIfNeeded()
|
||
return nil
|
||
}
|
||
|
||
// 创建新条目
|
||
entry := &FileEntry{
|
||
Path: path,
|
||
Data: data,
|
||
Size: size,
|
||
ModTime: modTime,
|
||
LastAccess: time.Now(),
|
||
}
|
||
entry.element = c.lruList.PushFront(entry)
|
||
c.entries[path] = entry
|
||
c.currentSize += size
|
||
|
||
c.evictIfNeeded()
|
||
return nil
|
||
}
|
||
|
||
// Delete 删除缓存条目。
|
||
//
|
||
// 根据文件路径删除对应的缓存条目。
|
||
//
|
||
// 参数:
|
||
// - path: 文件路径,作为缓存键
|
||
func (c *FileCache) Delete(path string) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if entry, ok := c.entries[path]; ok {
|
||
c.removeEntry(entry)
|
||
}
|
||
}
|
||
|
||
// removeEntry 内部删除条目(不加锁)。
|
||
//
|
||
// 从 LRU 链表和条目映射中移除指定条目,更新当前内存使用量。
|
||
// 调用此方法前必须已持有写锁。
|
||
//
|
||
// 参数:
|
||
// - entry: 要删除的缓存条目
|
||
func (c *FileCache) removeEntry(entry *FileEntry) {
|
||
c.lruList.Remove(entry.element)
|
||
delete(c.entries, entry.Path)
|
||
c.currentSize -= entry.Size
|
||
}
|
||
|
||
// evictIfNeeded 根据限制淘汰条目。
|
||
func (c *FileCache) evictIfNeeded() {
|
||
// 按条目数淘汰
|
||
for c.lruList.Len() > int(c.maxEntries) && c.maxEntries > 0 {
|
||
c.evictLRU()
|
||
}
|
||
|
||
// 按内存大小淘汰
|
||
for c.currentSize > c.maxSize && c.maxSize > 0 {
|
||
c.evictLRU()
|
||
}
|
||
}
|
||
|
||
// evictLRU 淘汰最久未使用的条目。
|
||
func (c *FileCache) evictLRU() {
|
||
if c.lruList.Len() == 0 {
|
||
return
|
||
}
|
||
|
||
element := c.lruList.Back()
|
||
if element == nil {
|
||
return
|
||
}
|
||
|
||
entry := element.Value.(*FileEntry)
|
||
c.removeEntry(entry)
|
||
}
|
||
|
||
// Clear 清空缓存。
|
||
func (c *FileCache) Clear() {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
c.entries = make(map[string]*FileEntry)
|
||
c.lruList = list.New()
|
||
c.currentSize = 0
|
||
}
|
||
|
||
// Stats 返回缓存统计信息。
|
||
func (c *FileCache) Stats() FileCacheStats {
|
||
c.mu.RLock()
|
||
defer c.mu.RUnlock()
|
||
|
||
return FileCacheStats{
|
||
Entries: int64(len(c.entries)),
|
||
MaxEntries: c.maxEntries,
|
||
Size: c.currentSize,
|
||
MaxSize: c.maxSize,
|
||
}
|
||
}
|
||
|
||
// FileCacheStats 文件缓存统计。
|
||
type FileCacheStats struct {
|
||
Entries int64
|
||
MaxEntries int64
|
||
Size int64
|
||
MaxSize int64
|
||
}
|
||
|
||
// ProxyCacheRule 代理缓存规则。
|
||
type ProxyCacheRule struct {
|
||
Path string // 匹配路径
|
||
Methods []string // 可缓存的 HTTP 方法
|
||
Statuses []int // 可缓存的状态码
|
||
MaxAge time.Duration // 缓存有效期
|
||
}
|
||
|
||
// ProxyCacheEntry 代理缓存条目。
|
||
type ProxyCacheEntry struct {
|
||
Key string // 缓存 key
|
||
Data []byte // 响应体
|
||
Headers map[string]string // 响应头
|
||
Status int // 状态码
|
||
Created time.Time // 创建时间
|
||
MaxAge time.Duration // 有效期
|
||
}
|
||
|
||
// ProxyCache 代理响应缓存,支持缓存锁防击穿。
|
||
type ProxyCache struct {
|
||
rules []ProxyCacheRule
|
||
entries map[string]*ProxyCacheEntry
|
||
mu sync.RWMutex
|
||
cacheLock bool // 缓存锁开关
|
||
pending map[string]*pendingRequest // 正在生成的缓存项
|
||
staleTime time.Duration // 过期缓存复用时间
|
||
}
|
||
|
||
// pendingRequest 等待中的缓存请求。
|
||
type pendingRequest struct {
|
||
done chan struct{} // 完成信号
|
||
err error // 生成结果
|
||
}
|
||
|
||
// NewProxyCache 创建代理缓存。
|
||
func NewProxyCache(rules []ProxyCacheRule, cacheLock bool, staleTime time.Duration) *ProxyCache {
|
||
return &ProxyCache{
|
||
rules: rules,
|
||
entries: make(map[string]*ProxyCacheEntry),
|
||
cacheLock: cacheLock,
|
||
pending: make(map[string]*pendingRequest),
|
||
staleTime: staleTime,
|
||
}
|
||
}
|
||
|
||
// Get 获取缓存的代理响应。
|
||
func (c *ProxyCache) Get(key string) (*ProxyCacheEntry, bool, bool) {
|
||
c.mu.RLock()
|
||
entry, ok := c.entries[key]
|
||
c.mu.RUnlock()
|
||
|
||
if !ok {
|
||
return nil, false, false
|
||
}
|
||
|
||
// 检查是否过期
|
||
now := time.Now()
|
||
expired := now.Sub(entry.Created) > entry.MaxAge
|
||
|
||
if expired {
|
||
// 检查是否可以使用过期缓存
|
||
if c.staleTime > 0 && now.Sub(entry.Created) <= entry.MaxAge+c.staleTime {
|
||
return entry, true, true // stale but usable
|
||
}
|
||
return nil, false, false
|
||
}
|
||
|
||
return entry, true, false
|
||
}
|
||
|
||
// Set 设置代理缓存条目。
|
||
func (c *ProxyCache) Set(key string, data []byte, headers map[string]string, status int, maxAge time.Duration) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
c.entries[key] = &ProxyCacheEntry{
|
||
Key: key,
|
||
Data: data,
|
||
Headers: headers,
|
||
Status: status,
|
||
Created: time.Now(),
|
||
MaxAge: maxAge,
|
||
}
|
||
|
||
// 如果有等待的请求,通知它们
|
||
if pending, ok := c.pending[key]; ok {
|
||
pending.err = nil
|
||
close(pending.done)
|
||
delete(c.pending, key)
|
||
}
|
||
}
|
||
|
||
// AcquireLock 获取缓存生成锁(防止击穿)。
|
||
// 如果返回 nil,表示获得锁,应该去生成缓存。
|
||
// 如果返回 chan,表示有其他请求正在生成,应该等待。
|
||
func (c *ProxyCache) AcquireLock(key string) <-chan struct{} {
|
||
if !c.cacheLock {
|
||
return nil // 不使用缓存锁
|
||
}
|
||
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
// 检查是否已有缓存
|
||
if _, ok := c.entries[key]; ok {
|
||
return nil
|
||
}
|
||
|
||
// 检查是否有 pending 请求
|
||
if pending, ok := c.pending[key]; ok {
|
||
return pending.done // 等待现有请求
|
||
}
|
||
|
||
// 创建新的 pending 请求
|
||
pending := &pendingRequest{
|
||
done: make(chan struct{}),
|
||
}
|
||
c.pending[key] = pending
|
||
return nil // 获得锁,应该生成缓存
|
||
}
|
||
|
||
// ReleaseLock 释放缓存生成锁。
|
||
func (c *ProxyCache) ReleaseLock(key string, err error) {
|
||
if !c.cacheLock {
|
||
return
|
||
}
|
||
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if pending, ok := c.pending[key]; ok {
|
||
pending.err = err
|
||
close(pending.done)
|
||
delete(c.pending, key)
|
||
}
|
||
}
|
||
|
||
// MatchRule 检查请求是否匹配缓存规则。
|
||
func (c *ProxyCache) MatchRule(path, method string, status int) *ProxyCacheRule {
|
||
for _, rule := range c.rules {
|
||
// 检查路径匹配(简单前缀匹配)
|
||
if rule.Path != "" && !pathMatch(rule.Path, path) {
|
||
continue
|
||
}
|
||
|
||
// 检查方法
|
||
if len(rule.Methods) > 0 && !contains(rule.Methods, method) {
|
||
continue
|
||
}
|
||
|
||
// 检查状态码
|
||
if len(rule.Statuses) > 0 && !containsInt(rule.Statuses, status) {
|
||
continue
|
||
}
|
||
|
||
return &rule
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// pathMatch 路径匹配(支持前缀和精确匹配)。
|
||
func pathMatch(pattern, path string) bool {
|
||
if pattern == "*" {
|
||
return true
|
||
}
|
||
// 通配符匹配
|
||
if pattern[len(pattern)-1] == '*' {
|
||
prefix := pattern[:len(pattern)-1]
|
||
return strings.HasPrefix(path, prefix)
|
||
}
|
||
// 前缀匹配(pattern 以 / 结尾)
|
||
if pattern[len(pattern)-1] == '/' {
|
||
return strings.HasPrefix(path, pattern)
|
||
}
|
||
// 精确匹配
|
||
return path == pattern
|
||
}
|
||
|
||
// contains 检查字符串切片是否包含某值。
|
||
func contains(slice []string, val string) bool {
|
||
for _, s := range slice {
|
||
if s == val {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// containsInt 检查整数切片是否包含某值。
|
||
func containsInt(slice []int, val int) bool {
|
||
for _, i := range slice {
|
||
if i == val {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Delete 删除缓存条目。
|
||
func (c *ProxyCache) Delete(key string) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
delete(c.entries, key)
|
||
}
|
||
|
||
// Clear 清空代理缓存。
|
||
func (c *ProxyCache) Clear() {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
c.entries = make(map[string]*ProxyCacheEntry)
|
||
c.pending = make(map[string]*pendingRequest)
|
||
}
|
||
|
||
// Stats 返回代理缓存统计。
|
||
func (c *ProxyCache) Stats() ProxyCacheStats {
|
||
c.mu.RLock()
|
||
defer c.mu.RUnlock()
|
||
|
||
return ProxyCacheStats{
|
||
Entries: len(c.entries),
|
||
Pending: len(c.pending),
|
||
}
|
||
}
|
||
|
||
// ProxyCacheStats 代理缓存统计。
|
||
type ProxyCacheStats struct {
|
||
Entries int
|
||
Pending int
|
||
}
|