cache2go源码阅读

l1nkkk · · 569 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

简介

项目源码:https://github.com/muesli/cac...

这个项目代码量很少,看完再模仿写一遍后觉得非常适合初学者。这也是我看的第一个go项目。对学习锁和并发有很大帮助,里面的很多代码姿势也值得去学习。

功能:

  • 实现了并发安全
  • 可以设置缓存的生命周期,删除,添加,访问数据时的回调函数。
  • 可以记录缓存的最近访问,访问次数,创建时间。
  • 可以自动执行过期缓存清理(里面的代码很值得去学习体会)

这个缓存库里边最主要的有三个文件cache.go,chchetable.go,cacheItem.go。这个代码分析是分析0.1版本的源码
cache.go里定义并声明了一个缓存数据库。
chchetable.go里定义了缓存数据表。
cacheItem.go里定义的则是缓存数据表的条目。
调用关系为cache.go -> chchetable.go -> cacheItem.go

疑问:

  • 在map中使用interface{},作者似乎没对interface{}底层数据为不能比较这种情况(比如是slice)做什么处理
  • chchetable.go中的log用的比较诡异,在我看来弄成内置的是不是更省事呢?
  • 有些单个函数里面频繁的加锁解锁(eg: 加只读锁,解只读锁->加互斥锁,解互斥锁)。是不是更加影响到性能,能不能直接加互斥锁,解互斥锁。

cacheItem.go

type CacheItem struct {
    sync.RWMutex

    // The item's key.
    // 条目的键
    key interface{}

    // The item's data.
    // 条目的值
    data interface{}

    // How long will the item live in the cache when not being accessed/kept alive.
    // 存活时间
    lifeSpan time.Duration

    // Creation timestamp.
    // 创造这个条目的时间
    createdOn time.Time
    // Last access timestamp.、
    //最近访问的时间
    accessedOn time.Time
    // How often the item was accessed.
    // 访问次数
    accessCount int64
    // Callback method triggered right before removing the item from the cache
    // 回调函数。当销毁时
    aboutToExpire func(key interface{})
}

下面列出重要的函数和方法

1.CacheItem的生成

func CreateCacheItem(key interface{}, lifeSpan time.Duration, data interface{}) CacheItem {
    t := time.Now()
    return CacheItem{
        key:           key,
        lifeSpan:      lifeSpan,
        createdOn:     t,
        accessedOn:    t,
        accessCount:   0,
        aboutToExpire: nil,
        data:          data,
    }
}

2.touch一下缓存,让其保持鲜活

func (item *CacheItem) KeepAlive() {
    item.Lock()
    defer item.Unlock()
    item.accessedOn = time.Now()
    item.accessCount++
}

chchetable.go

type CacheTable struct {
    //巧用内置匿名变量,这个类型有了读写锁的性质
    sync.RWMutex

    // The table's name.
    // 表名
    name string
    // All cached items.
    // 条目
    items map[interface{}]*CacheItem

    // Timer responsible for triggering cleanup.
    // 用来触发方法expirationCheck
    cleanupTimer *time.Timer
    // Current timer duration.
    // 赋值给cleanupTimer的清理周期
    cleanupInterval time.Duration

    // The logger used for this table.
    // 记录日志,怎不用成匿名变量呢?
    logger *log.Logger

    // Callback method triggered when trying to load a non-existing key.
    // 如果获取值不存在时调用
    loadData func(key interface{}, args ...interface{}) *CacheItem
    // Callback method triggered when adding a new item to the cache.
    // 如果加入条目要触发什么
    addedItem func(item *CacheItem)
    // Callback method triggered before deleting an item from the cache.
    // 如果删除条目要触发什么
    aboutToDeleteItem func(item *CacheItem)
}

下面列出重要的函数和方法

1.遍历表的所有条目,并根据提供的trans对条目进行解析

func (table *CacheTable) Foreach(trans func(key interface{}, item *CacheItem)) {
    table.RLock()
    defer table.RUnlock()

    for k, v := range table.items {
        // 翻译函数,map[interface{}]*CacheItem
        trans(k, v)
    }
}

2.缓存条目的过期检查,可自我更新检查的频率。(此源码的一个重要看点

func (table *CacheTable) expirationCheck() {
    table.Lock()
    if table.cleanupTimer != nil {
        // 不是nil就停止timer的执行
        table.cleanupTimer.Stop()
    }
    if table.cleanupInterval > 0 {
        // 之前table.cleanupTimer是执行的。
        // 能运行到这可能是因为cleanupTimer触发的,
        // 也可能是因为方法Add或方法NotFoundAdd的调用
        table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name)
    } else {
        // 之前table.cleanupTimer是stop的,为表下载过期检查
        // 能运行到这不可能是因为cleanupTimer触发的
        table.log("Expiration check installed for table", table.name)
    }

    // Cache value so we don't keep blocking the mutex.
    // 获取表数据
    items := table.items
    table.Unlock()

    // To be more accurate with timers, we would need to update 'now' on every
    // loop iteration. Not sure it's really efficient though.
    // now不该放到循环里面去每次更新一次吗?
    now := time.Now()
    //smallestDuration记录所有条目中剩余时间的最小值
    smallestDuration := 0 * time.Second
    for key, item := range items {
        // Cache values so we don't keep blocking the mutex.
        item.RLock()
        lifeSpan := item.lifeSpan     //存活时间
        accessedOn := item.accessedOn //最后访问时间
        item.RUnlock()

        if lifeSpan == 0 {
            //等于0表明没设置过期
            continue
        }
        if now.Sub(accessedOn) >= lifeSpan {
            // Item has excessed its lifespan.
            // 表明该条目已经过期
            table.Delete(key)
        } else {
            // Find the item chronologically closest to its end-of-lifespan.
            // 更新smallestDuration
            if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
                smallestDuration = lifeSpan - now.Sub(accessedOn)
            }
        }
    }

    // Setup the interval for the next cleanup run.
    // 决定下次再调用这个方法是什么时候
    table.Lock()
    table.cleanupInterval = smallestDuration
    // 关键哦!!!!!不过感觉这里使用go的意义不大吧。可以省略吗
    if smallestDuration > 0 {
        table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
            go table.expirationCheck()
        })
    }
    table.Unlock()
}

3.添加条目

func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
    item := CreateCacheItem(key, lifeSpan, data)

    // Add item to cache.
    table.Lock()
    table.log("Adding item with key", key, "and lifespan of", lifeSpan, "to table", table.name)
    table.items[key] = &item

    // Cache values so we don't keep blocking the mutex.
    expDur := table.cleanupInterval
    addedItem := table.addedItem
    table.Unlock()

    // Trigger callback after adding an item to cache.
    // 如果有添加条目时的触发器
    // 问题:如果table中此时的addedItem被改了,将怎样,
    // 调用的是那个函数,这取决于函数类型是值类型还是引用类型
    // 经过测试得出结论,其为值类型,改了还是运行原来的
    if addedItem != nil {
        addedItem(&item)
    }

    // If we haven't set up any expiration check timer or found a more imminent item.
    if lifeSpan > 0 && (expDur == 0 || lifeSpan < expDur) {
        //在这里运行检测expirationCheck
        table.expirationCheck()
    }

    return &item
}

4.删除条目

// Delete an item from the cache.
func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
    // 看看是否存在
    table.RLock()
    r, ok := table.items[key]
    if !ok {
        table.RUnlock()
        return nil, ErrKeyNotFound
    }

    // Cache value so we don't keep blocking the mutex.
    aboutToDeleteItem := table.aboutToDeleteItem
    table.RUnlock()

    // Trigger callbacks before deleting an item from cache.
    // table有没有delete的触发器,有的话就调用吧
    if aboutToDeleteItem != nil {
        aboutToDeleteItem(r)
    }

    r.RLock()
    defer r.RUnlock()
    // 看item有没有delete触发器。不太明白为什么两个触发器?
    if r.aboutToExpire != nil {
        r.aboutToExpire(key)
    }

    table.Lock()
    defer table.Unlock()
    table.log("Deleting item with key", key, "created on", r.createdOn, "and hit", r.accessCount, "times from table", table.name)
    // 可以发现前面的所有代码都是为了安全和清理做准备的,这一句才是函数主要意义
    delete(table.items, key)

    return r, nil
}

5.获取条目

func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
    table.RLock()
    r, ok := table.items[key]
    loadData := table.loadData
    table.RUnlock()

    if ok {
        // Update access counter and timestamp.
        r.KeepAlive()
        return r, nil
    }

    // Item doesn't exist in cache. Try and fetch it with a data-loader.
    if loadData != nil {
        // 如果存在loadData这个触发器就运行之
        item := loadData(key, args...)
        if item != nil {
            table.Add(key, item.lifeSpan, item.data)
            return item, nil
        }

        return nil, ErrKeyNotFoundOrLoadable
    }

    return nil, ErrKeyNotFound
}

6.清空表

func (table *CacheTable) Flush() {
    table.Lock()
    defer table.Unlock()

    table.log("Flushing table", table.name)
    //重新生成一个空的,让gc去处理
    table.items = make(map[interface{}]*CacheItem)
    table.cleanupInterval = 0
    if table.cleanupTimer != nil {
        table.cleanupTimer.Stop()
    }
}

根据访问次数排序

type CacheItemPair struct {
    Key         interface{}
    AccessCount int64
}

// A slice of CacheIemPairs that implements sort. Interface to sort by AccessCount.
type CacheItemPairList []CacheItemPair

func (p CacheItemPairList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p CacheItemPairList) Len() int           { return len(p) }
func (p CacheItemPairList) Less(i, j int) bool { return p[i].AccessCount > p[j].AccessCount }

// 按访问量排序输出
func (table *CacheTable) MostAccessed(count int64) []*CacheItem {
    table.RLock()
    defer table.RUnlock()

    p := make(CacheItemPairList, len(table.items))
    i := 0
    for k, v := range table.items {
        p[i] = CacheItemPair{k, v.accessCount}
        i++
    }
    sort.Sort(p)

    var r []*CacheItem
    c := int64(0)
    for _, v := range p {
        if c >= count {
            break
        }

        item, ok := table.items[v.Key]
        if ok {
            r = append(r, item)
        }
        c++
    }

    return r
}

cache.go

/*
 * Simple caching library with expiration capabilities
 *     Copyright (c) 2012, Radu Ioan Fericean
 *                   2013, Christian Muehlhaeuser <muesli@gmail.com>
 *
 *   For license see LICENSE.txt
 */

package cache2go

import (
    "sync"
)


var (
    // 相当于数据库
    cache = make(map[string]*CacheTable)
    mutex sync.RWMutex
)

// Returns the existing cache table with given name or creates a new one
// if the table does not exist yet.
// 把这个做成方法会是怎样呢?
func Cache(table string) *CacheTable {
    mutex.RLock()
    t, ok := cache[table]
    mutex.RUnlock()
    //如果cache map没有table这个key,创!
    if !ok {
        //课件CacneTable 两个最关键的变量就是如下
        t = &CacheTable{
            name:  table, //表名
            items: make(map[interface{}]*CacheItem),
        }

        //上锁并赋值
        mutex.Lock()
        cache[table] = t
        mutex.Unlock()
    }

    return t
}

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:l1nkkk

查看原文:cache2go源码阅读

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

569 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传