1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        ecache輕量級(jí)本地內(nèi)存緩存

        聯(lián)合創(chuàng)作 · 2023-09-29 05:40

        ecache 是一款極簡設(shè)計(jì)、高性能、并發(fā)安全、支持分布式一致性的內(nèi)存緩存。

        特性

        • 代碼量<300行、30s完成接入
        • 高性能、極簡設(shè)計(jì)、并發(fā)安全
        • 支持LRU  LRU-2兩種模式
        • 額外小組件支持分布式一致性

        基準(zhǔn)性能

        如何使用

        下載包(預(yù)計(jì)5秒)

        非go modules模式:
        sh> go get -u github.com/orca-zhang/ecache

        go modules模式:
        sh> go mod tidy && go mod download

        引入包(預(yù)計(jì)5秒)

        import (
            "time"
        
            "github.com/orca-zhang/ecache"
        )

        定義實(shí)例(預(yù)計(jì)5秒)

        可以放置在任意位置(全局也可以),建議就近定義

        var c = ecache.NewLRUCache(16, 200, 10 * time.Second)

        設(shè)置緩存(預(yù)計(jì)5秒)

        c.Put("uid1", o) // o可以是任意變量,一般是對(duì)象指針,存放固定的信息,比如*UserInfo

        查詢緩存(預(yù)計(jì)5秒)

        if v, ok := c.Get("uid1"); ok {
            return v.(*UserInfo) // 不用類型斷言,咱們自己控制類型
        }
        // 如果內(nèi)存緩存沒有查詢到,下面再回源查redis/db

        刪除緩存(預(yù)計(jì)5秒)

        在信息發(fā)生變化的地方

        c.Del("uid1")

        運(yùn)行吧

        ?? 完美搞定 ?? 性能直接提升X倍!
        sh> go run <你的main.go文件>

        參數(shù)說明

        • NewLRUCache
          • 第一個(gè)參數(shù)是桶的個(gè)數(shù),用來分散鎖的粒度,每個(gè)桶都會(huì)使用獨(dú)立的鎖
            • 不用擔(dān)心,隨意設(shè)置一個(gè)就好,ecache會(huì)找一個(gè)等于或者略大于輸入大小的2的冪次的數(shù)字,后面便于掩碼計(jì)算
          • 第二個(gè)參數(shù)是每個(gè)桶所能容納的item個(gè)數(shù)上限
            • 意味著ecache全部寫滿的情況下,應(yīng)該有第一個(gè)參數(shù)??第二個(gè)參數(shù)個(gè)item
          • 第三個(gè)參數(shù)是每個(gè)item的過期時(shí)間
            • ecache使用內(nèi)部定時(shí)器提升性能,默認(rèn)100ms精度,每秒校準(zhǔn)

        最佳實(shí)踐

        • 復(fù)雜對(duì)象優(yōu)先存放指針(注意??一旦放進(jìn)去不要再修改其字段,即使再拿出來也是,item有可能被其他人同時(shí)訪問)
        • 也可以存放對(duì)象(相對(duì)于上一個(gè)性能差一些,因?yàn)槟贸鋈ビ锌截悾?/li>
        • 緩存的對(duì)象盡可能越往業(yè)務(wù)上層越大越好(節(jié)省內(nèi)存拼裝和組織時(shí)間)
        • 如果不想因?yàn)轭愃票闅v的請(qǐng)求把熱數(shù)據(jù)刷掉,可以改用LRU-2模式,雖然可能有很少的損耗(?? 什么是LRU-2
        • 一個(gè)實(shí)例可以存儲(chǔ)多種類型的對(duì)象,試試key格式化的時(shí)候加上前綴,用冒號(hào)分割
        • 并發(fā)訪問量大的場景,試試2561024個(gè)桶,甚至更多

        特別場景

        LRU-2模式

        直接在NewLRUCache()后面跟.LRU2(<num>)就好,參數(shù)<num>代表LRU-2熱隊(duì)列的item上限個(gè)數(shù)(每個(gè)桶)

        var c = ecache.NewLRUCache(16, 200, 10 * time.Second).LRU2(1024)

        空緩存哨兵(不存在的對(duì)象不用再回源)

        // 設(shè)置的時(shí)候直接給`nil`就好
        c.Put("uid1", nil)
        // 讀取的時(shí)候,也和正常差不多
        if v, ok := c.Get("uid1"); ok {
          if v == nil { // 注意??這里需要判斷是不是空緩存哨兵
            return nil  // 是空緩存哨兵,那就返回沒有信息或者也可以讓`uid1`不出現(xiàn)在待回源列表里
          }
          return v.(*UserInfo)
        }
        // 如果內(nèi)存緩存沒有查詢到,下面再回源查redis/db

        需要修改部分?jǐn)?shù)據(jù),且用對(duì)象指針方式存儲(chǔ)時(shí)

        比如,我們從ecache中獲取了*UserInfo類型的用戶信息緩存v,需要修改其狀態(tài)字段

        import (
            "github.com/jinzhu/copier"
        )
        o := &UserInfo{}
        copier.Copy(o, v) // 從v復(fù)制到o
        o.Status = 1      // 修改副本的數(shù)據(jù)

        統(tǒng)計(jì)緩存使用情況

        實(shí)現(xiàn)超級(jí)簡單,注入inspector后,每個(gè)操作只多了一次原子操作,具體看代碼

        引入stats包

        import (
            "github.com/orca-zhang/ecache/stats"
        )

        綁定緩存實(shí)例(名稱為自定義的池子名稱,內(nèi)部會(huì)按名稱聚合)

        var _ = stats.Bind("user", c)
        var _ = stats.Bind("user", c, c1, c2)
        var _ = stats.Bind("room", caches...)

        打印統(tǒng)計(jì)信息

        stats.Stats().Range(func(k, v interface{}) bool {
            fmt.Printf("stats: %s %+v\n", k, v)
            return true
        })

        分布式一致性組件

        引入dist包

        import (
            "github.com/orca-zhang/ecache/dist"
        )

        綁定緩存實(shí)例

        名稱為自定義的池子名稱,內(nèi)部會(huì)按名稱聚合
        注意??綁定可以放在全局,不依賴初始化

        var _ = dist.Bind("user", c)
        var _ = dist.Bind("user", c, c1, c2)
        var _ = dist.Bind("token", caches...)

        綁定redis client

        目前支持redigo和goredis,其他庫可以自行實(shí)現(xiàn)dist.RedisCli接口,或者提issue給我

        go-redis v7及以下版本

        import (
            "github.com/orca-zhang/ecache/dist/goredis/v7"
        )
        
        dist.Init(goredis.Take(redisCli)) // redisCli是*redis.RedisClient類型
        dist.Init(goredis.Take(redisCli, 100000)) // 第二個(gè)參數(shù)是channel緩沖區(qū)大小,不傳默認(rèn)100

        go-redis v8及以上版本

        import (
            "github.com/orca-zhang/ecache/dist/goredis"
        )
        
        dist.Init(goredis.Take(redisCli)) // redisCli是*redis.RedisClient類型
        dist.Init(goredis.Take(redisCli, 100000)) // 第二個(gè)參數(shù)是channel緩沖區(qū)大小,不傳默認(rèn)100

        redigo

        注意??github.com/gomodule/redigo 要求最低版本 go 1.14

        import (
            "github.com/orca-zhang/ecache/dist/redigo"
        )
        
        dist.Init(redigo.Take(pool)) // pool是*redis.Pool類型

        主動(dòng)通知所有節(jié)點(diǎn)、所有實(shí)例刪除(包括本機(jī))

        當(dāng)db的數(shù)據(jù)發(fā)生變化或者刪除時(shí)調(diào)用
        發(fā)生錯(cuò)誤時(shí)會(huì)降級(jí)成只處理本機(jī)所有實(shí)例(比如未初始化或者網(wǎng)絡(luò)錯(cuò)誤)

        dist.OnDel("user", "uid1")

        不希望你白來

        • 客官,既然來了,學(xué)點(diǎn)東西再走吧!
        • 我想盡力讓你明白ecache做了啥,以及為什么要這么做

        什么是本地內(nèi)存緩存


        L1 緩存引用 .................... 0.5 ns
        分支錯(cuò)誤預(yù)測(cè) ...................... 5 ns
        L2 緩存引用 ...................... 7 ns
        互斥鎖/解鎖 ...................... 25 ns
        主存儲(chǔ)器引用 .................... 100 ns
        使用 Zippy 壓縮 1K 字節(jié) ........3,000 ns =   3 μs
        通過 1 Gbps 網(wǎng)絡(luò)發(fā)送 2K 字節(jié)... 20,000 ns =  20 μs
        從內(nèi)存中順序讀取 1 MB ........ 250,000 ns = 250 μs
        同一數(shù)據(jù)中心內(nèi)的往返........... 500,000 ns = 0.5 ms
        發(fā)送數(shù)據(jù)包 加州<->荷蘭 .... 150,000,000 ns = 150 ms
        
        • 從上表可以看出,內(nèi)存訪問和網(wǎng)絡(luò)訪問(同數(shù)據(jù)中心)差不多是一千到一萬倍的差距!
        • 曾經(jīng)遇到不止一個(gè)工程師:“緩存?上redis”,但我想說,redis不是萬金油,某些程度上講,用它還是噩夢(mèng)(當(dāng)然我說的是緩存一致性問題...??)
        • 因?yàn)閮?nèi)存操作非??欤鄬?duì)于redis/db你基本可以忽略不計(jì),比如現(xiàn)在有一個(gè)查詢接口,我們把結(jié)果緩存1秒,也就是1秒內(nèi)不會(huì)請(qǐng)求redis/db,如果接口的QPS是1000,那回源次數(shù)降低到了1/1000(理想情況),意味著訪問redis/db部分的性能提升了1000倍,聽上去是不是很棒?
        • 繼續(xù)看,你會(huì)愛上她的?。ó?dāng)然也可能是他,亦或者是牠,ahaha)

        使用場景,解決什么問題

        • 高并發(fā)大流量場景
          • 緩存熱點(diǎn)數(shù)據(jù)(比如人氣比較高的直播間)
          • 突發(fā)QPS削峰(比如信息流中突發(fā)新聞)
        • 節(jié)省成本
          • 單機(jī)場景(不部署redis、memcache也能快速提升QPS上限)
          • redis和db實(shí)例降配(能攔截大部分請(qǐng)求)
        • 不怎么會(huì)變化的數(shù)據(jù)(寫少讀多)
          • 比如配置等(這類數(shù)據(jù)使用地方多,會(huì)有放大效應(yīng),很多時(shí)候可能會(huì)因?yàn)檫@些配置熱key對(duì)redis實(shí)例的規(guī)格誤判,需要單獨(dú)為它們升配)
        • 可以容忍短暫不一致的數(shù)據(jù)
          • 信息查詢(用戶頭像、昵稱、商品庫存(實(shí)際下單會(huì)在db再次檢查)等)
          • 配置延遲生效(過期時(shí)間10秒,那最多10秒生效)

        設(shè)計(jì)思路

        ecachelrucache庫的升級(jí)版本

        • 最下層是用原生map和存雙鏈表的node實(shí)現(xiàn)的最基礎(chǔ)LRU(最久未訪問)
          • PS:我實(shí)現(xiàn)的其他版本(go / C++ / js)在leetcode都是超越100%的解法
        • 第2層包了分桶策略、并發(fā)控制、過期控制(會(huì)自動(dòng)適配等于或者略大于輸入大小的2的冪次個(gè)桶,便于掩碼計(jì)算)
        • 第2.5層用很簡單的方式實(shí)現(xiàn)了LRU-2能力,代碼不超過20行,直接看源碼(搜關(guān)鍵詞LRU-2

        什么是LRU

        • 最久未訪問的優(yōu)先驅(qū)逐
        • 每次被訪問,item會(huì)被刷新到隊(duì)列的最前面
        • 隊(duì)列滿后再次寫入新item,優(yōu)先驅(qū)逐隊(duì)列最后面、也就是最久未訪問的item

        什么是LRU-2

        • LRU-K是少于K次訪問的用單獨(dú)的LRU隊(duì)列存放,超過K次的另外存放
        • 主要優(yōu)化的場景是比如一些遍歷類型的查詢,批量刷緩存以后,很容易把一些本來較熱的item給驅(qū)逐掉
        • 為了實(shí)現(xiàn)簡單,我們這里實(shí)現(xiàn)的是LRU-2,也就是第2次訪問就放到熱隊(duì)列里,并不記錄訪問次數(shù)
        • 主要優(yōu)化的是熱key的緩存命中率

        分布式一致性組件原理

        • 其實(shí)簡單的利用了redis的pubsub功能
        • 主動(dòng)告知被緩存的信息有更新,廣播到其他所有節(jié)點(diǎn)
        • 某種意義上說,它只是縮小不一致時(shí)間窗口的一個(gè)方式(有網(wǎng)絡(luò)延遲且不保證一定完成)
        • 需要注意??:
          • 盡量減少使用,適合用在寫少讀多WORM(Write-Once-Read-Many)的場景
            • redis性能畢竟不如內(nèi)存,而且有廣播類通信(寫放大)
          • 以下場景會(huì)降級(jí)(時(shí)間窗口變大),但至少會(huì)保證當(dāng)前節(jié)點(diǎn)的強(qiáng)一致性
            • redis不可用、網(wǎng)絡(luò)錯(cuò)誤
            • 消費(fèi)goroutine panic
            • 存在未生效節(jié)點(diǎn)(灰度canary發(fā)布,或者發(fā)布過程中)的情況下,比如
              • 已使用ecache但首次添加此插件
              • 新加入緩存的數(shù)據(jù)或者新加的刪除操作

        關(guān)于性能

        • 釋放鎖不用defer(單接口性能差20倍,看到有宣稱高性能還用defer的,直接pass吧)
        • 不用異步清理(沒意義,分散到寫時(shí)驅(qū)逐更合理,不易抖動(dòng))
        • 沒有用內(nèi)存容量來控制(單個(gè)item的大小一般都有預(yù)估大小,簡單控制個(gè)數(shù)即可)
        • 分桶策略,自動(dòng)選擇2的冪次個(gè)桶(分散鎖競爭,2的冪次掩碼操作更快)
        • key用string類型(可擴(kuò)展性強(qiáng);語言內(nèi)建支持引用,更省內(nèi)存)
        • 不用虛表頭(雖然繞腦一些,但是有20%左右提升)
        • 選擇LRU-2實(shí)現(xiàn)LRU-K(實(shí)現(xiàn)簡單,近乎沒有額外損耗)
        • 沒用整塊內(nèi)存(寫滿后復(fù)用以前的內(nèi)存效果也很好,整塊方式嘗試過提升不大、但可讀性大大降低)
        • 可以直接存指針(不用序列化,如果使用[]byte那優(yōu)勢(shì)大大降低)
        • 使用內(nèi)部定時(shí)器計(jì)時(shí)(默認(rèn)100ms精度,每秒校準(zhǔn),剖析發(fā)現(xiàn)time.Now()產(chǎn)生臨時(shí)對(duì)象導(dǎo)致GC耗時(shí)增加)

        失敗的優(yōu)化嘗試

        • key由string改為reflect.StringHeader,結(jié)果:負(fù)優(yōu)化
        • node預(yù)分配連續(xù)空間,通過游標(biāo)和freelist決定新申請(qǐng)(是否滿)還是復(fù)用,結(jié)果:不明顯
        • 互斥鎖改為讀寫鎖,Get請(qǐng)求也會(huì)修改數(shù)據(jù),訪問違例,即使不改數(shù)據(jù),結(jié)果:讀寫混合場景負(fù)優(yōu)化
        • time.Timer實(shí)現(xiàn)內(nèi)部定時(shí)器,結(jié)果:觸發(fā)不穩(wěn)定,后直接用Sleep實(shí)現(xiàn)定時(shí)器
        • 分布式一致性組件掛inspector自動(dòng)同步更新和刪除,結(jié)果:性能影響較大且需要特殊處理循環(huán)調(diào)用問題

        關(guān)于GC優(yōu)化

        • 就像我在C++版性能剖析器里提到的性能優(yōu)化的幾個(gè)層次,單從一個(gè)層次考慮性能并不高明
        • 《第三層次》里有一句“沒有比不存在的東西性能更快的了”(類似奧卡姆剃刀),能砍掉一定不要想著優(yōu)化
        • 比如為了減少GC大塊分配內(nèi)存,卻提供[]byte的值存儲(chǔ),意味著必須序列化、拷貝(雖不在庫的性能指標(biāo)里,人家用還是要算,包括:GC、內(nèi)存、CPU)
        • 如果序列化的部分可以復(fù)用用在協(xié)議層拼接,能做到ZeroCopy,那也無可厚非,而ecache存儲(chǔ)指針直接省了額外的部分
        • 我想表達(dá)的并不是GC優(yōu)化不重要,而更多應(yīng)該結(jié)合場景,使用者額外損耗也需要考慮,而非宣稱gc-free,結(jié)果用起來并非那樣
        • 我所崇尚的“暴力美學(xué)”是極簡,缺陷率和代碼量成正比,復(fù)雜的東西早晚會(huì)被淘汰,KISS才是王道
        • ecache一共只有不到300行,千行bug率一定的情況下,它的bug不會(huì)多

        常見問題

        問:一個(gè)實(shí)例可以存儲(chǔ)多種對(duì)象嗎?

        • 答:可以呀,比如加前綴格式化key就可以了(像用redis那樣冒號(hào)分割),注意??別搞錯(cuò)類型。

        問:如何給不同item設(shè)置不同過期時(shí)間?

        • 答:用多個(gè)緩存實(shí)例。(??沒想到吧)

        問:如果有熱熱熱熱key問題怎么解決?

        • 答:本身【本地內(nèi)存緩存】就是用來抗熱key的,這里可以理解成是非常非常熱的key(單節(jié)點(diǎn)幾十萬QPS),它們最大的問題是對(duì)單一bucket鎖定次數(shù)過多,影響在同一個(gè)bucket的其他數(shù)據(jù)。那么可以這樣:一是改用LRU-2不讓類似遍歷的請(qǐng)求把熱數(shù)據(jù)刷掉,二是除了增加bucket,可以用多實(shí)例(同時(shí)寫入相同的item)+讀隨機(jī)訪問某一個(gè)的方式,讓熱key有多個(gè)副本,不過刪除(反寫)的時(shí)候要注意多實(shí)例全部刪除,適用于“寫少讀多WORM(Write-Once-Read-Many)”的場景,或者“寫多讀多”的場景可以把有變化的diff部分單獨(dú)摘出來轉(zhuǎn)化為“寫少讀多WORM(Write-Once-Read-Many)”的場景。

        問:為什么不用虛表頭方式處理雙鏈表?太弱了吧!

        • 答:2019-04-22泄漏的【lrucache】被人在V站上扒出來噴過,還真不是不會(huì),現(xiàn)在的寫法,雖然比pointer-to-pointer方式讀起來繞腦,但是有20%左右的提升哈!(??沒想到吧)

        問:為什么不提供int類型的key的接口?

        • 答:考慮過,但是為了分布式一致性處理的簡單,只提供string的接口看著也不錯(cuò),用fmt.Sprint(i)也不麻煩。
        瀏覽 28
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        編輯 分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        編輯 分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            一区二区三区在线播放 | 日韩免费福利视频 | 国产精品三级片视频 | 亚洲视频精品一区 | 女上男下啪啪激烈xo动态图 | 久久三级片成年人 | 香蕉操逼逼| 国产精品久久久久久久久非色 | A级成人毛片 | 1024手机在线观看 |