文章目錄
- Redis高級命令
- Redis持久化
- RDB快照(snapshot)
- **AOF(append-only file)**
- **Redis 4.0 混合持久化**
- 管道(Pipeline)
- **StringRedisTemplate與RedisTemplate詳解**
- Redis集群方案
- gossip
- 腦裂
- Redis Lua
- Redis MultiLock
- Redis紅鎖
- 緩存相關問題
- **緩存穿透**
- **緩存失效(擊穿)**
- **緩存雪崩**
- **熱點緩存key重建優化**
- **緩存與數據庫雙寫不一致**
- BigKey問題
- 主動清理策略
- Redis隊列與Stream
- Stream
- Redis隊列幾種實現
- **基于List的 LPUSH+BRPOP 的實現**
- **基于Sorted-Set的實現**
- Redis中的線程和IO模型
- 什么是Reactor模式 ?
- **單線程Reactor模式流程**
- **單線程Reactor,工作者線程池**
- **多Reactor線程模式**
- Redis中的線程和IO概述
- 事件的類型(套接字)
- HyperLogLog
- Redis事務
- **Redis事務**
- **Pipeline和事務的區別**
- Redis 復制緩存區相關問題分析
- 多從庫時主庫內存占用過多
- **OutputBuffer 拷貝和釋放的堵塞問題**
- **ReplicationBacklog 的限制**
- Redis7.0共享復制緩存區的設計與實現
- **簡述**
- **堵塞問題和限制問題的解決**
- 數據結構選取
- **rax樹**
- **Trie樹**
- 熱Key
- **什么是熱key**
Redis的 IO多路復用:redis利用epoll來實現IO多路復用,將連接信息和事件放到隊列中,依次放到文件事件分派器,事件分派器將事件分發給事件處理器。
Redis高級命令
scan:漸進式遍歷鍵
SCAN cursor [MATCH pattern] [COUNT count]
scan 參數提供了三個參數,第一個是 cursor 整數值(hash桶的索引值),第二個是 key 的正則模式,第三個是一次遍歷的key的數量(參考值,底層遍歷的數量不一定),并不是符合條件的結果數量。第一次遍歷時,cursor 值為 0,然后將返回結果中第一個整數值作為下一次遍歷的 cursor。一直遍歷到返回的 cursor 值為 0 時結束。
注意:但是scan并非完美無瑕, 如果在scan的過程中如果有鍵的變化(增加、 刪除、 修改) ,那么遍歷效果可能會碰到如下問題: 新增的鍵可能沒有遍歷到, 遍歷出了重復的鍵等情況, 也就是說scan并不能保證完整的遍歷出來所有的鍵, 這些是我們在開發時需要考慮的。
Redis持久化
RDB快照(snapshot)
RDB持久化是把當前進程數據生成快照保存到硬盤的過程。RDB 就是 Redis DataBase 的縮寫。
bgsave的寫時復制(COW)機制
Redis 借助操作系統提供的寫時復制技術(Copy-On-Write, COW),在生成快照的同時,依然可以正常處理寫命令。簡單來說,bgsave 子進程是由主線程 fork 生成的,可以共享主線程的所有內存數據。bgsave 子進程運行后,開始讀取主線程的內存數據,并把它們寫入 RDB 文件。此時,如果主線程對這些數據也都是讀操作,那么,主線程和 bgsave子進程相互不影響。但是,如果主線程要修改一塊數據,那么,這塊數據就會被復制一份,生成該數據的副本。然后,bgsave 子進程會把這個副本數據寫入 RDB 文件,而在這個過程中,主線程仍然可以直接修改原來的數據。
配置自動生成rdb文件后臺使用的是bgsave方式。
AOF(append-only file)
快照功能并不是非常耐久(durable): 如果 Redis 因為某些原因而造成故障停機, 那么服務器將丟失最近寫入、且仍未保存到快照中的那些數據。從 1.1 版本開始, Redis 增加了一種完全耐久的持久化方式: AOF 持久化,將修改的每一條指令記錄進文件appendonly.aof中(先寫入os cache,每隔一段時間fsync到磁盤)
AOF會定期根據內存的最新數據生成aof文件
注意,AOF重寫redis會fork出一個子進程去做(與bgsave命令類似),不會對redis正常命令處理有太多影響
Redis 4.0 混合持久化
重啟 Redis 時,我們很少使用 RDB來恢復內存狀態,因為會丟失大量數據。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 RDB來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。 Redis 4.0 為解決這個問題,帶來了一個新的持久化選項——混合持久化。
如果開啟了混合持久化,AOF在重寫時,不再是單純將內存數據轉換為RESP命令寫入AOF文件,而是將重寫這一刻之前的內存做RDB快照處理,并且將RDB快照內容和增量的AOF修改內存數據的命令存在一起,都寫入新的AOF文件,新的文件一開始不叫appendonly.aof,等到重寫完新的AOF文件才會進行改名,覆蓋原有的AOF文件,完成新舊兩個AOF文件的替換。
于是在 Redis 重啟的時候,可以先加載 RDB 的內容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重啟效率大幅得到提升。
復制風暴是指大量從節點對同一主節點或者對同一臺機器的多個主節點短時間內發起全量復制的過程。復制風暴對發起復制的主節點或者機器造成大量開銷,導致 CPU、內存、帶寬消耗。因此我們應該分析出復制風暴發生的場景,提前采用合理的方式規避。規避方式有如下幾個。
管道(Pipeline)
客戶端可以一次性發送多個請求而不用等待服務器的響應,待所有命令都發送完后再一次性讀取服務的響應,這樣可以極大的降低多條命令執行的網絡傳輸開銷,管道執行多條命令的網絡開銷實際上只相當于一次命令執行的網絡開銷。需要注意到是用pipeline方式打包命令發送,redis必須在處理完所有命令前先緩存起所有命令的處理結果。打包的命令越多,緩存消耗內存也越多。所以并不是打包的命令越多越好。
StringRedisTemplate與RedisTemplate詳解
spring 封裝了 RedisTemplate 對象來進行對redis的各種操作,它支持所有的 redis 原生的 api。在RedisTemplate中提供了幾個常用的接口方法的使用
StringRedisTemplate繼承自RedisTemplate,也一樣擁有這些操作。
StringRedisTemplate默認采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。
RedisTemplate默認采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
Redis集群方案
- 哨兵模式
在redis3.0以前的版本要實現集群一般是借助哨兵sentinel工具來監控master節點的狀態,如果master節點異常,則會做主從切換,將某一臺slave作為master,哨兵的配置略微復雜,并且性能和高可用性等各方面表現一般,特別是在主從切換的瞬間存在訪問瞬斷的情況,而且哨兵模式只有一個主節點對外提供服務,沒法支持很高的并發,且單個主節點內存也不宜設置得過大,否則會導致持久化文件過大,影響數據恢復或主從同步的效率
- 高可用集群模式
redis集群是一個由多個主從節點群組成的分布式服務器群,它具有復制、高可用和分片特性。Redis集群不需要sentinel哨兵·也能完成節點移除和故障轉移的功能。需要將每個節點設置成集群模式,這種集群模式沒有中心節點,可水平擴展,據官方文檔稱可以線性擴展到上萬個節點(官方推薦不超過1000個節點)。redis集群的性能和高可用性均優于之前版本的哨兵模式,且集群配置非常簡單
槽位定位算法
Redis Cluster 將所有數據劃分為 16384 個 slots(槽位),每個節點負責其中一部分槽位。槽位的信息存儲于每個節點中。
Cluster 默認會對 key 值使用 crc16 算法進行 hash 得到一個整數值,然后用這個整數值對 16384 進行取模來得到具體槽位。
跳轉重定位
當客戶端向一個錯誤的節點發出了指令,該節點會發現指令的 key 所在的槽位并不歸自己管理,這時它會向客戶端發送一個特殊的跳轉指令攜帶目標操作的節點地址,告訴客戶端去連這個節點去獲取數據。客戶端收到指令后除了跳轉到正確的節點上去操作,還會同步更新糾正本地的槽位映射表緩存,后續所有 key 將使用新的槽位映射表。
gossip
gossip協議包含多種消息,包括ping,pong,meet,fail等等。
meet:某個節點發送meet給新加入的節點,讓新節點加入集群中,然后新節點就會開始與其他節點進行通信;
ping:每個節點都會頻繁給其他節點發送ping,其中包含自己的狀態還有自己維護的集群元數據,互相通過ping交換元數據(類似自己感知到的集群節點增加和移除,hash slot信息等);
pong: 對ping和meet消息的返回,包含自己的狀態和其他信息,也可以用于信息廣播和更新;
fail: 某個節點判斷另一個節點fail之后,就發送fail給其他節點,通知其他節點,指定的節點宕機了。
gossip協議的優點在于元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續,打到所有節點上去更新,有一定的延時,降低了壓力;缺點在于元數據更新有延時可能導致集群的一些操作會有一些滯后。
腦裂
redis集群沒有過半機制會有腦裂問題,網絡分區導致腦裂后多個主節點對外提供寫服務,一旦網絡分區恢復,會將其中一個主節點變為從節點,這時會有大量數據丟失。
腦裂:一般來說是指一個分布式系統中有兩個子集,然后每個子集都有一個自己的主節點(Leader/Master)。那么整個分布式系統就會存在多個主節點了,而且每個都認為自己是正常的,這就會導致數據不一致或重復寫入的問題。
Redis Lua
EVAL命令對Lua腳本進行求值。EVAL命令的格式如下:
EVAL script numkeys key [key ...] arg [arg ...]
script參數是一段Lua腳本程序,它會被運行在Redis服務器上下文中,這段腳本不必(也不應該)定義為一個Lua函數。numkeys參數用于指定鍵名參數的個數。鍵名參數key [key …]從EVAL的第三個參數開始算起,表示在腳本中所用到的那些Redis鍵(key),這些鍵名參數可以在Lua中通過全局變量KEYS數組,用1為基址的形式訪問( KEYS[1], KEYS[2],以此類推)。
在命令的最后,那些不是鍵名參數的附加參數arg [arg …],可以在Lua中通過全局變量ARGV數組訪問,訪問的形式和KEYS變量類似(ARGV[1]、ARGV[2],諸如此類)。例如
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]]" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
在Lua腳本中,可以使用redis.call()函數來執行Redis命令
Redis MultiLock
基于 Redis 的 Redisson 分布式聯鎖 RedissonMultiLock 對象可以將多個 RLock 對象關聯為一個聯鎖,每個 RLock 對象實例可以來自于不同的 Redisson 實例。
它會嘗試同時獲取這些鎖,只有當所有鎖都成功獲取時,才算加鎖成功。
Redis紅鎖
redis分布式鎖在集群中會出現一些問題
假設線程1在主節點加鎖成功,主節點在同步數據到從節點的過程中宕機,重新選舉從節點為主節點,這個時候新的主節點是不存在線程1的鎖的,這個時候線程2過來加鎖成功執行邏輯完成,再來一個線程過來加鎖成功,而線程1并發問題還沒執行完成,這樣的話就又會出現“超賣”的問題,這樣的問題我們稱為redis主從架構鎖失效問題。
根據CAP理論,redis集群著重滿足AP,zk集群著重滿足CP
紅鎖主要是為減少redis主從架構鎖失效問題。就是對集群的每個節點進行加鎖,如果大多數(N/2+1)加鎖成功了,則認為獲取鎖成功。
Redisson RedLock 是基于聯鎖 MultiLock 實現的,但是使用過程中需要自己判斷 key 落在哪個節點上,對使用者不是很友好。
Redisson RedLock 已經被棄用,直接使用普通的加鎖即可,會基于 wait 機制將鎖同步到從節點,但是也并不能保證一致性。僅僅是最大限度的保證一致性。
緩存相關問題
緩存穿透
緩存穿透是指查詢一個根本不存在的數據, 緩存層和存儲層都不會命中, 通常出于容錯的考慮, 如果從存儲層查不到數據則不寫入緩存層。
緩存穿透將導致不存在的數據每次請求都要到存儲層去查詢, 失去了緩存保護后端存儲的意義。
造成緩存穿透的基本原因有兩個:
第一, 自身業務代碼或者數據出現問題。
第二, 一些惡意攻擊、 爬蟲等造成大量空命中。
解決方案:
1、緩存空對象
2、布隆過濾器
布隆過濾器就是一個大型的位數組和幾個不一樣的無偏 hash 函數。所謂無偏就是能夠把元素的 hash 值算得比較均勻。
當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。
緩存失效(擊穿)
由于大批量緩存在同一時間失效可能導致大量請求同時穿透緩存直達數據庫,可能會造成數據庫瞬間壓力過大甚至掛掉,對于這種情況我們在批量增加緩存時最好將這一批數據的緩存過期時間設置為一個時間段內的不同時間。
緩存雪崩
緩存雪崩指的是緩存層支撐不住或宕掉后, 流量會像奔逃的野牛一樣, 打向后端存儲層。
由于緩存層承載著大量請求, 有效地保護了存儲層, 但是如果緩存層由于某些原因不能提供服務(比如超大并發過來,緩存層支撐不住,或者由于緩存設計不好,類似大量請求訪問bigkey,導致緩存能支撐的并發急劇下降), 于是大量請求都會打到存儲層, 存儲層的調用量會暴增, 造成存儲層也會級聯宕機的情況。
預防和解決緩存雪崩問題, 可以從以下三個方面進行著手。
1) 保證緩存層服務高可用性,比如使用Redis Sentinel或Redis Cluster。
2) 依賴隔離組件為后端限流熔斷并降級。比如使用Sentinel或Hystrix限流降級組件。
比如服務降級,我們可以針對不同的數據采取不同的處理方式。當業務應用訪問的是非核心數據(例如電商商品屬性,用戶信息等)時,暫時停止從緩存中查詢這些數據,而是直接返回預定義的默認降級信息、空值或是錯誤提示信息;當業務應用訪問的是核心數據(例如電商商品庫存)時,仍然允許查詢緩存,如果緩存缺失,也可以繼續通過數據庫讀取。
3) 提前演練。 在項目上線前, 演練緩存層宕掉后, 應用以及后端的負載情況以及可能出現的問題, 在此基礎上做一些預案設定。
熱點緩存key重建優化
開發人員使用“緩存+過期時間”的策略既可以加速數據讀寫, 又保證數據的定期更新, 這種模式基本能夠滿足絕大部分需求。 但是有兩個問題如果同時出現, 可能就會對應用造成致命的危害:
- 當前key是一個熱點key(例如一個熱門的娛樂新聞),并發量非常大。
- 重建緩存不能在短時間完成, 可能是一個復雜計算, 例如復雜的SQL、 多次IO、 多個依賴等。
在緩存失效的瞬間, 有大量線程來重建緩存, 造成后端負載加大, 甚至可能會讓應用崩潰。
要解決這個問題主要就是要避免大量線程同時重建緩存。
我們可以利用互斥鎖來解決,此方法只允許一個線程重建緩存, 其他線程等待重建緩存的線程執行完, 重新從緩存獲取數據即可。
緩存與數據庫雙寫不一致
在大并發下,同時操作數據庫與緩存會存在數據不一致性問題
解決方案:
1、對于并發幾率很小的數據(如個人維度的訂單數據、用戶數據等),這種幾乎不用考慮這個問題,很少會發生緩存不一致,可以給緩存數據加上過期時間,每隔一段時間觸發讀的主動更新即可。
2、就算并發很高,如果業務上能容忍短時間的緩存數據不一致(如商品名稱,商品分類菜單等),緩存加上過期時間依然可以解決大部分業務對于緩存的要求。
3、如果不能容忍緩存數據不一致,可以通過加分布式讀寫鎖保證并發讀寫或寫寫的時候按順序排好隊,讀讀的時候相當于無鎖。
4、也可以用阿里開源的canal通過監聽數據庫的binlog日志及時的去修改緩存,但是引入了新的中間件,增加了系統的復雜度。
總結:能保障強一致性:**延時雙刪、分布式鎖;**不能保障強一致性,只能保障最終的一致性:異步通知
BigKey問題
實際中如果下面兩種情況,一般認為它是bigkey。
- 字符串類型:它的big體現在單個value值很大,一般認為超過10KB就是bigkey。
- 非字符串類型:哈希、列表、集合、有序集合,它們的big體現在元素個數太多。
非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進式刪除,同時要注意防止bigkey過期時間自動刪除問題(例如一個200萬的zset設置1小時過期,會觸發del操作,造成阻塞)
主動清理策略
當前已用內存超過maxmemory限定時,觸發主動清理策略。
a) 針對設置了過期時間的key做處理:
- volatile-ttl:在篩選時,會針對設置了過期時間的鍵值對,根據過期時間的先后進行刪除,越早過期的越先被刪除。
- volatile-random:就像它的名稱一樣,在設置了過期時間的鍵值對中,進行隨機刪除。
- volatile-lru:會使用 LRU 算法篩選設置了過期時間的鍵值對刪除。
- volatile-lfu:會使用 LFU 算法篩選設置了過期時間的鍵值對刪除。
b) 針對所有的key做處理:
- allkeys-random:從所有鍵值對中隨機選擇并刪除數據。
- allkeys-lru:使用 LRU 算法在所有數據中進行篩選刪除。
- allkeys-lfu:使用 LFU 算法在所有數據中進行篩選刪除。
c) 不處理:
- noeviction:不會剔除任何數據,拒絕所有寫入操作并返回客戶端錯誤信息"(error) OOM command not allowed when used memory",此時Redis只響應讀操作。
Redis隊列與Stream
Redis5.0 最大的新特性就是多出了一個數據結構 Stream,它是一個新的強大的支持多播的可持久化的消息隊列,作者聲明Redis Stream地借鑒了 Kafka 的設計。
Stream
每個 Stream 都有唯一的名稱,它就是 Redis 的 key,在我們首次使用 XADD 指令追加消息時自動創建。
- Consumer Group:消費者組,消費者組記錄了Stream的狀態,使用 XGROUP CREATE 命令手動創建,在同一個Stream內消費者組名稱唯一。一個消費組可以有多個消費者(Consumer)同時進行組內消費,所有消費者共享Stream內的所有信息,但同一條消息只會有一個消費者消費到,不同的消費者會消費Stream中不同的消息,這樣就可以應用在分布式的場景中來保證消息消費的唯一性。
- last_delivered_id:游標,用來記錄某個消費者組在Stream上的消費位置信息,每個消費組會有個游標,任意一個消費者讀取了消息都會使游標 last_delivered_id 往前移動。創建消費者組時需要指定從Stream的哪一個消息ID(哪個位置)開始消費,該位置之前的數據會被忽略,同時還用來初始化 last_delivered_id 這個變量。這個last_delivered_id一般來說就是最新消費的消息ID。
- pending_ids:消費者內部的狀態變量,作用是維護消費者的未確認的消息ID。pending_ids記錄了當前已經被客戶端讀取,但是還沒有 ack (Acknowledge character:確認字符)的消息。 目的是為了保證客戶端至少消費了消息一次,而不會在網絡傳輸的中途丟失而沒有對消息進行處理。如果客戶端沒有 ack,那么這個變量里面的消息ID 就會越來越多,一旦某個消息被ack,它就會對應開始減少。這個變量也被 Redis 官方稱為 PEL (Pending Entries List)。
Redis隊列幾種實現
基于List的 LPUSH+BRPOP 的實現
足夠簡單,消費消息延遲幾乎為零,但是需要處理空閑連接的問題。
如果線程一直阻塞在那里,Redis客戶端的連接就成了閑置連接,閑置過久,服務器一般會主動斷開連接,減少閑置資源占用,這個時候blpop和brpop或拋出異常,所以在編寫客戶端消費者的時候要小心,如果捕獲到異常需要重試。
基于Sorted-Set的實現
多用來實現延遲隊列,當然也可以實現有序的普通的消息隊列,但是消費者無法阻塞的獲取消息,只能輪詢,不允許重復消息。
Redis中的線程和IO模型
什么是Reactor模式 ?
“反應”器名字中”反應“的由來:
“反應”即“倒置”,“控制逆轉”,具體事件處理程序不調用反應器,而向反應器注冊一個事件處理器,表示自己對某些事件感興趣,有時間來了,具體事件處理程序通過事件處理器對某個指定的事件發生做出反應;這種控制逆轉又稱為“好萊塢法則”(不要調用我,讓我來調用你)
單線程Reactor模式流程
服務器端的Reactor是一個線程對象,該線程會啟動事件循環,并使用Acceptor事件處理器關注ACCEPT事件,這樣Reactor會監聽客戶端向服務器端發起的連接請求事件(ACCEPT事件)。
客戶端向服務器端發起一個連接請求,Reactor監聽到了該ACCEPT事件的發生并將該ACCEPT事件派發給相應的Acceptor處理器來進行處理。建立連接后關注的READ事件,這樣一來Reactor就會監聽該連接的READ事件了。
當Reactor監聽到有讀READ事件發生時,將相關的事件派發給對應的處理器進行處理。比如,讀處理器會通過讀取數據,此時read()操作可以直接讀取到數據,而不會堵塞與等待可讀的數據到來。
在目前的單線程Reactor模式中,不僅I/O操作在該Reactor線程上,連非I/O的業務操作也在該線程上進行處理了,這可能會大大延遲I/O請求的響應。所以我們應該將非I/O的業務邏輯操作從Reactor線程上卸載,以此來加速Reactor線程對I/O請求的響應。
單線程Reactor,工作者線程池
與單線程Reactor模式不同的是,添加了一個工作者線程池,并將非I/O操作從Reactor線程中移出轉交給工作者線程池來執行。這樣能夠提高Reactor線程的I/O響應,不至于因為一些耗時的業務邏輯而延遲對后面I/O請求的處理。
但是對于一些小容量應用場景,可以使用單線程模型,對于高負載、大并發或大數據量的應用場景卻不合適,主要原因如下:
① 一個NIO線程同時處理成百上千的鏈路,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的讀取和發送;
② 當NIO線程負載過重之后,處理速度將變慢,這會導致大量客戶端連接超時,超時之后往往會進行重發,這更加重了NIO線程的負載,最終會導致大量消息積壓和處理超時,成為系統的性能瓶頸;
多Reactor線程模式
Reactor線程池中的每一Reactor線程都會有自己的Selector、線程和分發的事件循環邏輯。
mainReactor可以只有一個,但subReactor一般會有多個。mainReactor線程主要負責接收客戶端的連接請求,然后將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通信。
多Reactor線程模式將“接受客戶端的連接請求”和“與該客戶端的通信”分在了兩個Reactor線程來完成。mainReactor完成接收客戶端連接請求的操作,它不負責與客戶端的通信,而是將建立好的連接轉交給subReactor線程來完成與客戶端的通信,這樣一來就不會因為read()數據量太大而導致后面的客戶端連接請求得不到即時處理的情況。并且多Reactor線程模式在海量的客戶端并發請求的情況下,還可以通過實現subReactor線程池來將海量的連接分發給多個subReactor線程,在多核的操作系統中這能大大提升應用的負載和吞吐量。
Redis中的線程和IO概述
Redis 基于 Reactor 模式開發了自己的網絡事件處理器 - 文件事件處理器(file event handler,后文簡稱為 FEH),而該處理器又是單線程的,所以redis設計為單線程模型。
采用I/O多路復用同時監聽多個socket,根據socket當前執行的事件來為 socket 選擇對應的事件處理器。
當被監聽的socket準備好執行accept、read、write、close等操作時,和操作對應的文件事件就會產生,這時FEH就會調用socket之前關聯好的事件處理器來處理對應事件。
所以雖然FEH是單線程運行,但通過I/O多路復用監聽多個socket,不僅實現高性能的網絡通信模型,又能和 Redis 服務器中其它同樣單線程運行的模塊交互,保證了Redis內部單線程模型的簡潔設計。
事件的類型(套接字)
I/O多路復用程序可以監聽多個套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件
這兩類事件和套接字操作之間的對應關系如下:
套接字變得可讀時(客戶端對套接字執行write操作,或者執行close操作),或者有新的可應答(acceptable)套接字出現時(客戶端對服務器的監聽套接字執行connect操作),套接字產生AE_READABLE事件
當套接字變得可寫時(客戶端對套接字執行read操作),套接字產生AE_WRITABLE事件
I/O多路復用程序允許服務器同時監聽套接字的AE_READABLE事件和AE_WRITABLE 事件,如果一個套接字同時產生了這兩種事件,那么文件事件分派器會優先處理 AE_READABLE事件,等到AE_READABLE事件處理完之后,才處理AE_WRITABLE事件。這也就是說,如果一個套接字又可讀又可寫的話,那么服務器將先讀套接字,后寫套接字
HyperLogLog
HyperLogLo并不是一種新的數據結構(實際類型為字符串類型),而是一種基數算法,通過HyperLogLog可以利用極小的內存空間完成獨立總數的統計,數據集可以是IP、Email、ID等。
HyperLogLog 提供不精確的去重計數方案,雖然不精確但是也不是非常不精確,Redis官方給出標準誤差是 0.81%
HyperLogLog基于概率論中伯努利試驗并結合了極大似然估算方法,并做了分桶優化。
實際上目前還沒有發現更好的在大數據場景中準確計算基數的高效算法,因此在不追求絕對準確的情況下,使用概率算法算是一個不錯的解決方案。概率算法不直接存儲數據集合本身,通過一定的概率統計方法預估值,這種方法可以大大節省內存,同時保證誤差控制在一定范圍內。目前用于基數計數的概率算法包括:
Linear Counting(LC):早期的基數估計算法,LC在空間復雜度方面并不算優秀;
LogLog Counting(LLC):LogLog Counting相比于LC更加節省內存,空間復雜度更低;
HyperLogLog Counting(HLL):HyperLogLog Counting是基于LLC的優化和改進,在同樣空間復雜度情況下,能夠比LLC的基數估計誤差更小。
Redis事務
Redis事務
大家應該對事務比較了解,簡單地說,事務表示一組動作,要么全部執行,要么全部不執行。例如在社交網站上用戶A關注了用戶B,那么需要在用戶A的關注表中加入用戶B,并且在用戶B的粉絲表中添加用戶A,這兩個行為要么全部執行,要么全部不執行,否則會出現數據不一致的情況。
Redis提供了簡單的事務功能,將一組需要一起執行的命令放到multi和exec兩個命令之間。multi(['m?lti]) 命令代表事務開始,exec(美[?ɡ?zek])命令代表事務結束,如果要停止事務的執行,可以使用discard命令代替exec命令即可。
可以看到Redis并不支持回滾功能,開發人員需要自己修復這類問題。
有些應用場景需要在事務之前,確保事務中的key沒有被其他客戶端修改過,才執行事務,否則不執行(類似樂觀鎖)。Redis 提供了watch命令來解決這類問題。
Pipeline和事務的區別
簡單來說,
1、pipeline是客戶端的行為,對于服務器來說是透明的,可以認為服務器無法區分客戶端發送來的查詢命令是以普通命令的形式還是以pipeline的形式發送到服務器的;
2、而事務則是實現在服務器端的行為,用戶執行MULTI命令時,服務器會將對應這個用戶的客戶端對象設置為一個特殊的狀態,在這個狀態下后續用戶執行的查詢命令不會被真的執行,而是被服務器緩存起來,直到用戶執行EXEC命令為止,服務器會將這個用戶對應的客戶端對象中緩存的命令按照提交的順序依次執行。
3、應用pipeline可以提高服務器的吞吐能力,并提高Redis處理查詢請求的能力。
但是這里存在一個問題,當通過pipeline提交的查詢命令數據較少,可以被內核緩沖區所容納時,Redis可以保證這些命令執行的原子性。然而一旦數據量過大,超過了內核緩沖區的接收大小,那么命令的執行將會被打斷,原子性也就無法得到保證。因此pipeline只是一種提升服務器吞吐能力的機制,如果想要命令以事務的方式原子性的被執行,還是需要事務機制,或者使用更高級的腳本功能以及模塊功能。
4、可以將事務和pipeline結合起來使用,減少事務的命令在網絡上的傳輸時間,將多次網絡IO縮減為一次網絡IO。
Redis提供了簡單的事務,之所以說它簡單,主要是因為它不支持事務中的回滾特性,同時無法實現命令之間的邏輯關系計算,當然也體現了Redis 的“keep it simple”的特性。
Redis 復制緩存區相關問題分析
多從庫時主庫內存占用過多
OutputBuffer 拷貝和釋放的堵塞問題
Redis 為了提升多從庫全量復制的效率和減少 fork 產生 RDB 的次數,會盡可能的讓多個從庫共用一個 RDB
當已經有一個從庫觸發 RDB BGSAVE 時,后續需要全量同步的從庫會共享這次 BGSAVE 的 RDB,為了從庫復制數據的完整性,會將之前從庫的 OutputBuffer 拷貝到請求全量同步從庫的 OutputBuffer 中。
其中的copyClientOutputBuffer 可能存在堵塞問題,因為 OutputBuffer 鏈表上的數據可達數百 MB 甚至數 GB 之多,對其拷貝可能使用百毫秒甚至秒級的時間,而且該堵塞問題沒法通過日志或者 latency 觀察到,但對Redis性能影響卻很大。
同樣地,當 OutputBuffer 大小觸發 limit 限制時,Redis 就是關閉該從庫鏈接,而在釋放 OutputBuffer 時,也需要釋放數百 MB 甚至數 GB 的數據,其耗時對 Redis 而言也很長。
ReplicationBacklog 的限制
我們知道復制積壓緩沖區 ReplicationBacklog 是 Redis 實現部分重同步的基礎,如果從庫可以進行增量同步,則主庫會從 ReplicationBacklog 中拷貝從庫缺失的數據到其 OutputBuffer。拷貝的數據量最大當然是 ReplicationBacklog 的大小,為了避免拷貝數據過多的問題,通常不會讓該值過大,一般百兆左右。但在大容量實例中,為了避免由于主從網絡中斷導致的全量同步,又希望該值大一些,這就存在矛盾了。
而且如果重新設置 ReplicationBacklog 大小時,會導致 ReplicationBacklog 中的內容全部清空,所以如果在變更該配置期間發生主從斷鏈重連,則很有可能導致全量同步。
Redis7.0共享復制緩存區的設計與實現
簡述
每個從庫在主庫上單獨擁有自己的 OutputBuffer,但其存儲的內容卻是一樣的,一個最直觀的想法就是主庫在命令傳播時,將這些命令放在一個全局的復制數據緩沖區中,多個從庫共享這份數據,不同的從庫對引用復制數據緩沖區中不同的內容,這就是『共享復制緩存區』方案的核心思想。實際上,復制積壓緩沖區(ReplicationBacklog)中的內容與從庫 OutputBuffer 中的數據也是一樣的,所以該方案中,ReplicationBacklog 和從庫一樣共享一份復制緩沖區的數據,也避免了 ReplicationBacklog 的內存開銷。
『共享復制緩存區』方案中復制緩沖區 (ReplicationBuffer) 的表示采用鏈表的表示方法,將 ReplicationBuffer 數據切割為多個 16KB 的數據塊 (replBufBlock),然后使用鏈表來維護起來。為了維護不同從庫的對 ReplicationBuffer 的使用信息,在 replBufBlock 中存在字段:
refcount:block 的引用計數
id:block 的唯一標識,單調遞增的數值
repl_offset:block 開始的復制偏移
ReplicationBuffer 由多個 replBufBlock 組成鏈表,當 復制積壓區 或從庫對某個 block 使用時,便對正在使用的 replBufBlock 增加引用計數,上圖中可以看到,復制積壓區正在使用的 replBufBlock refcount 是 1,從庫 A 和 B 正在使用的 replBufBlock refcount 是 2。當從庫使用完當前的 replBufBlock(已經將數據發送給從庫)時,就會對其 refcount 減 1 而且移動到下一個 replBufBlock,并對其 refcount 加 1。
堵塞問題和限制問題的解決
多從庫消耗內存過多的問題通過共享復制緩存區方案得到了解決,對于OutputBuffer 拷貝和釋放的堵塞問題和 ReplicationBacklog 的限制問題是否解決了呢?
首先來看 OutputBuffer 拷貝和釋放的堵塞問題問題, 這個問題很好解決,因為ReplicationBuffer 是個鏈表實現,當前從庫的 OutputBuffer 只需要維護共享 ReplicationBuffer 的引用信息即可。所以無需進行數據深拷貝,只需要更新引用信息,即對正在使用的 replBufBlock refcount 加 1,這僅僅是一條簡單的賦值操作,非常輕量。OutputBuffer 釋放問題呢?在當前的方案中釋放從庫 OutputBuffer 就變成了對其正在使用的 replBufBlock refcount 減 1,也是一條賦值操作,不會有任何阻塞。
對于ReplicationBacklog 的限制問題也很容易解決了,因為 ReplicatonBacklog 也只是記錄了對 ReplicationBuffer 的引用信息,對 ReplicatonBacklog 的拷貝也僅僅成了找到正確的 replBufBlock,然后對其 refcount 加 1。這樣的話就不用擔心 ReplicatonBacklog 過大導致的拷貝堵塞問題。而且對 ReplicatonBacklog 大小的變更也僅僅是配置的變更,不會清掉數據。
數據結構選取
rax樹
Redis中還有其他地方使用了Rax樹,比如我們前面學習過的streams 這個類型里面的 consumer group(消費者組) 的名稱還有和Redis集群名稱存儲。
RAX叫做基數樹(前綴壓縮樹),就是有相同前綴的字符串,其前綴可以作為一個公共的父節點,什么又叫前綴樹?
Trie樹
即字典樹,也有的稱為前綴樹,是一種樹形結構。廣泛應用于統計和排序大量的字符串(但不僅限于字符串),所以經常被搜索引擎系統用于文本詞頻統計。它的優點是最大限度地減少無謂的字符串比較,查詢效率比較高。
Trie的核心思想是空間換時間,利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。
當從庫嘗試與主庫進行增量重同步時,會發送自己的 repl_offset,主庫在每個 replBufBlock 中記錄了該其第一個字節對應的 repl_offset,但如何高效地從數萬個 replBufBlock 的鏈表中找到特定的那個?
**最終使用 rax 樹實現了對 replBufBlock 固定區間間隔的索引,每 64 個記錄一個索引點。**一方面,rax 索引占用的內存較少;另一方面,查詢效率也是非常高,理論上查找比較次數不會超過 100,耗時在 1 毫秒以內。
Radix樹:壓縮后的Trie樹,將不可分叉的單支分支合并,也就是壓縮。
熱Key
什么是熱key
1 、MySQL等數據庫會被頻繁訪問的熱數據
如爆款商品的skuId。
2 、redis的被密集訪問的key
如爆款商品的各維度信息,skuId、shopId等。
3 、機器人、爬蟲、刷子用戶
如用戶的userId、uuid、ip等。
4 、某個接口地址
如/sku/query或者更精細維度的。
5、 用戶id+接口信息
如userId + /sku/query,這代表某個用戶訪問某個接口的頻率。
6 、服務器id+接口信息
如ip + /sku/query,這代表某臺服務器某個接口被訪問的頻率。
7 、用戶id+接口信息+具體商品
如userId + /sku/query + skuId,這代表某個用戶訪問某個商品的頻率。
以上我們都稱之為有風險的key,注意,我們的熱key探測框架只關心key,其實就是一個字符串,隨意怎么組合成這個字符串由使用者自己決定,所以該框架具備非常強的靈活性,可以完成熱數據探測、限流熔斷、統計等多種功能。