微信公眾號:內核小王子
關注可了解更多關於數據庫,JVM內核相關的知識;
如果你有任何疑問也可以加我pigpdong[^1]
前言
在分布式系統中最好耗性能的地方就是最后端的數據庫,一般情況下數據庫上的insert操作很快,而update和delete操作如果帶有索引也不會慢,前提要控制好單表的數據量,並且不要建太多索引,
而最容易出現性能問題的往往是select語句,我們拋開join和group不說,大多數應用都是讀多寫少而且,而且帶有排序和limit等耗時操作,有些查詢還需要根據非索引字段進行過濾,以及like操作會加劇慢查詢,
在微服務中這些查詢接口往往以rpc的形式對外提供服務,因為網絡開銷導致整體響應時間增加,所以在某些性能要求較高的業務中引入緩存是非常必要的,下面我們將引入緩存的具體位置進行分類介紹.

客戶端緩存
移動客戶端可以將一些靜態資源緩存在設備上,避免重復從應用層獲取,在網絡不通暢的情況下也可以避免沒有數據前端UI錯亂,
而PC端瀏覽器一般可以通過nginx設置cache-control,expires,if-modified-since來控制緩存,避免重復請求服務器,也可以通過
cookie將一部分數據存在用戶瀏覽器中,下次請求可以將cookie發送給服務端,一般用cookie存儲用戶登錄信息
CDN緩存
一些靜態資源,尤其是圖片,我們可以在高並發的情況下,讓用戶優先訪問離用戶最近並且同一個網絡供應商的CDN節點,避免跨運營商垮地域訪問,
相比於集中式的機房內的服務器,CDN廠商的覆蓋范圍更廣,在每個運營商,每個地區都有自己的POP點,所以總可以找到更加靠近用戶網絡的CDN節點就近獲取靜態資源,CDN節點一般用來存儲
不頻繁變更的靜態圖片,頁面等資源,一般發布新版本,或者上新一個新活動都可以提前將這些靜態資源提前推送到CDN節點進行預熱,使用CDN一般通過CNAME的方式將域名解析交給CDN廠商的DNS服務器和全局負載均衡器

反向代理層緩存
反向代理層一般需要做動靜分離,將靜態資源存在在ngnix本地,靜態資源一般數據庫大請求頻繁,做動靜分離可以使應用層可以有更多資源處理動態請求,而靜態資源不用直接請求應用層,可以極大提高系統吞吐量
在做了動靜分離后,瀏覽器可以直接通過ajax請求服務端獲取動態數據,瀏覽器將數據進行整合后顯示給用戶.
分布式緩存


目前分布式緩存一般單獨部署在應用層進行讀寫控制,讀取的時候先去查詢緩存服務器,沒有命中在去查詢數據庫並寫入緩存,更新的時候先更新數據庫,然后在將緩存失效,
使用分布式緩存來替代應用層在JVM內緩存,可以避免各個JVM內緩存不一致的情況,也讓緩存可以集群化部署更容易水平擴展,
目前分布式緩存主要由memecache和redis,memecache主要提供key-value存儲,內存使用率較高,對大數據性能較好,但是集群支持不友好.
而redis提供多種數據結構,string,set,list,zset,hash等,還提供了RDB全量持久化,和AOF增量持久化,將內存中得數據化存儲在硬盤上,重啟可以再次加載使用,不過開啟持久化后會影響redis的內存使用率,尤其是開啟AOF同步后還會影響redis的寫性能,redis還提供了集群化master-slave數據備份以及多master進行分片來提高吞吐量.
不過memecache采用多線程模型,分為主線程和worker線程,而redis是單線程IO復用模型,對於IO操作可以將性能發揮的最大化,但是redis也提供了排序,聚合等操作,這些操作在單線程下會影響吞吐量.
memechache集群只能通過客戶端在讀寫的時候根據統一的分片算法選擇對應的機器,不支持master-slave數據備份

redis提供分片功能,將整個集群的16384個slot根據服務器的性能和讀寫頻率分道不同的master節點上,每個master可以下掛若干個slave節點,slave從master異步同步數據,當master掛了之后,slave可以通過選舉生成新德master,
master可以提供讀寫服務,而slave只提供讀服務,而redis集群對外提供服務也可以單獨加一層proxy也可以直接連接客戶端,兩種方式各有利弊,可根據實際場景進行選擇
像redis和memecache這種提供內存服務的應用,內存管理的效率直接影響系統的性能,在C語言中我們使用malloc和free分配和釋放內存,對於開發人員如果malloc和free不匹配容易造成內存泄露,頻繁使用也會造成大量的內存碎片,而且頻繁進行這種系統調用也會影響性能.
memecache會預先申請一塊內存,然后將這塊內存切分為若干個chunk,chunk的大小可以根據一個增長因子控制,比如增長因子為1.25,第一組chunk的大小為88字節,則第二組的大小為88*1.25=114字節,讓后將相同大小的chunk歸類為一個slab,當客戶端有一個寫請求后,會根據寫入數據的大小選擇對應的chunk,如果這個值占用空間小於chunk大小就會造成一定空間的浪費,刪除緩存的時候會標記這個chunk未使用.
而redis是現場申請內存的方式進行存儲數據,也很少對內存進行優化,所以redis一定程度上會產生內存碎片,並且當redis發生swap的時候也不會觸發內存整理.
當然redis並不是所有數據都存儲在內存中,當物理內存用完時或者達到某一個閾值,redis可以將一些很久沒有用到的value存儲到磁盤,只將key存在內存中,也就是所謂的swap操作,需要計算哪些key需要交換到磁盤,不過這種情況下當客戶端發起一個讀請求,value不在內存中得時候需要從硬盤讀取,默認情況下redis會阻塞.
目前經過benchmark測試,redis性能要優於memecache,原因可能是memecache用了libevent庫,而該庫為了迎合通用做了大量的代碼冗余,而redis使用libevent里面的兩個文件修改實現了epoll event loop,另一方面redis是單線程的,不用考慮精裝修改資源的情況,而memecache采用CAS的方式,CAS的實現需要為每一個cache key設置一個隱藏的token,
這個token會作為版本號,在set的時候會遞增,帶來CPU和內存的雙重開銷,盡管開銷很小,在QPS很高的情況下會帶來性能上的細微差別.
JVM本地緩存
本地緩存,這類緩存一般存儲在JVM堆空間內,由於容量受限制,也會影響到FullGC,當然也可以考慮使用堆外內存或者用jemalloc管理內存,
所以我們只是通過本地緩存來緩存一些並發訪問量特別高並且查詢數據庫很耗時的數據,而且這類數據可能不一定和數據庫完全保持一致,所以業務不會使用改變量做一些金額和狀態相關的核心操作.
這類緩存的典型代表為guava和ehcache,也有一些緩存放在ORM框架中,去緩存數據中的查詢操作.
數據庫緩存
數據庫本身也會有緩存功能,目前建議只針對一些讀多寫少特別頻繁的業務表開啟,大多數情況都建議關閉,因為mysql的緩存中當有任何一條記錄的update操作就會將緩存失效,如果頻繁update就會導致數據庫頻繁緩存和清除
使用說明
- 容量評估
在使用緩存前,最好做下容量評估,緩存系統主要消耗的是服務器內存,因此使用緩存時候必須對應用需要緩存的數據大小進行評估,包括緩存的數據結構,過期時間,緩存大小,緩存數量,然后根據未來一定時間內的業務增長情況進行預估.
- 業務分離
建議將使用的緩存進行分離,核心業務和非核心業務可以使用不同的緩存實例,最好能做到業務之間相互隔離,避免不同業務線共用一套緩存導致沖突.
- 監控
所有的緩存實例都需要有監控,內存使用情況,慢查詢,大對象,任何緩存key都設置過期時間,過期時間最好在原有設置上加減一個隨機值,避免一起失效導致雪崩。
- 先更新數據庫,后失效緩存
以下為先更新數據庫后清緩存的兩種情況,一種最后緩存清空后下一次讀請求就會恢復,另外一種發生的概率很小

以下為先清緩存后更新數據庫,會導致緩存中得數據一直是臟數據

其次,我們要考慮下如果數據庫是主從部署,從庫支持讀取,那么數據寫入主庫后,而應用讀取從庫還未更新的數據並寫入緩存導致緩存里的數據一直是臟數據,這種情況我們可以提供一種供參考的方案:通過canel訂閱mysql的binlog的方式去修改緩存可以避免該問題。
- 應用不要過渡依賴緩存
我們一般不會要求緩存服務器的更新和數據庫的更新在同一個事物內,所以肯定有概率緩存和數據庫不一致的情況,所以
數據的最終一致性最好不要依賴緩存,可以在應用層和或者數據庫CAS的方式增加校驗,另外應用也不應該嚴重依賴緩存,當緩存服務器掛掉之后至少要保證服務能夠在沒有高並發情況下繼續正常對外提供服務,
當然也不要過渡依賴緩存服務器的持久化功能,畢竟並不能完整復原歷史數據.
