<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

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

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

          特性

          • 代碼量<300行、30s完成接入
          • 高性能、極簡(jiǎn)設(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)存緩存沒(méi)有查詢到,下面再回源查redis/db

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

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

          c.Del("uid1")

          運(yùn)行吧

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

          參數(shù)說(shuō)明

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

          最佳實(shí)踐

          • 復(fù)雜對(duì)象優(yōu)先存放指針(注意??一旦放進(jìn)去不要再修改其字段,即使再拿出來(lái)也是,item有可能被其他人同時(shí)訪問(wèn))
          • 也可以存放對(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ā)訪問(wèn)量大的場(chǎng)景,試試256、1024個(gè)桶,甚至更多

          特別場(chǎng)景

          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  // 是空緩存哨兵,那就返回沒(méi)有信息或者也可以讓`uid1`不出現(xiàn)在待回源列表里
            }
            return v.(*UserInfo)
          }
          // 如果內(nèi)存緩存沒(méi)有查詢到,下面再回源查redis/db

          需要修改部分?jǐn)?shù)據(jù),且用對(duì)象指針?lè)绞酱鎯?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í)簡(jiǎn)單,注入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,其他庫(kù)可以自行實(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")

          不希望你白來(lái)

          • 客官,既然來(lái)了,學(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
          通過(guò) 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èn)和網(wǎng)絡(luò)訪問(wèn)(同數(shù)據(jù)中心)差不多是一千到一萬(wàn)倍的差距!
          • 曾經(jīng)遇到不止一個(gè)工程師:“緩存?上redis”,但我想說(shuō),redis不是萬(wàn)金油,某些程度上講,用它還是噩夢(mèng)(當(dāng)然我說(shuō)的是緩存一致性問(wèn)題...??)
          • 因?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(理想情況),意味著訪問(wèn)redis/db部分的性能提升了1000倍,聽(tīng)上去是不是很棒?
          • 繼續(xù)看,你會(huì)愛(ài)上她的?。ó?dāng)然也可能是他,亦或者是牠,ahaha)

          使用場(chǎng)景,解決什么問(wèn)題

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

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

          ecachelrucache庫(kù)的升級(jí)版本

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

          什么是LRU

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

          什么是LRU-2

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

          分布式一致性組件原理

          • 其實(shí)簡(jiǎn)單的利用了redis的pubsub功能
          • 主動(dòng)告知被緩存的信息有更新,廣播到其他所有節(jié)點(diǎn)
          • 某種意義上說(shuō),它只是縮小不一致時(shí)間窗口的一個(gè)方式(有網(wǎng)絡(luò)延遲且不保證一定完成)
          • 需要注意??:
            • 盡量減少使用,適合用在寫(xiě)少讀多WORM(Write-Once-Read-Many)的場(chǎng)景
              • redis性能畢竟不如內(nèi)存,而且有廣播類通信(寫(xiě)放大)
            • 以下場(chǎng)景會(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ā)布過(guò)程中)的情況下,比如
                • 已使用ecache但首次添加此插件
                • 新加入緩存的數(shù)據(jù)或者新加的刪除操作

          關(guān)于性能

          • 釋放鎖不用defer(單接口性能差20倍,看到有宣稱高性能還用defer的,直接pass吧)
          • 不用異步清理(沒(méi)意義,分散到寫(xiě)時(shí)驅(qū)逐更合理,不易抖動(dòng))
          • 沒(méi)有用內(nèi)存容量來(lái)控制(單個(gè)item的大小一般都有預(yù)估大小,簡(jiǎn)單控制個(gè)數(shù)即可)
          • 分桶策略,自動(dòng)選擇2的冪次個(gè)桶(分散鎖競(jìng)爭(zhēng),2的冪次掩碼操作更快)
          • key用string類型(可擴(kuò)展性強(qiáng);語(yǔ)言內(nèi)建支持引用,更省內(nèi)存)
          • 不用虛表頭(雖然繞腦一些,但是有20%左右提升)
          • 選擇LRU-2實(shí)現(xiàn)LRU-K(實(shí)現(xiàn)簡(jiǎn)單,近乎沒(méi)有額外損耗)
          • 沒(méi)用整塊內(nèi)存(寫(xiě)滿后復(fù)用以前的內(nèi)存效果也很好,整塊方式嘗試過(guò)提升不大、但可讀性大大降低)
          • 可以直接存指針(不用序列化,如果使用[]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ù)空間,通過(guò)游標(biāo)和freelist決定新申請(qǐng)(是否滿)還是復(fù)用,結(jié)果:不明顯
          • 互斥鎖改為讀寫(xiě)鎖,Get請(qǐng)求也會(huì)修改數(shù)據(jù),訪問(wèn)違例,即使不改數(shù)據(jù),結(jié)果:讀寫(xiě)混合場(chǎng)景負(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)用問(wèn)題

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

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

          常見(jiàn)問(wèn)題

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

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

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

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

          問(wèn):如果有熱熱熱熱key問(wèn)題怎么解決?

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

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

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

          問(wèn):為什么不提供int類型的key的接口?

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          編輯 分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费无码毛片一区二区本码视频 | 依依成人综合 | 天天操天天操天天 | 欧洲成人午夜精品无码区久久 | 大鸡巴AV |