Go实现缓存分页拉取
本文描述实现一个缓存分页拉取系统,并尽量减少远程调用。且本地缓存系统添加一个最大缓存容量(MaxSize)
限制。当缓存的数据量超过该上限时,使用LRU(最近最少使用)策略淘汰旧的数据。
1. 功能目标
-
入参:
offset
,size
-
出参:
length
,output
,err
-
使用本地缓存减少远程调用
-
如果本地有缓存的数据则直接返回,否则调用远程接口,并缓存结果
其中:
概念 | 含义 | 类比 |
---|---|---|
offset | 从哪里开始(起始位置) | 从第几页看书 |
size | 要读多少(长度/数量) | 看几页内容 |
示例:
原始数据: h e l l o w o r l d
字节索引: 0 1 2 3 4 5 6 7 8 9 10
|------------|
↑ offset=6 ↑ size=5
输出结果: w o r l d
2. 简单版本
2.1. 接口定义
// Fetcher 是远程数据获取接口,返回 offset 起始的 size 个字节的数据。
type Fetcher interface {
Fetch(offset int, size int) ([]byte, error)
}
// Cache 是本地缓存结构体。
type Cache struct {
mu sync.RWMutex
data map[int]byte // offset -> byte
fetcher Fetcher
}
2.2. 方法实现
func NewCache(fetcher Fetcher) *Cache {
return &Cache{
data: make(map[int]byte),
fetcher: fetcher,
}
}
// Read 尝试从本地缓存读取 offset 起 size 个字节数据,缺失的部分会调用 fetcher.Fetch。
func (c *Cache) Read(offset int, size int) (length int, output []byte, err error) {
c.mu.RLock()
defer c.mu.RUnlock()
output = make([]byte, 0, size)
missing := make([]int, 0)
// 记录已有缓存数据和缺失的 offset
for i := 0; i < size; i++ {
off := offset + i
if val, ok := c.data[off]; ok {
output = append(output, val)
} else {
missing = append(missing, off)
}
}
// 如果没有缺失,直接返回
if len(missing) == 0 {
return len(output), output, nil
}
c.mu.RUnlock() // 需要升级为写锁
c.mu.Lock()
defer c.mu.Unlock()
// 再次检查(避免并发重复拉取)
retryMissing := make([]int, 0)
for _, off := range missing {
if _, ok := c.data[off]; !ok {
retryMissing = append(retryMissing, off)
}
}
if len(retryMissing) > 0 {
// 合并成连续段拉取(这里简化处理:一次拉取整段)
start := retryMissing[0]
end := retryMissing[len(retryMissing)-1] + 1
fetched, err := c.fetcher.Fetch(start, end-start)
if err != nil {
return 0, nil, err
}
for i, b := range fetched {
c.data[start+i] = b
}
}
// 最终重新构建输出
output = make([]byte, 0, size)
for i := 0; i < size; i++ {
b, ok := c.data[offset+i]
if !ok {
return 0, nil, fmt.Errorf("missing data at offset %d", offset+i)
}
output = append(output, b)
}
return len(output), output, nil
}
2.3. 远程接口实现
type DummyFetcher struct {
Source []byte
}
func (f *DummyFetcher) Fetch(offset int, size int) ([]byte, error) {
if offset >= len(f.Source) {
return nil, fmt.Errorf("offset out of range")
}
end := offset + size
if end > len(f.Source) {
end = len(f.Source)
}
return f.Source[offset:end], nil
}
3. 增强版本(LRU淘汰策略)
为上面的本地缓存系统添加一个最大缓存容量(MaxSize)
限制。当缓存的数据量超过该上限时,使用LRU(最近最少使用)策略淘汰旧的数据。
3.1. 更新结构体:增加 LRU 支持和 MaxSize 限制
import (
"container/list"
"fmt"
"sync"
)
// 缓存项
type cacheItem struct {
offset int
value byte
}
type Cache struct {
mu sync.RWMutex
data map[int]*list.Element
lruList *list.List
maxSize int
fetcher Fetcher
}
func NewCache(fetcher Fetcher, maxSize int) *Cache {
return &Cache{
data: make(map[int]*list.Element),
lruList: list.New(),
maxSize: maxSize,
fetcher: fetcher,
}
}
3.2. 插入数据并更新 LRU
// 更新本地缓存
func (c *Cache) put(offset int, b byte) {
if elem, exists := c.data[offset]; exists {
c.lruList.MoveToFront(elem)
elem.Value.(*cacheItem).value = b
return
}
// 插入新元素
elem := c.lruList.PushFront(&cacheItem{offset: offset, value: b})
c.data[offset] = elem
// 检查容量限制
if c.lruList.Len() > c.maxSize {
// 淘汰最旧的元素
last := c.lruList.Back()
if last != nil {
item := last.Value.(*cacheItem)
delete(c.data, item.offset)
c.lruList.Remove(last)
}
}
}
// 获取本地缓存
func (c *Cache) get(offset int) (byte, bool) {
if elem, ok := c.data[offset]; ok {
c.lruList.MoveToFront(elem)
return elem.Value.(*cacheItem).value, true
}
return 0, false
}
3.3. 修改 Read
方法以支持 LRU 缓存逻辑
func (c *Cache) Read(offset int, size int) (length int, output []byte, err error) {
c.mu.RLock()
output = make([]byte, 0, size)
missing := make([]int, 0)
for i := 0; i < size; i++ {
off := offset + i
if val, ok := c.get(off); ok {
output = append(output, val)
} else {
missing = append(missing, off)
}
}
c.mu.RUnlock()
if len(missing) == 0 {
return len(output), output, nil
}
c.mu.Lock()
defer c.mu.Unlock()
retryMissing := make([]int, 0)
for _, off := range missing {
if _, ok := c.data[off]; !ok {
retryMissing = append(retryMissing, off)
}
}
if len(retryMissing) > 0 {
start := retryMissing[0]
end := retryMissing[len(retryMissing)-1] + 1
fetched, err := c.fetcher.Fetch(start, end-start)
if err != nil {
return 0, nil, err
}
for i, b := range fetched {
// 存入本地缓存
c.put(start+i, b)
}
}
// 构建完整输出
output = make([]byte, 0, size)
for i := 0; i < size; i++ {
b, ok := c.get(offset + i) // 获取本地缓存
if !ok {
return 0, nil, fmt.Errorf("missing data at offset %d", offset+i)
}
output = append(output, b)
}
return len(output), output, nil
}
3.4. 使用用例
func main() {
// 假设这是远程数据源
src := []byte("The quick brown fox jumps over the lazy dog.")
// 创建一个最大容量为 1000 字节的缓存
fetcher := &DummyFetcher{Source: src}
cache := NewCache(fetcher, 1000)
// 第一次读取 offset=10, size=10,需要远程拉取
length, out, err := cache.Read(10, 10)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Length: %d, Output: %s\n", length, string(out))
// 第二次读取相同范围,不会触发远程拉取
length, out, err = cache.Read(10, 10)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Length: %d, Output: %s (from cache)\n", length, string(out))
// 另一次读取不同范围 offset=20, size=15,可能触发远程拉取
length, out, err = cache.Read(20, 15)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Length: %d, Output: %s\n", length, string(out))
}
// 输出
2025/06/17 11:23:12 Remote fetch: offset=10 size=10
Length: 10, Output: brown fox
Length: 10, Output: brown fox (from cache)
2025/06/17 11:23:12 Remote fetch: offset=20 size=15
Length: 15, Output: jumps over the
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.