在Eino 的duckduckgo 搜索工具组件中有个线程安全、可设置过期时间的 Cache 实现,具有以下特点:
- 内置 map
- sync.RWMutex 读写锁
- 后台 goroutine 定期扫描并清理过期条目
代码见下图:
/*
* Copyright 2024 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ddgsearch
import (
"runtime"
"sync"
"time"
)
// janitor is a background task that cleans up expired Cache items
type janitor struct {
interval time.Duration
stop chan struct{}
}
// Run starts the janitor in a new goroutine
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.deleteExpired()
case <-j.stop:
ticker.Stop()
return
}
}
}
// stopJanitor stops the janitor
func stopJanitor(c *cache) {
c.janitor.stop <- struct{}{}
}
// newJanitor creates a new janitor with the specified interval
func newJanitor(interval time.Duration) *janitor {
return &janitor{
interval: interval,
stop: make(chan struct{}),
}
}
// cache implements a simple in-memory cache with expiration
type cache struct {
mu sync.RWMutex
items map[string]*cacheItem
maxAge time.Duration
janitor *janitor
}
type cacheItem struct {
value interface{}
expiration time.Time
}
func newCache(maxAge time.Duration) *cache {
c := &cache{
items: make(map[string]*cacheItem),
maxAge: maxAge,
janitor: newJanitor(maxAge),
}
go c.janitor.Run(c)
runtime.SetFinalizer(c, stopJanitor)
return c
}
func (c *cache) get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.items[key]
if !exists {
return nil, false
}
if time.Now().After(item.expiration) {
delete(c.items, key)
return nil, false
}
return item.value, true
}
func (c *cache) set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = &cacheItem{
value: value,
expiration: time.Now().Add(c.maxAge),
}
}
func (c *cache) delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *cache) clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = make(map[string]*cacheItem)
}
func (c *cache) deleteExpired() {
now := time.Now()
expiredKeys := make([]string, 0)
c.mu.RLock() // add read lock extract expired key
for k, v := range c.items {
if now.After(v.expiration) {
expiredKeys = append(expiredKeys, k)
}
}
c.mu.RUnlock()
c.mu.Lock() // add write locks Delete expired keys
defer c.mu.Unlock()
for _, k := range expiredKeys {
delete(c.items, k)
}
}
上述实现cache 的get方法实现时, 在检索到key对应的元素时, 如果过期, 从对应的map中删除。 该操作有点多余, 且RLock无法保证写操作的线程安全性, 建议检索到过期后直接视为未命中即可, 让后台线程定期删除
修改后的代码如下:
func (c *cache) get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.items[key]
if !exists {
return nil, false
}
if time.Now().After(item.expiration) {
//delete(c.items, key)
return nil, false
}
return item.value, true
}
⚠️ 注意事项
- 不能在读锁中删除map中的元素:因为 delete 是写操作,必须加写锁(Lock())。
- 避免双重检查加锁:不要先用 RLock() 读一次,再用 Lock() 写一次,这样可能导致竞态条件。如果需求直接用写锁一次完成判断+删除即可。