5、分散式鎖
日常開發中我們可以用 synchronized 、Lock 實現併發程式設計。但是Java中的鎖只能保證在同一個JVM程序內中執行。如果在分散式叢集環境下用鎖呢?日常一般有兩種選擇方案。
5.1、 Zookeeper實現分散式鎖你需要知道一點基本zookeeper知識:
1、持久節點:客戶端斷開連線zk不刪除persistent型別節點 2、臨時節點:客戶端斷開連線zk刪除ephemeral型別節點 3、順序節點:節點後面會自動生成類似0000001的數字表示順序 4、節點變化的通知:客戶端註冊了監聽節點變化的時候,會呼叫回撥方法
大致流程如下,其中注意每個節點只監控它前面那個節點狀態,從而避免羊群效應。關於模板程式碼百度即可。
缺點:
5.2、 Redis實現分散式鎖本身原理也比較簡單,Redis 自身就是一個單執行緒處理器,具備互斥的特性,透過setNX,exist等命令就可以完成簡單的分散式鎖,處理好超時釋放鎖的邏輯即可。
SETNX
SETNX 是SET if Not eXists的簡寫,日常指令是SETNX key value,如果 key 不存在則set成功返回 1,如果這個key已經存在了返回0。
SETEX
SETEX key seconds value 表達的意思是 將值 value 關聯到 key ,並將 key 的生存時間設為多少秒。如果 key 已經存在,setex命令將覆寫舊值。並且 setex是一個原子性(atomic)操作。
加鎖:
一般就是用一個標識唯一性的字串比如UUID 配合 SETNX 實現加鎖。
解鎖:
這裡用到了LUA指令碼,LUA可以保證是原子性的,思路就是判斷一下Key和入參是否相等,是的話就刪除,返回成功1,0就是失敗。
缺點:
這個鎖是無法重入的,且自己實心的話各種邊邊角角都要考慮到,所以瞭解個大致思路流程即可,工程化還是用開源工具包就行。
5.3、 Redisson實現分散式鎖Redisson 是在Redis基礎上的一個服務,採用了基於NIO的Netty框架,不僅能作為Redis底層驅動客戶端,還能將原生的RedisHash,List,Set,String,Geo,HyperLogLog等資料結構封裝為Java裡大家最熟悉的對映(Map),列表(List),集(Set),通用物件桶(Object Bucket),地理空間物件桶(Geospatial Bucket),基數估計算法(HyperLogLog)等結構。
這裡我們只是用到了關於分散式鎖的幾個指令,他的大致底層原理:
Redisson加鎖解鎖 大致流程圖如下:
6、Redis 過期策略和記憶體淘汰策略6.1、Redis的過期策略Redis中 過期策略 通常有以下三種:
1、定時過期:
每個設定過期時間的key都需要建立一個定時器,到過期時間就會立即對key進行清除。該策略可以立即清除過期的資料,對記憶體很友好;但是會佔用大量的CPU資源去處理過期的資料,從而影響快取的響應時間和吞吐量。
2、惰性過期:
只有當訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節省CPU資源,卻對記憶體非常不友好。極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,佔用大量記憶體。
3、定期過期:
每隔一定的時間,會掃描一定數量的資料庫的expires字典中一定數量的key,並清除其中已過期的key。該策略是前兩者的一個折中方案。透過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和記憶體資源達到最優的平衡效果。
expires字典會儲存所有設定了過期時間的key的過期時間資料,其中 key 是指向鍵空間中的某個鍵的指標,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis叢集中儲存的所有鍵。
6.2、6種記憶體淘汰策略Redis的記憶體淘汰策略是指在Redis的用於快取的記憶體不足時,怎麼處理需要新寫入且需要申請額外空間的資料。
1、volatile-lru:從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰
2、volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰
3、volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰
4、allkeys-lru:從資料集(server.db[i].dict)中挑選最近最少使用的資料淘汰
5、allkeys-random:從資料集(server.db[i].dict)中任意選擇數據淘汰 6、no-enviction(驅逐):禁止驅逐資料,不刪除的意思。
面試常問常考的也就是LRU了,大家熟悉的LinkedHashMap中也實現了LRU演算法的,實現如下:
class SelfLRUCache<K, V> extends LinkedHashMap<K, V> { private final int CACHE_SIZE; /** * 傳遞進來最多能快取多少資料 * @param cacheSize 快取大小 */ public SelfLRUCache(int cacheSize) { // true 表示讓 linkedHashMap 按照訪問順序來進行排序,最近訪問的放在頭部,最老訪問的放在尾部。 super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); CACHE_SIZE = cacheSize; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { // 當 map中的資料量大於指定的快取個數的時候,就自動刪除最老的資料。 return size() > CACHE_SIZE; }}
6.2、總結Redis的記憶體淘汰策略的選取並不會影響過期的key的處理。記憶體淘汰策略用於處理記憶體不足時的需要申請額外空間的資料,過期策略用於處理過期的快取資料。
7、Redis 叢集高可用單機問題有機器故障、容量瓶頸、QPS瓶頸。在實際應用中,Redis的多機部署時候會涉及到redis主從複製、Sentinel哨兵模式、Redis Cluster。
模式優點缺點單機版架構簡單,部署方便機器故障、容量瓶頸、QPS瓶頸主從複製高可靠性,讀寫分離故障恢復複雜,主庫的寫跟存受單機限制Sentinel 哨兵叢集部署簡單,HA原理繁瑣,slave存在資源浪費,不能解決讀寫分離問題Redis Cluster資料動態儲存solt,可擴充套件,高可用客戶端動態感知後端變更,批次操作支援查
7.1、redis主從複製該模式下 具有高可用性且讀寫分離, 會採用 增量同步 跟 全量同步 兩種機制。
7.1.1、全量同步Redis全量複製一般發生在Slave初始化階段,這時Slave需要將Master上的所有資料都複製一份:
1、slave連線master,傳送psync命令。
2、master接收到psync命名後,開始執行bgsave命令生成RDB檔案並使用緩衝區記錄此後執行的所有寫命令。
3、master傳送快照檔案到slave,並在傳送期間繼續記錄被執行的寫命令。4、slave收到快照檔案後丟棄所有舊資料,載入收到的快照。
5、master快照發送完畢後開始向slave傳送緩衝區中的寫命令。
6、slave完成對快照的載入,開始接收命令請求,並執行來自master緩衝區的寫命令。
7.1.2、增量同步也叫指令同步,就是從庫重放在主庫中進行的指令。Redis會把指令存放在一個環形佇列當中,因為記憶體容量有限,如果備機一直起不來,不可能把所有的記憶體都去存指令,也就是說,如果備機一直未同步,指令可能會被覆蓋掉。
Redis增量複製是指Slave初始化後開始正常工作時master發生的寫操作同步到slave的過程。增量複製的過程主要是master每執行一個寫命令就會向slave傳送相同的寫命令。
7.1.3、Redis主從同步策略:1、主從剛剛連線的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。2、slave在同步master資料時候如果slave丟失連線不用怕,slave在重新連線之後丟失重補。
3、一般透過主從來實現讀寫分離,但是如果master掛掉後如何保證Redis的 HA呢?引入Sentinel進行master的選擇。
7.2、高可用之哨兵模式Redis-sentinel 本身是一個獨立執行的程序,一般sentinel叢集 節點數至少三個且奇數個,它能監控多個master-slave叢集,sentinel節點發現master宕機後能進行自動切換。Sentinel可以監視任意多個主伺服器以及主伺服器屬下的從伺服器,並在被監視的主伺服器下線時,自動執行故障轉移操作。這裡需注意sentinel也有single-point-of-failure問題。大致羅列下哨兵用途:
叢集監控:迴圈監控master跟slave節點。
訊息通知:當它發現有redis例項有故障的話,就會發送訊息給管理員
故障轉移:這裡分為主觀下線(單獨一個哨兵發現master故障了)。客觀下線(多個哨兵進行抉擇發現達到quorum數時候開始進行切換)。
配置中心:如果發生了故障轉移,它會通知將master的新地址寫在配置中心告訴客戶端。
7.3、Redis ClusterRedisCluster是Redis的分散式解決方案,在3.0版本後推出的方案,有效地解決了Redis分散式的需求。
7.3.1、分割槽規則常見的分割槽規則
節點取餘:hash(key) % N
一致性雜湊:一致性雜湊環
虛擬槽雜湊:CRC16[key] & 16383
RedisCluster採用了虛擬槽分割槽方式,具題的實現細節如下:
1、採用去中心化的思想,它使用虛擬槽solt分割槽覆蓋到所有節點上,取資料一樣的流程,節點之間使用輕量協議通訊Gossip來減少頻寬佔用所以效能很高,
2、自動實現負載均衡與高可用,自動實現failover並且支援動態擴充套件,官方已經玩到可以1000個節點 實現的複雜度低。
3、每個Master也需要配置主從,並且內部也是採用哨兵模式,如果有半數節點發現某個異常節點會共同決定更改異常節點的狀態。
4、如果叢集中的master沒有slave節點,則master掛掉後整個叢集就會進入fail狀態,因為叢集的slot對映不完整。如果叢集超過半數以上的master掛掉,叢集都會進入fail狀態。
5、官方推薦 叢集部署至少要3臺以上的master節點。
8、Redis 限流經常乘坐北京西二旗地鐵或者在北京西站乘坐的時候經常會遇到一種情況就是如果人很多,地鐵的工作人員拿個小牌前面一檔讓你等會兒再檢票,這就是實際生活應對人流量巨大的措施。
在開發高併發系統時,有三把利器用來保護系統:快取、降級和限流。那麼何為限流呢?顧名思義,限流就是限制流量,就像你寬頻包了1個G的流量,用完了就沒了。透過限流,我們可以很好地控制系統的qps,從而達到保護系統的目的。
1、基於Redis的setnx、zset1.2、setnx比如我們需要在10秒內限定20個請求,那麼我們在setnx的時候可以設定過期時間10,當請求的setnx數量達到20時候即達到了限流效果。
缺點:比如當統計1-10秒的時候,無法統計2-11秒之內,如果需要統計N秒內的M個請求,那麼我們的Redis中需要保持N個key等等問題。
1.3、zset其實限流涉及的最主要的就是滑動視窗,上面也提到1-10怎麼變成2-11。其實也就是起始值和末端值都各+1即可。我們可以將請求打造成一個zset陣列,當每一次請求進來的時候,value保持唯一,可以用UUID生成,而score可以用當前時間戳表示,因為score我們可以用來計算當前時間戳之內有多少的請求數量。而zset資料結構也提供了range方法讓我們可以很輕易的獲取到2個時間戳內有多少請求,
缺點:就是zset的資料結構會越來越大。
2、漏桶演算法漏桶演算法思路:把水比作是請求,漏桶比作是系統處理能力極限,水先進入到漏桶裡,漏桶裡的水按一定速率流出,當流出的速率小於流入的速率時,由於漏桶容量有限,後續進入的水直接溢位(拒絕請求),以此實現限流。
3、令牌桶演算法令牌桶演算法的原理:可以理解成醫院的掛號看病,只有拿到號以後才可以進行診病。
細節流程大致:
1、所有的請求在處理之前都需要拿到一個可用的令牌才會被處理。
2、根據限流大小,設定按照一定的速率往桶裡新增令牌。
3、設定桶最大可容納值,當桶滿時新新增的令牌就被丟棄或者拒絕。
工程化:
1、自定義註解、aop、Redis + Lua 實現限流。
2、推薦 guava 的RateLimiter實現。
9、常見知識點字串模糊查詢時用Keys可能導致執行緒阻塞,儘量用scan指令進行無阻塞的取出資料然後去重下即可。多個操作的情況下記得用pipeLine把所有的命令一次發過去,避免頻繁的傳送、接收帶來的網路開銷,提升效能。bigkeys可以掃描redis中的大key,底層是使用scan命令去遍歷所有的鍵,對每個鍵根據其型別執行STRLEN、LLEN、SCARD、HLEN、ZCARD這些命令獲取其長度或者元素個數。缺陷是線上試用並且個數多不一定空間大,線上應用記得開啟Redis慢查詢日誌哦,基本思路跟MySQL類似。Redis中因為記憶體分配策略跟增刪資料是會導致記憶體碎片,你可以重啟服務也可以執行activedefrag yes進行記憶體重新整理來解決此問題。1、Ratio >1 表明有記憶體碎片,越大表明越多嚴重。
2、Ratio < 1 表明正在使用虛擬記憶體,虛擬記憶體其實就是硬碟,效能比記憶體低得多,這是應該增強機器的記憶體以提高效能。
3、一般來說,mem_fragmentation_ratio的數值在1 ~ 1.5之間是比較健康的。