35

cache2go源码阅读

 4 years ago
source link: https://www.tuicool.com/articles/BVjIvaU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

简介

项目源码: 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 <[email protected]>
 *
 *   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
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK