今天我们来看cachetable.go这个源码文件,除了前面介绍过的主要数据结构CacheTable外还有如下2个类型:

下面先看剩下2个类型是怎么定义的:

CacheItemPair非常简单,注释一句话讲的很清楚,是用来映射key到访问计数的

  CacheItemPairList明显就是一个CacheItemPair组成的“列表”,在go中对应的就是切片,绑定到CacheItemPairList类型的方法有3个,Swap和Len太直观了,不再赘述,Less方法判断CacheItemPairList中的第i个CacheItemPair和第j个CacheItemPair的AccessCount大小关系,前者大则返回true,反之false。逻辑是这样,用处我们在后面具体看(和自定义排序相关,后面我们会讲到这里的玄机)。

  • CacheTable类型绑定的方法

这个源码文件中除了上面讲到的部分外就只剩下CacheTable类型绑定的方法了,先看一下有多少:

下面一个个来看吧~


1.

Count()方法返回的是指定CacheTable中的item条目数,这里的table.items是一个map类型,len函数返回map中元素数量


2.

Foreach方法接收一个函数参数,方法内遍历了items,把每个key和value都丢给了trans函数来处理,trans函数的参数列表是key interface{}, item *CacheItem,分别对应任意类型的key和CacheItem类型的缓存条目的引用。


3.

  先看一下参数列表:f func(interface{}, ...interface{}) *CacheItem

  形参的意思是:形参名:f,形参类型:func(interface{}, ...interface{}) *CacheItem

  形参类型是一个函数类型,这个函数的参数是一个key和不定数目的argument,返回值是CacheItem指针。

  然后在SetDataLoader中将这个函数f丢给了table的loadData属性。loadData所指向的方法是什么时候被调用?注释说which will be called when trying to access a non-existing key.也就是访问一个不存在的key时会调用到。也就是说当访问一个不存在的key时,需要调用一个方法,这个方法通过SetDataLoader设定,方法的实现由用户来自定义。


4.

  SetAddedItemCallback方法也很直观,当添加一个Item到缓存表中时会被调用的一个方法,绑定到CacheTable.addedItem上,被绑定的这个方法只接收一个参数,但是这里的参数变量名呢?为什么只写了类型*CacheItem?我去作者的github上提一个issue问问吧~开玩笑开玩笑,这里其实不需要形参变量的名字,发现没?

SetAddedItemCallback方法的形参名是f,类型是后面这个函数,也就是说func(*CacheItem)被作为一个类型,这时候假设写成func(item *CacheItem),这里的item用得到吗?看下面这个例子就清晰了:

1func main() {
2    var show func(int)
3    show = func(num int) { fmt.Println(num) }
4    show(123)
5}

  如上所示,定义变量show为func(int)类型的时候不需要形参变量。这个地方明白的看起来很简单,一时没有想明白的可能心中会纠结一会~

ok,也就是说SetAddedItemCallback方法设置了一个回调函数,当添加一个CacheItem的时候,同时会调用这个回调函数,这个函数可以选择对CacheItem做一些处理,比如打个日志啊什么的。


5.

看到这里应该很轻松了,和上面的回调函数一样,这个方法是设置删除Item的时候调用的一个方法。


6.

这个就不需要讲解了,把一个logger实例丢给table的logger属性。日志相关的知识点我会在golang专题中详细介绍。


7.

expirationCheck方法比较长,分析部分放在一起有点臃肿,所以我选择了源码加注释的方式来展示,每一行代码和相应的含义如下:

 1// Expiration check loop, triggered by a self-adjusting timer.
2// 【由计时器触发的到期检查】
3func (table *CacheTable) expirationCheck() {
4    table.Lock()
5    //【计时器暂停】
6    if table.cleanupTimer != nil {
7        table.cleanupTimer.Stop()
8    }
9    //【计时器的时间间隔】
10    if table.cleanupInterval > 0 {
11        table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name)
12    } else {
13        table.log("Expiration check installed for table", table.name)
14    }
15
16    // To be more accurate with timers, we would need to update 'now' on every
17    // loop iteration. Not sure it's really efficient though.
18    //【当前时间】
19    now := time.Now()
20    //【最小时间间隔,这里暂定义为0,下面代码会更新这个值】
21    smallestDuration := 0 * time.Second
22    //【遍历一个table中的items】
23    for key, item := range table.items {
24        // Cache values so we don't keep blocking the mutex.
25        item.RLock()
26        //【设置好的存活时间】
27        lifeSpan := item.lifeSpan
28        //【最后一次访问的时间】
29        accessedOn := item.accessedOn
30        item.RUnlock()
31
32        //【存活时间为0的item不作处理,也就是一直存活】
33        if lifeSpan == 0 {
34            continue
35        }
36        //【这个减法算出来的是这个item已经没有被访问的时间,如果比存活时间长,说明过期了,可以删了】
37        if now.Sub(accessedOn) >= lifeSpan {
38            // Item has excessed its lifespan.
39            //【删除操作】
40            table.deleteInternal(key)
41        } else {
42            // Find the item chronologically closest to its end-of-lifespan.
43            //【按照时间顺序找到最接近过期时间的条目】
44            //【如果最后一次访问的时间到当前时间的间隔小于smallestDuration,则更新smallestDuration】
45            if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
46                smallestDuration = lifeSpan - now.Sub(accessedOn)
47            }
48        }
49    }
50
51    // Setup the interval for the next cleanup run.
52    //【上面已经找到了最近接过期时间的时间间隔,这里将这个时间丢给了cleanupInterval】
53    table.cleanupInterval = smallestDuration
54    //【如果是0就不科学了,除非所有条目都是0,那就不需要过期检测了】
55    if smallestDuration > 0 {
56        //【计时器设置为smallestDuration,时间到则调用func这个函数】
57        table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
58            //这里并不是循环启动goroutine,启动一个新的goroutine后当前goroutine会退出,这里不会引起goroutine泄漏。
59            go table.expirationCheck()
60        })
61    }
62    table.Unlock()
63}

expirationCheck方法无非是做一个定期的数据过期检查操作,到目前为止这是项目中最复杂的一个方法,下面继续看剩下的部分。


8.

  如上图所示,剩下的方法中划红线三个互相关联,我们放在一起看。

  这次自上而下分析,明显Add和NotFoundAdd方法会调用addInternal方法,所以我们先看Add和NotFoundAdd方法。

先看Add()

  注释部分说的很清楚,Add方法添加一个key/value对到cache,三个参数除了key、data、lifeSpan的含义我们在第一讲分析CacheItem类型的时候都已经介绍过。

  NewCacheItem函数是cacheitem.go中定义的一个创建CacheItem类型实例的函数,返回值是*CacheItem类型。Add方法创建一个CacheItem类型实例后,将该实例的指针丢给了addInternal方法,然后返回了该指针。addInternal我们后面再看具体做了什么。


9.

  大家注意到没有,这里的注释有一个单词写错了,they key应该是the key。

  这个方法的参数和上面的Add方法是一样一样的,含义无需多说,方法体主要分2个部分:

  开始的if判断是检查items中是否有这个key,存在则返回false;后面的代码自然就是不存在的时候执行的,创建一个CacheItem类型的实例,然后调用addInternal添加item,最后返回true;也就是说这个函数返回true是NotFound的情况。

  ok,下面就可以看看addInternal这个方法干了啥了。


10.

  这个方法无非是将CacheItem类型的实例添加到CacheTable中。方法开头的注释告诉我们调用这个方法前需要加锁,函数体前2行做了一个打日志和赋值操作,很好理解,然后将table.cleanupInterval和table.addedItem保存到局部变量,紧接着释放了锁。

  后面的if部分调用了addedItem这个回调函数,也就是添加一个item时需要调用的函数。最后一个if判断稍微绕一点;

  if的第一个条件:item.lifeSpan > 0,也就是当前item设置的存活时间是正数;然后&& (expDur == 0 || item.lifeSpan < expDur),expDur保存的是table.cleanupInterval,这个值为0也就是还没有设置检查时间间隔,或者item.lifeSpan < expDur也就是设置了,但是当前新增的item的lifeSpan要更小,这个时候就触发expirationCheck执行。这里可能有点绕,要注意lifeSpan是一个item的存活时间,而cleanupInterval是对于一个table来说触发检查还剩余的时间,如果前者更小,那么就说明需要提前出发check操作了。


11.

剩下的不多了,我们再看一组删除相关的方法

还是上面的套路,先看上层的调用者,当然就是Delete

  收一个key,调用deleteInternal(key)来完成删除操作,这里实在没有啥可讲的了,我们来看deleteInternal方法是怎么写的


12.

deleteInternal方法我也用详细注释的方式来解释吧~

 1func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) {
2    r, ok := table.items[key]
3    //【如果table中不存在key对应的item,则返回一个error】
4    //【ErrKeyNotFound在errors.go中定义,是errors.New("Key not found in cache")】
5    if !ok {
6        return nil, ErrKeyNotFound
7    }
8
9    // Cache value so we don't keep blocking the mutex.
10    //【将要删除的item缓存起来】
11    aboutToDeleteItem := table.aboutToDeleteItem
12    table.Unlock()
13
14    // Trigger callbacks before deleting an item from cache.
15    //【删除操作执行前调用的回调函数,这个函数是CacheTable的属性,对应下面的是aboutToExpire是CacheItem的属性】
16    if aboutToDeleteItem != nil {
17        aboutToDeleteItem(r)
18    }
19
20    r.RLock()
21    defer r.RUnlock()
22    //【这里对这条item加了一个读锁,然后执行了aboutToExpire回调函数,这个函数需要在item刚好要删除前执行】
23    if r.aboutToExpire != nil {
24        r.aboutToExpire(key)
25    }
26
27    table.Lock()
28    //【这里对表加了锁,上面已经对item加了读锁,然后这里执行delete函数删除了这个item】
29    //【delete函数是专门用来从map中删除特定key指定的元素的】
30    table.log("Deleting item with key", key, "created on", r.createdOn, "and hit", r.accessCount, "times from table", table.name)
31    delete(table.items, key)
32
33    return r, nil
34}

13.

万里长征最后几步咯~

最后5(Not打头这个咱说过了)个方法目测不难,咱一个一个来过,先看Exists

这里我是想说:来,咱略过吧~

算了,为了教程的完整性,还是简单说一下,读锁的相关代码不需要说了,剩下的只有一行:

_, ok := table.items[key]

这里如果key存在,ok为true,反之为false,就是这样,简单吧~


14.

Value()方法讲解,看注释吧~

 1// Value returns an item from the cache and marks it to be kept alive. You can
2// pass additional arguments to your DataLoader callback function.
3func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
4    table.RLock()
5    r, ok := table.items[key]
6    //【loadData在load一个不存在的数据时被调用的回调函数】
7    loadData := table.loadData
8    table.RUnlock()
9
10    //【如果值存在,执行下面操作】
11    if ok {
12        // Update access counter and timestamp.
13        //【更新accessedOn为当前时间】
14        r.KeepAlive()
15        return r, nil
16    }
17
18    //【这里当然就是值不存在的时候了】
19    // Item doesn't exist in cache. Try and fetch it with a data-loader.
20    if loadData != nil {
21        //【loadData这个回调函数是需要返回CacheItem类型的指针数据的】
22        item := loadData(key, args...)
23        if item != nil {
24            //【loadData返回了item的时候,万事大吉,执行Add】
25            table.Add(key, item.lifeSpan, item.data)
26            return item, nil
27        }
28        //【item没有拿到,那就只能返回nil+错误信息了】
29        //【ErrKeyNotFoundOrLoadable是执行回调函数也没有拿到data的情况对应的错误类型】
30        return nil, ErrKeyNotFoundOrLoadable
31    }
32
33    //【这个return就有点无奈了,在loadData为nil的时候执行,也就是直接返回Key找不到】
34    return nil, ErrKeyNotFound
35}

15.

从注释可以看出来这个函数就是清空数据的作用,实现方式简单粗暴,让table的items属性指向一个新建的空map,cleanup操作对应的时间间隔设置为0,并且计时器停止。这里也可以得到cleanupInterval为0是什么场景,也就是说0不是代表清空操作死循环,间隔0秒就执行,而是表示不需要操作,缓存表还是空的。


16.

这个MostAccessed方法有点意思,涉及到sort.Sort的玩法,具体看下面注释:

 1// MostAccessed returns the most accessed items in this cache table
2//【访问频率高的count条item全部返回】
3func (table *CacheTable) MostAccessed(count int64) []*CacheItem {
4    table.RLock()
5    defer table.RUnlock()
6    //【这里的CacheItemPairList是[]CacheItemPair类型,是类型不是实例】
7    //【所以p是长度为len(table.items)的一个CacheItemPair类型的切片类型
8    p := make(CacheItemPairList, len(table.items))
9    i := 0
10    //【遍历items,将Key和AccessCount构造成CacheItemPair类型数据存入p切片】
11    for k, v := range table.items {
12        p[i] = CacheItemPair{k, v.accessCount}
13        i++
14    }
15    //【这里可以直接使用Sort方法来排序是因为CacheItemPairList实现了sort.Interface接口,也就是Swap,Len,Less三个方法】
16    //【但是需要留意上面的Less方法在定义的时候把逻辑倒过来了,导致排序是从大到小的】
17    sort.Sort(p)
18
19    var r []*CacheItem
20    c := int64(0)
21    for _, v := range p {
22        //【控制返回值数目】
23        if c >= count {
24            break
25        }
26
27        item, ok := table.items[v.Key]
28        if ok {
29            //【因为数据是按照访问频率从高到底排序的,所以可以从第一条数据开始加】
30            r = append(r, item)
31        }
32        c++
33    }
34
35    return r
36}

17.

最后一个方法了,哇咔咔,好长啊~~~

  这个函数也没有太多可以讲的,为了方便而整的内部日志函数

  都看懂了吗?下一讲我们会一口气把cache.go和examples里全部内容放在一起讲完,也就是完成整个项目的分析。3讲看完之后你肯定就对cache2go这个项目有一个全面的了解了!

cache2go - cachetable源码分析的更多相关文章

  1. 启航 - cache2go源码分析

    一.概述 我们今天开始第一部分“golang技能提升”.这一块我计划分析3个项目,一个是很流行的golang源码阅读入门项目cache2go,接着是非常流行的memcache的go语言版groupca ...

  2. 第九篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 cache table

    /** Spark SQL源码分析系列文章*/ Spark SQL 可以将数据缓存到内存中,我们可以见到的通过调用cache table tableName即可将一张表缓存到内存中,来极大的提高查询效 ...

  3. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  4. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  5. nginx源码分析之网络初始化

    nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...

  6. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  7. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  8. zookeeper源码分析之三客户端发送请求流程

    znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...

  9. java使用websocket,并且获取HttpSession,源码分析

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...

随机推荐

  1. Windows Programming ---- Beginning Visual C#

    span.kw { color: #007020; font-weight: bold; } code > span.dt { color: #902000; } code > span. ...

  2. CSS3中的Transition属性详解

    w3c标准中对CSS3的transition这是样描述的:“CSS的transition允许CSS的属性值在一定的时间区间内平滑地过渡.这种效果可以在鼠标单击.获得焦点.被点击或对元素任何改变中触发, ...

  3. [置顶] Android应用开发之版本更新你莫愁

    传送门 ☞ 轮子的专栏 ☞ 转载请注明 ☞ http://blog.csdn.net/leverage_1229 今天我们学习如何实现Android应用的自动更新版本功能,这是在各种语言编写的应用中都 ...

  4. dataList中实现用复选框一次删除多行问题

    先遍历每一行,判断checkBox是否选中,再获取选中行的主键Id 删除就行了 ,,,foreach(DatalistRow rowview in Datalist.Rows) //遍历Datalis ...

  5. AppSettings

    1.winform中读写配置文件appSettings 一节中的配置. #region 读写配置文件 /// <summary> /// 修改配置文件中某项的值 /// </summ ...

  6. U盘安装win7+CentOS7双系统

    决定要好好学习一下Linux了,不管是为了以后技术发展的需要抑或是满足自己的兴趣,都是时候来涉足一下了.我准备在我的ThinkPad X200i(一个老掉牙的老TP本子)上装一个Linux发行版,这里 ...

  7. Java Swing创建自定义闪屏:在闪屏上添加Swing进度条控件(转)

    本文将讲解如何做一个类似MyEclipse启动画面的闪屏,为Java Swing应用程序增添魅力. 首先看一下效果图吧, 原理很简单,就是创建一个Dialog,Dialog有一个进度条和一个Label ...

  8. 数据库中Schema、Database、User、Table的关系[转]

    数据库的初学者往往会对关系型数据库模式(schema).数据库(database).表(table).用户(user)之间感到迷惘,总感觉他们的关系千丝万缕,但又不知道他们的联系和区别在哪里,对一些问 ...

  9. USACO Section 1.2 Dual Palindromes 解题报告

    题目 题目描述 有一些数(如 21),在十进制时不是回文数,但在其它进制(如二进制时为 10101)时就是回文数. 编一个程序,从文件读入两个十进制数N.S.然后找出前 N 个满足大于 S 且在两种以 ...

  10. CentOS 7 网卡命名修改为ethx格式

    Linux 操作系统的网卡设备的传统命名方式是 eth0.eth1.eth2等,而 CentOS7 提供了不同的命名规则,默认是基于固件.拓扑.位置信息来分配.这样做的优点是命名全自动的.可预知的,缺 ...