網站首頁 編程語言 正文
多樣的數據類型
string 類型簡單方便,支持空間預分配,也就是每次會多分配點空間,這樣 string 如果下次變長的話,就不需要額外的申請空了,當然前提是剩余的空間夠用。
List 類型可以實現簡單的消息隊列,但是注意可能存在消息丟失哦,它并不持 ACK 模式。
Hash 表有點像關系型數據庫,但是當 hash 表越來越大的時候,請注意,避免使用 hgetall 之類的語句,因為請求大量的數據會導致redis阻塞,這樣后面的兄弟們就得等待了。
set 集合類型可以幫你做一些統計,比如你要統計某天活躍的用戶,可以直接把用戶ID扔到集合里,集合支持一些騷操作,比如 sdiff 可以獲取集合之間的差集,sunion 可以獲取集合之間的并集,功能很多,但是一定需要謹慎,因為牛逼的功能是有代價的,這些操作需要耗費一些 CPU 和IO 資源,可能會導致阻塞,因此大集合之間的騷操作要慎用,
zset 可以說是最閃耀的星,可以做排序,因為可以排序,因此應用場景挺多,比如點贊前xx名用戶,延時隊列等等。
bitmap 位圖的好處就是在于節省空間,特別在做一些統計類的方面,比如要統計某一天有多少個用戶簽到了并且某個用戶是否簽到了,如果不用bitmap的話,你可能會想到用set。
SADD day 1234//簽到就添加到集合 SISMEMBER day 1234//判斷1234是否簽到 SCARD day //有多少個簽到的
set 在功能上可以滿足,但是相比bitmap的話,set要更耗費存儲空間,set的底層主要是由整數集合或者 hashtable 組成,整數集合只有在數據量非常小的情況下才會使用,一般是小于512個元素,同時元素必須都是整數,對于set來說,整數集合的數據更加緊湊,他們在內存是上連續的,查詢的話只能是二分查找了,時間復雜度是O(logN),而 hashtable 就不同了,這里的 hashtable 和 redis 的5大數據類型中的hash是一樣的,只不過沒有 value 而已,value 指向個 null,同時也不存在沖突,因為這里是集合,但是需要考慮 rehash 相關問題。ok,扯的有點遠,我們說的用戶簽到問題,在用戶非常多的情況下,set 的話肯定會用到 hashtable,hashtable 的話,其實每個元素都是個 dictEntry 結構體
typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下個哈希表節點,形成鏈表 struct dictEntry *next; ???????} dictEntry;
從這個結構體可以看到什么呢?首先雖然值 union(沒有 value)和 next(沒有沖突)是空的,但是結構體本身需要空間,還需要加上個 key,這個占用空間是實打實的,而如果用 bitmap 的話,一個bit位就可以代表一個數字,很省空間,我們來看看 bitmap 的方式如何設置和統計。
SETBIT day 1234 1//簽到 GETBIT day 1234//判斷1234是否簽到 BITCOUNT day//有多少個簽到的
bf 這是 redis4.0 之后支持的布隆過濾器 RedisBloom,但是需要單獨加載對應的 module,當然我們也可以基于上述的 bitmap 來實現自己的布隆過濾器,不過既然 redis 已經支持了,通過 RedisBloom 可以減少我們的開發時間,布隆過濾器是干嘛的,我這里就不贅述了,直接來看看 RedisBloom 相關的用法吧。
# 可以通過docker的方式快速拉取鏡像來玩耍 docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest docker exec -it redis-redisbloom bash redis-cli # 相關操作 bf.reserve sign 0.001 10000 bf.add sign 99 //99這個用戶加入 bf.add exists 99//判斷99這個用戶是否存在
因為布隆過濾器是存在誤判的,所有 bf 支持自定義誤判率,0.001就代表誤判率,10000 代表布隆過濾器可以存儲的元素個數,當實際存儲的元素個數超過這個值的時候,誤判率會提高。
HyperLogLog 可以用于統計,它的優點就是占用的存儲空間極小,只需要 12KB 的內存就可以統計 2^64 個元素,那它主要統計什么呢?其實主要就是基數統計,比如像 UV 這種,從功能上來說 UV 可以用 set 或者 hash 來存儲,但是缺點就是耗費存儲,容易使之變成大 key,如果想要節省空間,bitmap 也可以,12KB 空間的 bitmap 只能統計 12*1024*8=98304個元素,而 HyperLogLog 卻可以統計 2^64 個元素,但是這么牛逼的技術其實是有誤差的,HyperLogLog 是基于概率來統計的,標準誤算率是 0.81%,在統計海量數據并且對精度要求不那么高的場景下,HyperLogLog 在節省空間這塊還是很優秀的。
PFADD uv 1 2 3 //1 2 3是活躍用戶 PFCOUNT uv //統計
GEO 是可以應用在地理位置的業務上,比如微信附近的人或者附近的車輛等等,先來看一下如果沒有GEO 這種數據結構,你如何知道你附近的人?首先得上報自己的地理位置信息吧,比如經度 116.397128,緯度 39.916527,此時可以用 string、hash 數據類型存儲,但是如果要查找你附近的人,string 和 hash 這種就無能為例了,你不可能每次都要遍歷全部的數據來判斷,這樣太耗時了,當然你也不可能通過 zset 這種數據結構來把經緯度信息當成權重,但是如果我們能把經緯度信息通過某種方式轉換成一個數字,然后當成權重好像也可以,這時我們只需通過zrangebyscore key v1 v2也可以找到附近的人。真的需要這么麻煩嗎?于是 GEO 出現了,GEO 轉換經緯度為數字的方法是“二分區間,區間編碼”,這是什么意思呢?以經度為例,它的范圍是[-180,180],如果要采用3位編碼值,那么就是需要二分3次,二分后落在左邊的用0表示,右邊的用1表示,以經度是121.48941 來說,第一次是在[0,180]這個區間,因此記1,第二次是在[90,180],因此再記1,第三次是在[90,135],因此記0。緯度也是同樣的邏輯,假設此時對應的緯度編碼后是010,最后把經緯度合并在一起,需要注意的是經度的每個值在偶數位,緯度的每個值在奇數位。
1 1 0 //經度 0 1 0 //緯度 ------------ 101100 //經緯度對應的數值
原理是這樣,我們再來看看 redis 如何使用 GEO:
GEOADD location 112.123456 41.112345 99 //上報用戶99的地理位置信息 GEORADIUS location 112.123456 41.112345 1 km ASC COUNT 10 //獲取附近1KM的人
搞懂集群
生產環境用單實例 redis 的應該比較少,單實例的風險在于:
- 單點故障即服務故障,沒有backup
- 單實例壓力大,又要提供讀,又要提供寫
于是我們首先想到的就是經典的主從模式,而且往往是一主多從,這是因為大部分應用都是讀多寫少的情況,我們的主負責更新,從負責提供讀,就算我們的主宕機了,我們也可以選擇一個從來充當主,這樣整個應用依然可以提供服務。
復制過程的細節
當一個 redis 實例首次成為某個主的從的時候,這時主得把數據發給它,也就是 rdb 文件,這個過程 master 是要 fork 一個子進程來處理的,這個子進程會執行 bgsave 把當前的數據重新保存一下,然后準備發給新來的從,bgsave 的本質是讀取當前內存中的數據然后保存到 rdb 文件中,這個過程涉及大量的 IO,如果直接在主進程中來處理的話,大概率會阻塞正常的請求,因此使用個子進程是個明智的選擇。
那 fork 的子進程在 bgsave 過程中如果有新的變更請求會怎么辦?
嚴格來說子進程出來的一瞬間,要保存的數據應該就是當時那個點的快照數據,所以是直接把當時的內存再復制一份嗎?不復制的話,如果這期間又有變更改怎么辦?其實這要說到寫實復制(COW)機制,首先從表象上來看內存是一整塊空間,其實這不太好維護,因此操作系統會把內存分成一小塊一小塊的,也就是內存分頁管理,一頁的大小一般是4K、8K或者16K等等,redis 的數據都是分布在這些頁面上的,出于效率問題,fork 出來的子進程是和主進程是共享同一塊的內存的,并不會復制內存,如果這期間主進程有數據變更,那么為了區分,這時最快捷的做法就是把對應的數據頁重新復制一下,然后主的變更就在這個新的數據頁上修改,并不會修改來的數據頁,這樣就保證了子進程處理的還是當時的快照。
以上說的變更是從快照的角度來考慮的,如果從數據的一致性來說,當快照的 rdb 被從庫應用之后,這期間的變更該如何同步給從庫?答案是緩沖區,這個緩沖區叫做 replication buffer,主庫在收到需要同步的命令之后,會把期間的變更都先保存在這個緩沖區中,這樣在把 rdb 發給從庫之后,緊接著會再把 replication buffer 的數據也發給從庫,最終主從就保持了一致。
replication buffer不是萬能的補給劑
我們來看看 replication buffer 持續寫入的時間有多長。
- 我們知道主從同步的時候,主庫會執行 fork 來讓子進程完成相應地工作,因此子進程從開始執行 bgsave 到執行完畢這期間,變更是要寫入 replication buffer 的。
- rdb 生成好之后,需要把它發送給從庫,這個網絡傳輸是不是也需要耗點時間,這期間也是要寫入 replication buffer 的。
- 從庫在收到 rdb 之后需要把 rdb 應用到內存里,這期間從庫是阻塞的,無法提供服務,因此這期間也是要寫入 replication buffer 的。
replication buffer 既然是個 buffer,那么它的大小就是有限的,如果說上面3個步驟中,只要有一個耗時長,就會導致 replication buffer 快速增長(前提是有正常的寫入),當 replication buffer 超過了限制之后就會導致主庫和從庫之間的連接斷開,斷開之后如果從庫再次連接上來就會導致重新開始復制,然后重復同樣的漫長的復制步驟,因此這個 replication buffer 的大小還是很關鍵的,一般需要根據寫入的速度、每秒寫入的量和網絡傳輸的速度等因素來綜合判斷。
從庫網絡不好和主庫斷了該怎么辦?
正常來說,只要主從之間的連接建立好了,后面主庫的變更可以直接發給從庫,讓從庫直接回放,但是我們并不能保證網絡環境是百分百的通暢的,因此也要考慮從庫和主庫之間的斷聯問題。
應該是在 redis2.8 以前,只要從庫斷聯,哪怕只有很短的時間,后面從庫再次連接上來的時候,主庫也會直接無腦的進行全量同步。在 2.8 版本及以后,開始支持增量復制了,增量復制的原理就是得有個緩沖區來保存變更的記錄,這里這個緩沖區叫做repl_backlog_buffer,這個緩沖區從邏輯上來說是個環形緩沖區,寫滿了就會從頭開始覆蓋,所以也有大小限制。在從庫重新連接上來的時候,從庫會告訴主庫:“我當前已經復制到了xx位置”,主庫收到從庫的消息之后開始查看xx位置的數據是否還在 repl_backlog_buffer 中,如果在的話,直接把xx后面的數據發給從庫即可,如果不在的話,那無能為力了,只能再次進行全量同步。
需要一個管理者
在主從模式下,如果主庫掛了,我們可以把一個從庫升級成主庫,但是這個過程是手動的,靠人力來操作,不能使損失降到最低,還是需要一套自動管理和選舉的機制,這就是哨兵,哨兵它本身也是個服務,只不過它不處理數據的讀寫而已,它只負責管理所有的 redis 實例,哨兵每隔一段時間會和各個 redis 通信(ping 操作),每個 redis 實例只要在規定的時間內及時回復,就可以表明自己的立場。當然哨兵本身也可能存在宕機或者網絡不通的情況,因此一般哨兵也會搭建個哨兵集群,這個集群的個數最好是奇數,比如3個或者5這個這種,奇數的目的主要就是為了選舉(少數服從多數)。
當某個哨兵在發起 ping 后沒有及時收到 pong,那么就會把這個 redis 實例標記下線,此時它還是不是真正的下線,這時其他的哨兵也會判定當前這個哨兵是不是真正的下線,當大多數哨兵都認定這個 redis 是下線狀態,那么就會把它從集群中踢出去,如果下線的是從庫,那么還好,直接踢出去就ok,如果是主庫還要觸發選舉,選舉也不是盲目選舉,肯定是要選出最合適的那個從來充當新的主庫。這個最合適充當主庫的庫,一般會按照以下優先級來確定:
- 權重,每個從庫其實都可以設置一個權重,權重越高的從庫會被優先選擇
- 復制的進度,每個從庫復制的進度可能是不一樣的,優先選擇當前和主庫數據差距最小的那個
- 服務的 ID,其實每個 redis 實例都有自己的 ID,如果以上條件都一樣,那么會選擇 ID 最小的那個庫來充當主庫
更強的橫向伸縮性
主從模式解決了單點故障問題,同時讀寫分離技術使得應用支撐能力更強,哨兵模式可以自動監管集群,實現自動選主,自動剔除故障節點的能力。
正常來說只要讀的壓力越來越大,我們可以添加從庫來緩解,那如果主庫壓力很大怎么辦?這就得提到接下來要說的分片技術了,我們只需要把主庫切成幾片,部署到不同的機器上即可。這個分片就是 redis 中的槽概念了,當分片的時候,redis 會默認分成 0~16383 也就是一共 16384 個槽,然后把這些槽平均分到每個分片節點上就可以起到負載均衡的作用了。每個 key 具體該分到哪個槽中,主要是先 CRC16 得到一個 16bit 的數字,然后這個數字再對 16384 取模即可:
crc16(key)%16384
然后客戶端會緩存槽信息,這樣每當一個 key 到來時,只要通過計算就知道該發給哪個實例來處理來了。但是客戶端緩存的槽信息并不是一成不變的,比如在增加實例的時候,這時候會導致重新分片,那么原來客戶端緩存的信息就會不準確,一般這時候會發生兩個常見的錯誤,嚴格來說也不是錯誤,更像一種信息,一個叫做MOVED,一個叫做ASK。moved的意思就說,原來是實例A負責的數據,現在被遷移到了實例B,MOVED 代表的是遷移完成的,但是 ASK 代表的是正在遷移過程中,比如原來是實例A負責的部分數據,現在被遷移到了實例B,剩下的還在等待遷移中,當數據遷移完畢之后 ASK 就會變成 MOVED,然后客戶端收到 MOVED 信息之后就會再次更新下本地緩存,這樣下次就不會出現這兩個錯誤了。
總結
原文鏈接:https://mp.weixin.qq.com/s/5QSLUtbyCI3tiB0papRhIA
相關推薦
- 2024-01-09 JPA查詢——setResultTransformer過期替換
- 2022-07-21 配置nacos持久化
- 2021-12-09 C++中的編譯與鏈接_C 語言
- 2022-06-24 Python利用隨機函數生成變化圖形詳解_python
- 2022-03-28 go實現一個分布式限流器的方法步驟_Golang
- 2022-12-12 C語言中帶頭雙向循環鏈表基本操作的實現詳解_C 語言
- 2022-12-13 C++實現defer聲明方法詳解_C 語言
- 2022-04-09 整合Spring + SpringMVC + Mybatis基礎框架的配置文件
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支