首頁 >資料庫 >Redis >聊聊redis中多樣的資料類型,以及與叢集相關的知識

聊聊redis中多樣的資料類型,以及與叢集相關的知識

青灯夜游
青灯夜游轉載
2022-01-10 18:48:471531瀏覽

這篇文章帶大家了解redis中多元的資料類型,以及集群相關的知識,帶大家搞懂集群,希望對大家有幫助!

聊聊redis中多樣的資料類型,以及與叢集相關的知識

多樣的資料型別

string 類型簡單方便,支援空間預先分配,也就是每次都會多分配點空間,這樣string 如果下次變長的話,就不需要額外的申請空了,當然前提是剩餘的空間夠用。 【相關建議:Redis影片教學

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 信息之后就会再次更新下本地缓存,这样下次就不会出现这两个错误了。

更多编程相关知识,请访问:编程入门!!

以上是聊聊redis中多樣的資料類型,以及與叢集相關的知識的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除