Golang package輕量級KV數據緩存——go-cache源碼分析

来源:https://www.cnblogs.com/Moon-Light-Dream/archive/2020/03/14/12494683.html
-Advertisement-
Play Games

作者:Moon Light Dream 出處:https://www.cnblogs.com/Moon Light Dream/ 轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則必究法律責任 什麼是go cache KV存儲引擎有很多,常用的如redis,rocksd ...


作者:Moon-Light-Dream
出處:https://www.cnblogs.com/Moon-Light-Dream/
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則必究法律責任

什麼是go-cache

KV存儲引擎有很多,常用的如redis,rocksdb等,如果在實際使用中只是在記憶體中實現一個簡單的kv緩存,使用上述引擎就太大費周章了。在Golang中可以使用go-cache這個package實現一個輕量級基於記憶體的kv存儲或緩存。GitHub源碼地址是:https://github.com/patrickmn/go-cache
go-cache這個包實際上是在記憶體中實現了一個線程安全的map[string]interface{},可以將任何類型的對象作為value,不需要通過網路序列化或傳輸數據,適用於單機應用。對於每組KV數據可以設置不同的TTL(也可以永久存儲),並可以自動實現過期清理。
在使用時一般都是將go-cache作為數據緩存來使用,而不是持久性的數據存儲。對於停機後快速恢復的場景,go-cache支持將緩存數據保存到文件,恢復時從文件中load數據載入到記憶體。

如何使用go-cache

常用介面分析

對於資料庫的基本操作,無外乎關心的CRUD(增刪改查),對應到go-cache中的介面如下:

  • 創建對象:在使用前需要先創建cache對象
    1. func New(defaultExpiration, cleanupInterval time.Duration) *Cache:指定預設有效時間和清除間隔,創建cache對象。
      • 如果defaultExpiration<1或是NoExpiration,kv中的數據不會被清理,必須手動調用介面刪除。
      • 如果cleanupInterval<1,不會自動觸發清理邏輯,要手動觸發c.DeleteExpired()。
    2. func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache:與上面介面的不同是,入參增加了一個map,可以將已有數據按格式構造好,直接創建cache。
  • C(Create):增加一條數據,go-cache中有幾個介面都能實現新增的功能,但使用場景不同
    1. func (c Cache) Add(k string, x interface{}, d time.Duration) error:只有當key不存在或key對應的value已經過期時,可以增加成功;否則,會返回error。
    2. func (c Cache) Set(k string, x interface{}, d time.Duration):在cache中增加一條kv記錄。
      • 如果key不存在,增加一個kv記錄;如果key已經存在,用新的value覆蓋舊的value。
      • 對於有效時間d,如果是0(DefaultExpiration)使用預設有效時間;如果是-1(NoExpiration),表示沒有過期時間。
    3. func (c Cache) SetDefault(k string, x interface{}):與Set用法一樣,只是這裡的TTL使用預設有效時間。
  • R(Read):只支持按key進行讀取
    1. func (c Cache) Get(k string) (interface{}, bool) :通過key獲取value,如果cache中沒有key,返回的value為nil,同時返回一個bool類型的參數表示key是否存在。
    2. func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool):與Get介面的區別是,返回參數中增加了key有效期的信息,如果是不會過期的key,返回的是time.Time類型的零值。
  • U(Update):按key進行更新
    1. 直接使用Set介面,上面提到如果key已經存在會用新的value覆蓋舊的value,也可以達到更新的效果。
    2. func (c Cache) Replace(k string, x interface{}, d time.Duration) error:如果key存在且為過期,將對應value更新為新的值;否則返回error。
    3. func (c Cache) Decrement(k string, n int64) error:對於cache中value是int, int8, int16, int32, int64, uintptr, uint,uint8, uint32, or uint64, float32,float64這些類型記錄,可以使用該介面,將value值減n。如果key不存在或value不是上述類型,會返回error。
    4. DecrementXXX:對於Decrement介面中提到的各種類型,還有對應的介面來處理,同時這些介面可以得到value變化後的結果。如func (c *cache) DecrementInt8(k string, n int8) (int8, error),從返回值中可以獲取到value-n後的結果。
    5. func (c Cache) Increment(k string, n int64) error:使用方法與Decrement相同,將key對應的value加n。
    6. IncrementXXX:使用方法與DecrementXXX相同。
  • D(Delete)
    1. func (c Cache) Delete(k string):按照key刪除記錄,如果key不存在直接忽略,不會報錯。
    2. func (c Cache) DeleteExpired():在cache中刪除所有已經過期的記錄。cache在聲明的時候會指定自動清理的時間間隔,使用者也可以通過這個介面手動觸發。
    3. func (c Cache) Flush():將cache清空,刪除所有記錄。
  • 其他介面:
    1. func (c Cache) ItemCount() int:返回cache中的記錄數量。需要註意的是,返回的數值可能會比實際能獲取到的數值大,對於已經過期但還沒有即使清理的記錄也會被統計。
    2. func (c *cache) OnEvicted(f func(string, interface{})):設置一個回調函數(可選項),當一條記錄從cache中刪除(使用者主動delete或cache自助清理過期記錄)時,調用該函數。設置為nil關閉操作。

安裝go-cache包

介紹了go-cache的常用介面,接下來從代碼中看看如何使用。在coding前需要安裝go-cache,命令如下。

go get github.com/patrickmn/go-cache

一個Demo

如何在golang中使用上述介面實現kv資料庫的增刪改查,接下來看一個demo。其他更多介面的用法和更詳細的說明,可以參考GoDoc

import (
    "fmt"
    "time"
    
    "github.com/patrickmn/go-cache" // 使用前先import包
)

func main() {
    // 創建一個cache對象,預設ttl 5分鐘,每10分鐘對過期數據進行一次清理
    c := cache.New(5*time.Minute, 10*time.Minute)

    // Set一個KV,key是"foo",value是"bar"
    // TTL是預設值(上面創建對象的入參,也可以設置不同的值)5分鐘
    c.Set("foo", "bar", cache.DefaultExpiration)

    // Set了一個沒有TTL的KV,只有調用delete介面指定key時才會刪除
    c.Set("baz", 42, cache.NoExpiration)

    // 從cache中獲取key對應的value
    foo, found := c.Get("foo")
    if found {
        fmt.Println(foo)
    }

    // 如果想提高性能,存儲指針類型的值
    c.Set("foo", &MyStruct, cache.DefaultExpiration)
    if x, found := c.Get("foo"); found {
        foo := x.(*MyStruct)
            // ...
    }
}

源碼分析

1. 常量:內部定義的兩個常量`NoExpiration`和`DefaultExpiration`,可以作為上面介面中的入參,`NoExpiration`表示沒有設置有效時間,`DefaultExpiration`表示使用New()或NewFrom()創建cache對象時傳入的預設有效時間。
const (
    NoExpiration time.Duration = -1
    DefaultExpiration time.Duration = 0
)
2.  Item:cache中存儲的value類型,Object是真正的值,Expiration表示過期時間。可以使用Item的```Expired()```介面確定是否到期,實現方式是過比較當前時間和Item設置的到期時間來判斷是否過期。
type Item struct {
    Object     interface{}
    Expiration int64
}

func (item Item) Expired() bool {
    if item.Expiration == 0 {
        return false
    }
    return time.Now().UnixNano() > item.Expiration
}
3. cache:go-cache的核心數據結構,其中定義了每條記錄的預設過期時間,底層的存儲結構等信息。
type cache struct {
    defaultExpiration time.Duration              // 預設過期時間
    items             map[string]Item            // 底層存儲結構,使用map實現 
    mu                sync.RWMutex               // map本身非線程安全,操作時需要加鎖
    onEvicted         func(string, interface{})  // 回調函數,當記錄被刪除時觸發相應操作
    janitor           *janitor                   // 用於定時輪詢失效的key
}
4. janitor:用於定時輪詢失效的key,其中定義了輪詢的周期和一個無緩存的channel,用來接收結束信息。
type janitor struct {
    Interval time.Duration // 定時輪詢周期
    stop     chan bool     // 用來接收結束信息
}

func (j *janitor) Run(c *cache) {
    ticker := time.NewTicker(j.Interval) // 創建一個timeTicker定時觸發
    for {
        select {
        case <-ticker.C:
            c.DeleteExpired()            // 調用DeleteExpired介面處理刪除過期記錄
        case <-j.stop:
            ticker.Stop()
            return
        }
    }
}

對於janitor的處理,這裡使用的技巧值得學習 ,下麵這段代碼是在New() cache對象時,會同時開啟一個goroutine跑janitor,在run之後可以看到做了runtime.SetFinalizer的處理,這樣處理了可能存在的記憶體泄漏問題。

func stopJanitor(c *Cache) {
    c.janitor.stop <- true
}

func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
    c := newCache(de, m)
    // This trick ensures that the janitor goroutine (which--granted it
    // was enabled--is running DeleteExpired on c forever) does not keep
    // the returned C object from being garbage collected. When it is
    // garbage collected, the finalizer stops the janitor goroutine, after
    // which c can be collected.
    C := &Cache{c}
    if ci > 0 {
        runJanitor(c, ci)
        runtime.SetFinalizer(C, stopJanitor)
    }
    return C
}

可能的泄漏場景如下,使用者創建了一個cache對象,在使用後置為nil,在使用者看來在gc的時候會被回收,但是因為有goroutine在引用,在gc的時候不會被回收,因此導致了記憶體泄漏。

    c := cache.New()
    // do some operation
    c = nil

解決方案可以增加Close介面,在使用後調用Close介面,通過channel傳遞信息結束goroutine,但如果使用者在使用後忘了調用Close介面,還是會造成記憶體泄漏。
另外一種解決方法是使用runtime.SetFinalizer,不需要用戶顯式關閉, gc在檢查C這個對象沒有引用之後, gc會執行關聯的SetFinalizer函數,主動終止goroutine,並取消對象C與SetFinalizer函數的關聯關係。這樣下次gc時,對象C沒有任何引用,就可以被gc回收了。

總結

  1. go-cache的源碼代碼里很小,代碼結構和處理邏輯都比較簡單,可以作為golang新手閱讀的很好的素材。
  2. 對於單機輕量級的記憶體緩存如果僅從功能實現角度考慮,go-cache是一個不錯的選擇,使用簡單。
  3. 但在實際使用中需要註意:
    • go-cache沒有對記憶體使用大小或存儲數量進行限制,可能會造成記憶體峰值較高;
    • go-cache中存儲的value儘量使用指針類型,相比於存儲對象,不僅在性能上會提高,在記憶體占用上也會有優勢。由於golang的gc機制,map在擴容後原來占用的記憶體不會立刻釋放,因此如果value存儲的是對象會造成占用大量記憶體無法釋放。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 高級創意介面 1.cartier datapush介面文檔 ...
  • 一、FileOutputStream詳解 1.該類的構造方法,有第二個參數 FileOutputStream(String address,boolean append) append預設false,也就是新的寫入會覆蓋原來的東西。改為true的話,也就是以追加的形式寫入文件 package com ...
  • ***該隨筆僅用於學習*** 最近在學習java的時候,老師佈置了一道很有意思的題目:電腦給出隨機的石頭剪刀布,用戶可以輸入r,p,s輸出一項,直接給出這一局的輸贏。在老師原有代碼基礎上,增添了用e退出,計算這次游戲的獲勝者及分數。用隨機數和switch語句隨機電腦給出選項。 3 import ja ...
  • 背景 項目交叉編譯為可執行文件之後,在其他目錄執行文件時提示找不到配置文件 解決方案 直接採用以下代碼獲取到實際執行文件的路徑,然後拼接配置文件即可 代碼分析 os.Args是用來獲取命令行執行參數分片的,當使用 時 分片0會是一串複雜的路徑,原因是直接run go文件時會將文件轉移到臨時路徑下,然 ...
  • Dubbo admin管理控制台目前還沒有正式發佈,但是源碼已托管在github上,我們可以自行下載使用; 目前的管理控制台已經發佈0.1版本,結構上採取了前後端分離的方式,前端使用Vue和Vuetify分別作為Javascript框架和UI框架,後端採用Spring Boot框架。既可以按照標準的 ...
  • 1、Spring 1.x時代 在Spring 1.x時代,都是通過XML文件配置Bean。隨著項目的不斷擴大,需要將Bean的定義配置分放到不同的XML配置文件中。開發的時候需要頻繁的在java類和XML配置文件中切換。 2、Spring 2.x時代 隨著 JDK 1.5帶來的註解支持,Spring ...
  • 資源限制 時間限制:1.0s 記憶體限制:256.0MB 問題描述 利用字母可以組成一些美麗的圖形,下麵給出了一個例子: ABCDEFG BABCDEF CBABCDE DCBABCD EDCBABC 這是一個5行7列的圖形,請找出這個圖形的規律,並輸出一個n行m列的圖形。 輸入格式 輸入一行,包含兩 ...
  • [TOC] pandas對象有一個常用數學,統計學方法的集合。大部分屬於歸納或彙總統計。這些方法從DataFrame的行或列中抽取一個Series或一系列的值。 pandas的描述性統計的方法和NumPy的方法相比,內建了處理缺失值的功能,很好地針對於每一個我們需要處理的數據。 一:一些基本方法 1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...