Files
pipelinedb/cache.go
2025-09-30 15:05:56 +08:00

470 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package pipelinedb provides an integrated pipeline database system
// 集成了数据库存储和业务管道处理的一体化解决方案
package pipelinedb
import (
"container/list"
"sync"
)
// PageCache 实现了基于LRULeast Recently Used算法的页面缓存系统
//
// 核心设计思想:
// 1. 使用HashMap提供O(1)的页面查找性能
// 2. 使用双向链表维护页面的访问顺序LRU顺序
// 3. 支持脏页标记和批量刷新机制
// 4. 提供线程安全的并发访问控制
// 5. 统计缓存命中率用于性能监控
//
// 适用场景:
// - 数据库页面缓存
// - 文件系统缓存
// - 任何需要LRU淘汰策略的缓存系统
type PageCache struct {
capacity int // 缓存容量(最大页面数)
cache map[uint16]*cacheEntry // 页号到缓存条目的映射提供O(1)查找
lru *list.List // LRU双向链表维护访问顺序头部=最近使用,尾部=最久未使用)
mu sync.RWMutex // 读写锁,支持多读单写的并发控制
hits int64 // 缓存命中次数统计
misses int64 // 缓存未命中次数统计
}
// cacheEntry 缓存条目,存储单个页面的完整信息
//
// 设计要点:
// 1. pageNo: 页面编号,用于标识唯一页面
// 2. page: 页面数据的完整副本,避免外部修改影响缓存
// 3. dirty: 脏页标记,标识页面是否被修改过需要写回磁盘
// 4. elem: 指向LRU链表中对应节点的指针用于O(1)时间复杂度的链表操作
type cacheEntry struct {
pageNo uint16 // 页面编号16位支持65536个页面
page []byte // 页面数据副本(避免外部修改)
dirty bool // 脏页标记true=需要写回磁盘false=与磁盘一致)
elem *list.Element // LRU链表节点指针用于快速移动和删除
}
// NewPageCache 创建一个新的LRU页面缓存实例
//
// 参数说明:
// - capacity: 缓存容量,即最大可缓存的页面数量
// 建议根据可用内存和页面大小来设置,例如:
// 可用内存100MB页面大小4KB则capacity可设为25600
//
// 返回值:
// - *PageCache: 初始化完成的页面缓存实例
//
// 初始化内容:
// 1. 设置缓存容量
// 2. 创建空的HashMap用于O(1)查找
// 3. 创建空的双向链表用于LRU排序
// 4. 统计计数器初始化为0hits和misses
func NewPageCache(capacity int) *PageCache {
return &PageCache{
capacity: capacity, // 设置最大缓存页面数
cache: make(map[uint16]*cacheEntry), // 初始化页号到缓存条目的映射
lru: list.New(), // 初始化LRU双向链表
// hits和misses会自动初始化为0
}
}
// Get 从缓存中获取指定页面的数据
//
// 核心算法LRU缓存查找
// 时间复杂度O(1) - HashMap查找 + 双向链表移动
// 空间复杂度O(1) - 只创建页面数据副本
//
// 参数说明:
// - pageNo: 要获取的页面编号16位范围0-65535
//
// 返回值:
// - []byte: 页面数据的副本(如果找到)
// - bool: 是否在缓存中找到该页面
//
// 执行流程:
// 1. 获取读锁(支持并发读取)
// 2. 在HashMap中查找页面
// 3. 如果找到:
// a. 将页面移动到LRU链表头部标记为最近使用
// b. 增加命中计数
// c. 创建并返回页面数据副本(防止外部修改)
// 4. 如果未找到:
// a. 增加未命中计数
// b. 返回nil和false
//
// 并发安全使用读锁保护支持多个goroutine同时读取
func (pc *PageCache) Get(pageNo uint16) ([]byte, bool) {
pc.mu.RLock() // 获取读锁,允许并发读取
defer pc.mu.RUnlock() // 确保函数退出时释放锁
if entry, exists := pc.cache[pageNo]; exists {
// 缓存命中执行LRU更新和数据返回
// 将访问的页面移动到LRU链表头部
// 这是LRU算法的核心最近访问的页面不会被淘汰
pc.lru.MoveToFront(entry.elem)
// 更新命中统计(用于计算缓存命中率)
pc.hits++
// 创建页面数据副本并返回
// 重要:返回副本而不是原始数据,防止外部修改影响缓存
pageCopy := make([]byte, len(entry.page))
copy(pageCopy, entry.page)
return pageCopy, true
}
// 缓存未命中:更新统计并返回失败
pc.misses++
return nil, false
}
// Put 将页面数据放入缓存
//
// 核心算法LRU缓存插入/更新
// 时间复杂度O(1) - HashMap操作 + 双向链表操作
// 空间复杂度O(1) - 创建页面数据副本
//
// 参数说明:
// - pageNo: 页面编号16位范围0-65535
// - p: 页面数据(字节数组)
// - dirty: 脏页标记true=页面已修改需要写回false=页面与磁盘一致)
//
// 执行流程:
// 1. 获取写锁(独占访问)
// 2. 检查页面是否已存在:
// a. 如果存在更新页面数据和脏页标记移动到LRU头部
// b. 如果不存在检查容量必要时淘汰LRU页面然后插入新页面
// 3. 所有操作都使用数据副本,确保缓存数据独立性
//
// 脏页处理:
// - 如果新数据标记为dirty则页面变为脏页
// - 如果页面已经是脏页则保持脏页状态dirty标记具有粘性
// - 脏页在淘汰时需要写回磁盘
//
// 并发安全:使用写锁保护,确保缓存状态一致性
func (pc *PageCache) Put(pageNo uint16, p []byte, dirty bool) {
pc.mu.Lock() // 获取写锁,独占访问缓存
defer pc.mu.Unlock() // 确保函数退出时释放锁
// 情况1页面已存在执行更新操作
if entry, exists := pc.cache[pageNo]; exists {
// 创建新的页面数据副本
// 重要:使用副本避免外部修改影响缓存
pageCopy := make([]byte, len(p))
copy(pageCopy, p)
// 更新页面数据
entry.page = pageCopy
// 更新脏页标记:一旦标记为脏页,就保持脏页状态
// 使用OR操作确保脏页标记的粘性sticky
entry.dirty = entry.dirty || dirty
// 将页面移动到LRU链表头部标记为最近使用
pc.lru.MoveToFront(entry.elem)
return
}
// 情况2页面不存在需要插入新页面
// 检查缓存容量,如果已满则淘汰最久未使用的页面
if len(pc.cache) >= pc.capacity {
pc.evictLRU()
}
// 创建页面数据副本
pageCopy := make([]byte, len(p))
copy(pageCopy, p)
// 创建新的缓存条目
entry := &cacheEntry{
pageNo: pageNo, // 设置页面编号
page: pageCopy, // 设置页面数据副本
dirty: dirty, // 设置脏页标记
}
// 将新条目添加到LRU链表头部最近使用位置
// 同时将链表元素指针保存到条目中用于后续的O(1)操作
entry.elem = pc.lru.PushFront(entry)
// 将新条目添加到HashMap中建立页号到条目的映射
pc.cache[pageNo] = entry
}
// evictLRU 淘汰最久未使用的页面
//
// 核心算法LRU淘汰策略
// 时间复杂度O(1) - 直接访问链表尾部
// 空间复杂度O(1) - 只释放一个页面的空间
//
// 执行流程:
// 1. 检查LRU链表是否为空
// 2. 获取链表尾部元素(最久未使用的页面)
// 3. 检查页面是否为脏页:
// a. 如果是脏页:需要写回磁盘(当前实现中暂时跳过)
// b. 如果不是脏页:直接淘汰
// 4. 从HashMap和LRU链表中移除页面
//
// 脏页处理:
// - 脏页表示页面数据与磁盘不一致,淘汰前必须写回
// - 当前实现中暂时跳过写回操作标记为TODO
// - 生产环境中应该实现写回机制或者抛出错误
//
// 注意事项:
// - 该方法假设调用者已经持有写锁
// - 该方法不检查容量,由调用者确保需要淘汰
func (pc *PageCache) evictLRU() {
// 安全检查:如果链表为空,直接返回
if pc.lru.Len() == 0 {
return
}
// 获取LRU链表尾部元素最久未使用的页面
elem := pc.lru.Back()
if elem == nil {
return // 双重检查,防止并发问题
}
// 从链表元素中提取缓存条目
if elem.Value == nil {
return // 防止并发竞争导致的nil值
}
entry := elem.Value.(*cacheEntry)
pageNo := entry.pageNo
// 脏页处理:检查是否需要写回磁盘
if entry.dirty {
// TODO: 实现脏页写回机制
// 在生产环境中,这里应该:
// 1. 调用写回函数将页面数据写入磁盘
// 2. 处理写回失败的情况(重试或报错)
// 3. 只有写回成功后才能淘汰页面
//
// 示例实现:
// if err := pc.writeBackFunc(pageNo, entry.page); err != nil {
// // 处理写回失败,可能需要保留页面或记录错误
// return
// }
}
// 从HashMap中移除页面映射
delete(pc.cache, pageNo)
// 从LRU链表中移除页面节点
pc.lru.Remove(elem)
}
// Invalidate 从缓存中强制移除指定页面
//
// 使用场景:
// - 页面数据已知损坏,需要强制从缓存中移除
// - 外部直接修改了磁盘数据,缓存数据已过期
// - 内存压力大,需要主动释放特定页面
//
// 核心算法:直接删除
// 时间复杂度O(1) - HashMap删除 + 链表删除
// 空间复杂度O(1) - 释放一个页面的空间
//
// 参数说明:
// - pageNo: 要移除的页面编号
//
// 执行流程:
// 1. 获取写锁(独占访问)
// 2. 检查页面是否存在于缓存中
// 3. 如果存在从HashMap和LRU链表中移除
// 4. 如果不存在:静默忽略(幂等操作)
//
// 注意事项:
// - 该操作不检查脏页状态,直接丢弃数据
// - 如果页面是脏页,未保存的修改将丢失
// - 适用于确定不需要保存数据的场景
//
// 并发安全:使用写锁保护,确保操作原子性
func (pc *PageCache) Invalidate(pageNo uint16) {
pc.mu.Lock() // 获取写锁,独占访问缓存
defer pc.mu.Unlock() // 确保函数退出时释放锁
// 检查页面是否存在于缓存中
if entry, exists := pc.cache[pageNo]; exists {
// 从HashMap中移除页面映射
delete(pc.cache, pageNo)
// 从LRU链表中移除页面节点
pc.lru.Remove(entry.elem)
// 注意:这里不检查脏页状态,直接丢弃
// 如果需要保护脏页应该在删除前检查entry.dirty
}
// 如果页面不存在,静默忽略(幂等操作)
}
// Flush 将所有脏页刷新到磁盘
//
// 使用场景:
// - 数据库关闭前确保所有修改都已保存
// - 定期检查点,将内存中的修改持久化
// - 内存压力大,主动释放脏页占用的内存
//
// 核心算法:遍历所有缓存条目,写回脏页
// 时间复杂度O(n) - n为缓存中的页面数量
// 空间复杂度O(1) - 不创建额外的数据结构
//
// 参数说明:
// - writeFunc: 写回函数,由调用者提供具体的磁盘写入逻辑
// 函数签名func(pageNo uint16, p []byte) error
// 参数pageNo=页面编号p=页面数据
// 返回error=写入错误nil表示成功
//
// 返回值:
// - error: 第一个写回失败的错误(如果有)
//
// 执行流程:
// 1. 获取写锁(独占访问)
// 2. 遍历所有缓存条目
// 3. 对于每个脏页:
// a. 调用writeFunc写回磁盘
// b. 如果写回成功:清除脏页标记
// c. 如果写回失败:立即返回错误
// 4. 所有脏页写回成功后返回nil
//
// 错误处理:
// - 遇到第一个写回错误时立即停止并返回
// - 已成功写回的页面会清除脏页标记
// - 失败的页面保持脏页状态,可以稍后重试
//
// 并发安全:使用写锁保护,确保刷新过程中缓存状态一致
func (pc *PageCache) Flush(writeFunc func(pageNo uint16, p []byte) error) error {
pc.mu.Lock() // 获取写锁,独占访问缓存
defer pc.mu.Unlock() // 确保函数退出时释放锁
// 遍历所有缓存条目,查找脏页
for pageNo, entry := range pc.cache {
// 只处理脏页(已修改但未写回磁盘的页面)
if entry.dirty {
// 调用外部提供的写回函数
if err := writeFunc(pageNo, entry.page); err != nil {
// 写回失败:立即返回错误
// 注意已成功写回的页面会保持clean状态
return err
}
// 写回成功:清除脏页标记
entry.dirty = false
}
}
// 所有脏页都成功写回
return nil
}
// Stats 获取缓存性能统计信息
//
// 用途:
// - 监控缓存性能和效果
// - 调优缓存大小和策略
// - 诊断性能问题
//
// 核心算法:简单的统计计算
// 时间复杂度O(1) - 直接读取计数器
// 空间复杂度O(1) - 不创建额外数据
//
// 返回值:
// - hits: 缓存命中次数(成功从缓存获取页面的次数)
// - misses: 缓存未命中次数(需要从磁盘加载页面的次数)
// - hitRate: 缓存命中率hits / (hits + misses)
//
// 命中率解读:
// - 0.0 - 1.0:命中率范围
// - 0.9+:优秀的缓存效果
// - 0.7-0.9:良好的缓存效果
// - 0.5-0.7:一般的缓存效果
// - <0.5:较差的缓存效果,可能需要增加缓存大小
//
// 并发安全:使用读锁保护,支持并发读取统计信息
func (pc *PageCache) Stats() (hits int64, misses int64, hitRate float64) {
pc.mu.RLock() // 获取读锁,允许并发读取
defer pc.mu.RUnlock() // 确保函数退出时释放锁
// 读取命中和未命中计数
hits = pc.hits
misses = pc.misses
total := hits + misses
// 计算命中率,避免除零错误
if total > 0 {
hitRate = float64(hits) / float64(total)
} else {
hitRate = 0.0 // 没有访问记录时命中率为0
}
return hits, misses, hitRate
}
// Size 获取当前缓存中的页面数量
//
// 用途:
// - 监控缓存使用情况
// - 检查缓存是否接近容量上限
// - 内存使用量估算
//
// 核心算法直接返回HashMap大小
// 时间复杂度O(1) - 直接读取map长度
// 空间复杂度O(1) - 不创建额外数据
//
// 返回值:
// - int: 当前缓存中的页面数量0 <= size <= capacity
//
// 使用示例:
// - 内存使用量 ≈ Size() * 页面大小
// - 缓存利用率 = Size() / Capacity()
//
// 并发安全:使用读锁保护,支持并发读取
func (pc *PageCache) Size() int {
pc.mu.RLock() // 获取读锁,允许并发读取
defer pc.mu.RUnlock() // 确保函数退出时释放锁
// 返回HashMap中的条目数量即缓存的页面数
return len(pc.cache)
}
// Clear 清空整个缓存
//
// 使用场景:
// - 系统重启或重新初始化
// - 内存压力极大,需要释放所有缓存
// - 测试环境中重置缓存状态
// - 数据库结构发生重大变化
//
// 核心算法:重新初始化所有数据结构
// 时间复杂度O(1) - 直接创建新的数据结构
// 空间复杂度O(1) - 释放所有缓存数据
//
// 执行流程:
// 1. 获取写锁(独占访问)
// 2. 重新创建空的HashMap
// 3. 重新创建空的LRU链表
// 4. 重置所有统计计数器
//
// 注意事项:
// - 该操作会丢失所有脏页数据
// - 清空后所有页面访问都会导致缓存未命中
// - 适用于确定不需要保存任何缓存数据的场景
//
// 并发安全:使用写锁保护,确保清空操作原子性
func (pc *PageCache) Clear() {
pc.mu.Lock() // 获取写锁,独占访问缓存
defer pc.mu.Unlock() // 确保函数退出时释放锁
// 重新创建空的HashMap释放所有页面数据
pc.cache = make(map[uint16]*cacheEntry)
// 重新创建空的LRU链表释放所有链表节点
pc.lru = list.New()
// 重置统计计数器
pc.hits = 0
pc.misses = 0
// 注意capacity保持不变因为它是缓存的配置参数
}