本文目錄一覽:
什麼是Redis?
REmote DIctionary Server(Redis) 是一個由Salvatore Sanfilippo寫的key-value存儲系統
Redis是一個開源的使用ANSIC語言編寫、遵守BSD協議、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API
它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets)和有序集合(sorted sets)等類型
Redis 簡介
Redis是完全開源免費的,遵守BSD協議,是一個高性能的key-value數據庫
Redis與其他key – value緩存產品有以下三個特點:
①Redis支持數據的持久化,可以將內存中的數據保存在磁盤中,重啟的時候可以再次加載進行使用。
②Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
③Redis支持數據的備份,即master-slave模式的數據備份。
Redis 的特點
高性能:Redis 將所有數據集存儲在內存中,可以在入門級 Linux 機器中每秒寫(SET)11 萬次,讀(GET)8.1 萬次
持久化:當所有數據都存在於內存中時,可以根據自上次保存以來經過的時間和/或更新次數,使用靈活的策略將更改異步保存在磁盤上
數據結構:Redis 支持各種類型的數據結構,例如字符串、散列、集合、列表、帶有範圍查詢的有序集、位圖、超級日誌和帶有半徑查詢的地理空間索引
原子操作:處理不同數據類型的 Redis 操作是原子操作,因此可以安全地 SET 或 INCR 鍵,添加和刪除集合中的元素等
支持的語言:Redis 支持許多語言,如C、C++、Erlang、Go、Haskell、Java、JavaScript(Node.js)、Lua、Objective-C、Perl、PHP、Python、R、Ruby、Rust、Scala、Smalltalk等
主/從複製:Redis 遵循非常簡單快速的主/從複製。配置文件中只需要一行來設置它,而 Slave 在 Amazon EC2 實例上完成 10 MM
key 集的初始同步只需要 21 秒
分片:Redis 支持分片。與其他鍵值存儲一樣,跨多個 Redis 實例分發數據集非常容易
可移植:Redis 是用 C 編寫的,適用於大多數 POSIX 系統,如 Linux、BSD、Mac OS X、Solaris 等
golang sync.pool對象復用 並發原理 緩存池
在go http每一次go serve(l)都會構建Request數據結構。在大量數據請求或高並發的場景中,頻繁創建銷毀對象,會導致GC壓力。解決辦法之一就是使用對象復用技術。在http協議層之下,使用對象復用技術創建Request數據結構。在http協議層之上,可以使用對象復用技術創建(w,*r,ctx)數據結構。這樣即可以回快TCP層讀包之後的解析速度,也可也加快請求處理的速度。
先上一個測試:
結論是這樣的:
貌似使用池化,性能弱爆了???這似乎與net/http使用sync.pool池化Request來優化性能的選擇相違背。這同時也說明了一個問題,好的東西,如果濫用反而造成了性能成倍的下降。在看過pool原理之後,結合實例,將給出正確的使用方法,並給出預期的效果。
sync.Pool是一個 協程安全 的 臨時對象池 。數據結構如下:
local 成員的真實類型是一個 poolLocal 數組,localSize 是數組長度。這涉及到Pool實現,pool為每個P分配了一個對象,P數量設置為runtime.GOMAXPROCS(0)。在並發讀寫時,goroutine綁定的P有對象,先用自己的,沒有去偷其它P的。go語言將數據分散在了各個真正運行的P中,降低了鎖競爭,提高了並發能力。
不要習慣性地誤認為New是一個關鍵字,這裡的New是Pool的一個字段,也是一個閉包名稱。其API:
如果不指定New字段,對象池為空時會返回nil,而不是一個新構建的對象。Get()到的對象是隨機的。
原生sync.Pool的問題是,Pool中的對象會被GC清理掉,這使得sync.Pool只適合做簡單地對象池,不適合作連接池。
pool創建時不能指定大小,沒有數量限制。pool中對象會被GC清掉,只存在於兩次GC之間。實現是pool的init方法註冊了一個poolCleanup()函數,這個方法在GC之前執行,清空pool中的所有緩存對象。
為使多協程使用同一個POOL。最基本的想法就是每個協程,加鎖去操作共享的POOL,這顯然是低效的。而進一步改進,類似於ConcurrentHashMap(JDK7)的分Segment,提高其並發性可以一定程度性緩解。
注意到pool中的對象是無差異性的,加鎖或者分段加鎖都不是較好的做法。go的做法是為每一個綁定協程的P都分配一個子池。每個子池又分為私有池和共享列表。共享列表是分別存放在各個P之上的共享區域,而不是各個P共享的一塊內存。協程拿自己P里的子池對象不需要加鎖,拿共享列表中的就需要加鎖了。
Get對象過程:
Put過程:
如何解決Get最壞情況遍歷所有P才獲取得對象呢:
方法1止前sync.pool並沒有這樣的設置。方法2由於goroutine被分配到哪個P由調度器調度不可控,無法確保其平衡。
由於不可控的GC導致生命周期過短,且池大小不可控,因而不適合作連接池。僅適用於增加對象重用機率,減少GC負擔。2
執行結果:
單線程情況下,遍歷其它無元素的P,長時間加鎖性能低下。啟用協程改善。
結果:
測試場景在goroutines遠大於GOMAXPROCS情況下,與非池化性能差異巨大。
測試結果
可以看到同樣使用*sync.pool,較大池大小的命中率較高,性能遠高於空池。
結論:pool在一定的使用條件下提高並發性能,條件1是協程數遠大於GOMAXPROCS,條件2是池中對象遠大於GOMAXPROCS。歸結成一個原因就是使對象在各個P中均勻分布。
池pool和緩存cache的區別。池的意思是,池內對象是可以互換的,不關心具體值,甚至不需要區分是新建的還是從池中拿出的。緩存指的是KV映射,緩存里的值互不相同,清除機制更為複雜。緩存清除算法如LRU、LIRS緩存算法。
池空間回收的幾種方式。一些是GC前回收,一些是基於時鐘或弱引用回收。最終確定在GC時回收Pool內對象,即不迴避GC。用java的GC解釋弱引用。GC的四種引用:強引用、弱引用、軟引用、虛引用。虛引用即沒有引用,弱引用GC但有空間則保留,軟引用GC即清除。ThreadLocal的值為弱引用的例子。
regexp 包為了保證並發時使用同一個正則,而維護了一組狀態機。
fmt包做字串拼接,從sync.pool拿[]byte對象。避免頻繁構建再GC效率高很多。
一頓騷操作版本號比較性能提升300%
在一次性能分析中,發現線上服務 CompareVersion 佔用了較長的CPU時間。如下圖所示。
其中佔用時間最長的為 strings.Split 函數,這個函數對Gopher來說應該是非常熟悉的。而 CompareVersion 就是基於 strings.Split 函數來實現版本比較的,下面看一下 CompareVersion 的實現。
CompareVersion 的邏輯清晰且簡單,而根據火焰圖知性能主要消耗在 strings.Split 函數上,所以老許的第一目標是嘗試優化 strings.Split 函數。
每當此時老許首先想到的方法就是百度大法和谷歌大法,最後在某篇文章中發現 strings.FieldsFunc 函數,根據該文章描述, strings.FieldsFunc 函數分割字符串的速度遠快於 strings.Split 函數。那麼我們到底能不能使用 strings.FieldsFunc 函數替換 strings.Split 函數請看下面測試結果。
上述benchmark測試在老許的機器上某次運行結果如下:
根據輸出知, strings.FieldsFunc 函數沒有想象中那麼快,甚至還比不過 strings.Split 函數。既然此路不通,老許只好再另尋他法。
按照最卷的公司來,假如我們每周一個版本,且全年無休則一個公司要發布1000個版本需 19年 (1000/(365 / 7))。基於這個內卷的數據,我們如果能夠把這些版本都緩存起來,然後再比較大小,其執行速度絕對有一個質的提升。
要引入緩存的話,老許第一個想到的就是過期緩存。同時為了儘可能的輕量所以自己實現一個過期緩存無疑是一個不錯的方案。
1、定義一個包含過期時間和數據的結構體
2、使用 sync.Map 作為並發安全的緩存
3、定義一個通過 . 分割可存儲每部分數據的結構體
4、將app版本轉為切片以方便緩存
5、使用帶緩存的方式進行版本大小比較
CompareVersionWithCache1 函數比較步驟為:
最後進行性能驗證,以下為 CompareVersionWithCache1 函數和 CompareVersion 函數的benchmark對比。
通過上述結果分析知,使用緩存後唯一的優化只是減少了微乎其微的內存分配。這個結果實在令老許充滿了疑惑,在使用pprof分析後終於發現性能沒有提升的原因。以下為benchmark期間 BenchmarkCompareVersionWithCache1 函數的火焰圖。
因為考慮到app版本數量較小,所以使用了惰性淘汰的方式淘汰過期緩存,在每次讀取數據時判斷緩存是否過期。根據火焰圖知性能損耗最大的就是判斷緩存是否過期,每次判斷緩存是否過期都需要調用 time.Now().Unix() 得到當前時間戳。也就是因為 time.Now() 的這個調用導致這次優化功虧一簣。
考慮到版本數量本身不多,且對於常用的版本可以儘可能永久緩存,因此引入LRU緩存做進一步性能優化嘗試。
1、引入開源的LRU緩存,對應開源庫為: github.com/hashicorp/golang-lru
2、在 CompareVersionWithCache1 函數的基礎上將讀寫緩存替換為引入的LRU緩存
最後進行性能驗證,以下為 CompareVersionWithCache2 函數和 CompareVersion 函數的benchmark對比。
哎,這個結果終於有點樣子了,但優化效果並不明顯,還有進一步提升的空間。
選擇LRU緩存是有效果的,在這個基礎上老許決定自己實現一個極簡的LRU緩存。
1、定義一個緩存節點結構體
2、 定義一個操作LRU緩存的結構體
3、LRU緩存的Set方法
4、LRU緩存的Get方法
5、在 CompareVersionWithCache1 函數的基礎上將讀寫緩存替換為自實現的LRU緩存
最後進行性能驗證,以下為 CompareVersionWithCache3 函數和 CompareVersion 函數的benchmark對比:
引入自實現的LRU緩存後,性能足足提升了一倍,到這裡老許幾乎準備去公司裝逼了,但是心裡總有個聲音在問我有沒有無鎖的方式讀取緩存。
無鎖的方式確實沒有想到,只想到了兩種減少鎖競爭的方式。
加入隨機數後的實現如下:
加入隨機數後的 CompareVersionWithCache3 函數和 CompareVersion 函數的benchmark對比如下:
加入隨機數後, CompareVersionWithCache3 函數性能再次提升 20% 左右。優化還沒結束,當緩存數量遠不足設置的緩存上限時不需要移動到鏈表頭。
引入上述優化後,benchmark對比如下:
至此,最終版的版本比較實現在理想情況下(緩存空間較足)性能達到原先的 4 倍。
本來老許都準備去公司裝逼了,萬萬沒想到同事已經搞了一個更加合理且穩定的版本比較算法,讓老許自愧不如。
該算法思路如下:
三種算法benchmark如下:
BenchmarkCompareVersionNoSplit 函數不需要引入緩存,也不會像 BenchmarkCompareVersionWithCache3 中的緩存數量接近上限後會有一定的性能損失,幾乎是我目前發現的最為理想的版本比較方案。
老許也不說什麼當局者迷,旁觀者清這種酸葡萄一般的話,只得承認有的人就是老天爺賞飯吃。有一說一碰上這種人是我的幸運,我相信他只要有口飯吃,我就能在他屁股後面蹭口湯喝。關於文中最後提到的版本號比較算法完整實現請至下面的github倉庫查看:
最後,衷心希望本文能夠對各位讀者有一定的幫助。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/151315.html