Chinaunix首页 | 论坛 | 博客
  • 博客访问: 425795
  • 博文数量: 43
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 518
  • 用 户 组: 普通用户
  • 注册时间: 2015-12-14 12:10
个人简介

邮箱:oxwangfeng@qq.com

文章分类

全部博文(43)

文章存档

2021年(1)

2018年(7)

2017年(9)

2016年(26)

我的朋友

分类: LINUX

2018-08-28 21:02:01



  1. - [freecache](#freecache)
  2.     - [结构](#结构)
  3.     - [segment](#segment)
  4.         - [set()](#set)
  5.             - [淘汰算法](#淘汰算法)
  6.         - [get()](#get)
  7.     - [RingBuf](#ringbuf)
  8.         - [Write()](#write)
  9.         - [ReadAt()](#readat)
  10.         - [Evacuate()](#evacuate)

  11. <!-- /TOC -->

  12. # freecache


  13. ## 结构

  14. 一个 Cache 有 256 个 segment, 每个 segment 有一把锁。

  15. 一个 segment 有 `256 * n` 个 slot, 每个 slot 存放的是 entry 的指针信息。槽的数量是动态调整的, 如果同一 slotID 对应的槽的数量超过或等于 n 了, 那么槽的数量就会增多一倍。

  16. segment 的结构如下:

  17. ```go
  18. type segment struct {
  19.     rb RingBuf // 存放具体的数据
  20.     segId int
  21.     entryCount int64 // entry 的数量
  22.     totalCount int64 // entry 的数量, 包括删除的entry
  23.     totalTime int64 // 时间总和, 用于计算平均时间
  24.     vacuumLen int64 // 空闲空间, 空闲空间不够的话需要覆盖旧数据
  25.     slotLens [256]int32 // 每个槽的实际长度
  26.     slotCap int32 // 每个指针在槽中占用多大空间, 初始时是1
  27.     slotsData []entryPtr // 指针数组
  28. }
  29. ```

  30. 指针记录了 entry 在 RingBuf 中的信息, 指针结构如下:

  31. ```go
  32. type entryPtr struct {
  33.     offset int64 // 在 RingBuf 中的偏移位置
  34.     hash16 uint16 // 用于排序的, RingBuf 中的 entry 根据这个字段排序, 查找时使用二分查找。
  35.     keyLen uint16 // key 长度, 用于比较指针指向的 entry 是不是想要的(主要是可以在不查找 entry 的情况下排除掉一些不匹配的)
  36.     reserved uint32
  37. }
  38. ```

  39. 具体的数据放在了 RingBuf 中, RingBuf 是一个环形缓存, 底层是一个字节数组, 数组按序写入, 写满后重新从0位置开始写。


  40. ## segment


  41. ### set()

  42. slotID 和 hash16 的计算(hashVal 是 `hash(key)` 的值):

  43. ```go
  44. slotID := uint8(hashVal >> 8)
  45. hash16 := uint16(hashVal >> 16)
  46. ```

  47. 在一个初始状态的 segment 中写入数据的过程是这样的:

  48. ```go
  49. // 查找具有相同 slotID 的 slot, 因为相同 id 的都放在一起了, 像这样: `| idx | idx | id-ok | id-ok | idx | idx | ...`, 所以可以直接根据 slotID 来计算出偏移量
  50. slotOff := slotID * slotCap
  51. slot := findSlotsSameID(slotsData, slotID, slotOff)
  52. // 查找新的 entry 应该在哪个索引位置, 这里使用了二分查找来加速, 根据 hash16 加速
  53. idx := lookupSlotIndex(slot, key, hash16)
  54. // 把新的 entryPtr 插入到 idx 位置, 保证在 slot 中是按照 hash16 排序的。
  55. insertEntryPtr(idx)
  56. // 写入到 RingBuf 中的数据按先后顺序分别是: hdr, key, value, [一些空格], 这里的空格可能发生的情况是 value 为空时。
  57. insertDataToRingBuf()
  58. ```

  59. - 怎么按照 hash16 排序?
  60.     - 先根据 key 和 hash16 通过二分查找来找到应该放在哪个位置 idx, 然后插入数据的时候插入到 idx 中, 这样就保证了相同 slotID 的那一块数据是按照 hash16 来排序的。
  61. - 插入 entryPtr 时的扩容问题?
  62.     - 插入数据时会检测 slotID 对应的槽中有多少 ptr, 如果 `slotLen[id] == slotCap` 了, 再插入 ptr 时就会覆盖旧的 ptr, 所以这里会有一个扩容的操作。
  63.     - 扩容很粗暴, 直接将 `slotCap * 2`, 然后原来的 ptr 放到正确的位置上。 比如现在是 `|id1|id1|id2|id2|`, 扩容后 slotCap 变为 4, 变成 `|id1|id1| | |id2|id2| | |`


  64. hdr 的结构如下:

  65. ```go
  66. type entryHdr struct {
  67.     accessTime uint32
  68.     expireAt uint32
  69.     keyLen uint16
  70.     hash16 uint16
  71.     valLen uint32
  72.     valCap uint32
  73.     deleted bool
  74.     slotId uint8
  75.     reserved uint16
  76. }
  77. ```


  78. 如果前后两个 entry 的 key 相同, 那么前一个 entry 会被删除。 这里的删除分两种情况:
  79. 1. 新的 value 长度小于等于旧的 value 长度。 这意味着旧 entry 在 RingBuf 中的空间可以被复用, 只需要修改 entryHdr 的信息以及用新 value 覆盖旧 value 即可。
  80. 2. 新的 value 长度大于旧的 value 长度。此时会将旧 entry 软删除(entryHdr的deleted字段置为true, entryPtr从slotsData中删除)。然后再将新 entry 插入到 RingBuf 中, 将 entryHdr 插入到 slotsData 中。


  81. #### 淘汰算法

  82. 淘汰发生在 `set()` 被调用时, 会检查空闲空间是否能容得下新数据。

  83. ```go
  84. func (seg *segment) evacuate(entryLen int64, slotId uint8, now uint32) (slotModified bool) {
  85.     consecutiveEvacuate := 0 // 连续将有效 entry 重写的次数
  86.     for seg.vacuumLen < entryLen {
  87.         // 软删除的 和 过期的, vacuumLen 会增加被删除的 entryLen, 所以下次 oldOff 可以指向下一个 entry.
  88.         // 重写 entry 的话会修改 end, 但 vacuumLen 不变, 所以下次 oldOff 也可以指向下一个 entry.
  89.         oldOff := seg.rb.End() + seg.vacuumLen - seg.rb.Size()
  90.         oldEntry := readEntryByOff(oldOff)
  91.         // 被软删除了, 回收空间
  92.         if oldEntry.deleted {
  93.             consecutiveEvacuate = 0
  94.             seg.vacuumLen += oldEntryLen
  95.             continue
  96.         }
  97.         // 过期的, 或者最后一次访问时间小于等于平均访问时间的, 或者连续重写 entry 的次数超过 5 次
  98.         expired := oldHdr.expireAt != 0 && oldHdr.expireAt < now
  99.         leastRecentUsed := int64(oldHdr.accessTime)*seg.totalCount <= seg.totalTime // accessTime <= totalTime / totalCount
  100.         if expired || leastRecentUsed || consecutiveEvacuate > 5 {
  101.             // 删除 ptr 并且标记 entry 被删除了
  102.             seg.delEntryPtr(oldHdr.slotId, oldHdr.hash16, oldOff)
  103.             if oldHdr.slotId == slotId {
  104.                 slotModified = true
  105.             }
  106.             consecutiveEvacuate = 0
  107.             seg.vacuumLen += oldEntryLen
  108.         } else {
  109.             // evacuate an old entry that has been accessed recently for better cache hit rate.
  110.             // 将 oldOff 对应的 entry 追加到 ringBuf 后, 相当于重新写了一遍。
  111.             newOff := seg.rb.Evacuate(oldOff, int(oldEntryLen))
  112.             // 更新指针的偏移
  113.             seg.updateEntryPtr(oldHdr.slotId, oldHdr.hash16, oldOff, newOff)
  114.             consecutiveEvacuate++
  115.         }
  116.     }
  117.     return
  118. }
  119. ```

  120. 在检查空余空间时会将没有被删除的且没有过期的 entry 从前面重写到 RingBuf 后面, 防止被新的数据覆盖。




  121. ### get()

  122. ```go
  123. hdr := lookupHdr(slotsData, key, hash16) // 二分查找
  124. if expired(hdr) {
  125.     // 软删除, 标记 hdr.deleted, 删除 entryPtr
  126.     delEntryPtr()
  127.     return
  128. }
  129. // 设置 hdr.accessTime 为当前时间
  130. setHdrAccessTime(now)
  131. return readValue()
  132. ```



  133. ## RingBuf

  134. ### Write()

  135. 代码:

  136. ```go
  137. func (rb *RingBuf) Write(p []byte) (n int, err error) {
  138.     if len(p) > len(rb.data) {
  139.         err = ErrOutOfRange
  140.         return
  141.     }
  142.     for n < len(p) {
  143.         written := copy(rb.data[rb.index:], p[n:])
  144.         rb.end += int64(written)
  145.         n += written
  146.         rb.index += written
  147.         if rb.index >= len(rb.data) {
  148.             rb.index -= len(rb.data) // 只重置了 index, 没有重置 end
  149.         }
  150.     }
  151.     // begin 和 end 最多相距 len(data)
  152.     if int(rb.end-rb.begin) > len(rb.data) {
  153.         rb.begin = rb.end - int64(len(rb.data))
  154.     }
  155.     return
  156. }
  157. ```

  158. 假设 RingBuf 大小是 16, 初始状态:

  159. ```
  160. 0 16
  161. +--------------------------------------------------+
  162. | |
  163. +--------------------------------------------------+
  164. ^
  165. +
  166. begin, end, index
  167. ```

  168. 写了 10 个字节后:

  169. ```
  170. 0 10 16
  171. +--------------------------------+-----------------+
  172. | | |
  173. +--------------------------------+-----------------+
  174. ^ ^
  175. + +
  176. begin end, index
  177. ```

  178. 再写 10 个字节后:

  179. ```
  180. 0 4 16 20
  181. +------+-------------------------------------------+ +
  182. | | | |
  183. +------+-------------------------------------------+ +
  184.        ^ ^
  185.        + +
  186.     begin, index end
  187. ```


  188. ### ReadAt()

  189. 代码:

  190. ```go
  191. func (rb *RingBuf) ReadAt(p []byte, off int64) (n int, err error) {
  192.     if off > rb.end || off < rb.begin {
  193.         err = ErrOutOfRange
  194.         return
  195.     }
  196.     // off 是相对于 begin 而言的, off - begin 就是从有效数据的某个偏移位置开始读
  197.     var readOff int
  198.     if rb.end-rb.begin < int64(len(rb.data)) { // 说明 end 还没有超出 data 的范围
  199.         readOff = int(off - rb.begin) // 有效数据从 0 开始的
  200.     } else {
  201.         readOff = rb.index + int(off-rb.begin) // 有效数据从 index 开始, index 是有效数据的第一个位置, 也是下一个要覆盖写的位置
  202.     }
  203.     if readOff >= len(rb.data) {
  204.         readOff -= len(rb.data)
  205.     }
  206.     readEnd := readOff + int(rb.end-off) // 可被读取的数据最长为 end-off.
  207.     if readEnd <= len(rb.data) { // 没有超出实际边界
  208.         n = copy(p, rb.data[readOff:readEnd])
  209.     } else { // 超出实际边界了要分两次读取
  210.         n = copy(p, rb.data[readOff:])
  211.         if n < len(p) {
  212.             // readEnd - len(data) 是从 0 开始的有效数据的长度
  213.             n += copy(p[n:], rb.data[:readEnd-len(rb.data)])
  214.         }
  215.     }
  216.     if n < len(p) {
  217.         err = io.EOF
  218.     }
  219.     return
  220. }
  221. ```

  222. 假设现在写了 10 个字节, 要全部读取出来, 可能会调用 `ReadAt(p, 0)`, 其中 `len(p) == 10`:

  223. ```
  224. 0 10 16
  225. +--------------------------------+-----------------+
  226. | | |
  227. +--------------------------------+-----------------+
  228. ^ ^
  229. + +
  230. begin end, index
  231. ```

  232. 此时读取的位置是 `data[0:10]`

  233. 再次放入 10 个字节后, 状态变为:

  234. ```
  235. 0 4 16 20
  236. +------+-------------------------------------------+ +
  237. | | | |
  238. +------+-------------------------------------------+ +
  239.        ^ ^
  240.        + +
  241.     begin, index end
  242. ```

  243. 将刚才写入的 10 个字节读出来, 调用 `ReadAt(p, 10)`, 其中 `len(p) == 10`, 此时读取的位置是 `data[10: 20]`, 分两次读取, 先读取 `data[10:16]`, 再读取 `data[0:4]`



  244. ### Evacuate()

  245. Evacuate 读取特定位置的数据重新写到 RingBuffer 的后面。

  246. ```go
  247. func (rb *RingBuf) Evacuate(off int64, length int) (newOff int64) {
  248.     if off+int64(length) > rb.end || off < rb.begin {
  249.         return -1
  250.     }
  251.     // 转换 off
  252.     var readOff int
  253.     if rb.end-rb.begin < int64(len(rb.data)) {
  254.         readOff = int(off - rb.begin)
  255.     } else {
  256.         readOff = rb.index + int(off-rb.begin)
  257.     }
  258.     if readOff >= len(rb.data) {
  259.         readOff -= len(rb.data)
  260.     }

  261.     if readOff == rb.index {
  262.         // 读取的位置正好是有效数据开始的位置, 不需要复制了, 直接移动指针就可以了
  263.         rb.index += length
  264.         if rb.index >= len(rb.data) {
  265.             rb.index -= len(rb.data)
  266.         }
  267.     } else if readOff < rb.index {
  268.         // 要读取的数据在 index 前面
  269.         // 如果 readOff + length 超过了有效数据的范围了呢? 不会的, 上面会检测 off+length 是否在有效数据范围内的
  270.         var n = copy(rb.data[rb.index:], rb.data[readOff:readOff+length])
  271.         rb.index += n
  272.         if rb.index == len(rb.data) {
  273.             // data 已经写到底了, 读取的数据还不够 length, 读取剩余的数据到 data 开始的位置
  274.             rb.index = copy(rb.data, rb.data[readOff+n:readOff+length])
  275.         }
  276.     } else {
  277.         // 读取的位置在 index 后面
  278.         var readEnd = readOff + length
  279.         var n int
  280.         if readEnd <= len(rb.data) {
  281.             // 不用转弯, 要读取的数据在 index - len(data) 之间
  282.             n = copy(rb.data[rb.index:], rb.data[readOff:readEnd])
  283.             rb.index += n
  284.             if rb.index == len(rb.data) {
  285.                 // 既然 readOff > index, readEnd <= len(data), 那这里 index 不可能达到 len(data) 的位置啊
  286.                 rb.index = copy(rb.data, rb.data[readOff+n:readEnd])
  287.             }
  288.         } else {
  289.             // 需要转弯
  290.             n = copy(rb.data[rb.index:], rb.data[readOff:])
  291.             rb.index += n
  292.             var tail = length - n // 还有多少没读的
  293.             n = copy(rb.data[rb.index:], rb.data[:tail])
  294.             rb.index += n
  295.             if rb.index == len(rb.data) { // 写到底了还没有写完
  296.                 rb.index = copy(rb.data, rb.data[n:tail])
  297.             }
  298.         }
  299.     }
  300.     // 更新 end 和 begin, 确保 begin 和 end 最多相差 len(data) 的长度
  301.     newOff = rb.end
  302.     rb.end += int64(length)
  303.     if rb.begin < rb.end-int64(len(rb.data)) {
  304.         rb.begin = rb.end - int64(len(rb.data))
  305.     }
  306.     return
  307. }
  308. ```

  309. 基本思路是确定从 data 的哪个位置开始读取数据, 然后写到 index 后面。


阅读(4638) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~