溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何進行Redis深度分析

發(fā)布時間:2021-10-18 10:31:51 來源:億速云 閱讀:154 作者:柒染 欄目:大數(shù)據(jù)

今天就跟大家聊聊有關(guān)如何進行Redis深度分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

0、基礎(chǔ):萬丈高樓平地起

1) 當字符串長度小于1M時,擴容都是加倍現(xiàn)有的空間,如果超過1M,擴容時一次只會多擴1M的空間。需要注意的是字符串最大長度為512M。

2) 如果value值是一個整數(shù),還可以對它進行自增操作。自增是有范圍的,它的范圍是signed long的最大最小值,超過了這個值,Redis會
報錯,最大值 9223372036854775807
    
3) Redis的列表相當于Java語言里面的LinkedList,注意它是鏈表而不是數(shù)組。Redis的列表結(jié)構(gòu)常用來做異步隊列使用。將需要延后處理
的任務(wù)結(jié)構(gòu)體序列化成字符串塞進Redis的列表,另一個線程從這個列表中輪詢數(shù)據(jù)進行處理。
   右邊進左邊出--> 隊列
   右邊進右邊出--> 棧
   
   如果再深入一點,你會發(fā)現(xiàn) Redis 底層存儲的還不是一個簡單的linkedlist,而是稱之為快速鏈表quicklist的一個結(jié)構(gòu)。首先在列表
元素較少的情況下會使用一塊連續(xù)的內(nèi)存存儲,這個結(jié)構(gòu)是ziplist,也即是壓縮列表。它將所有的元素緊挨著一起存儲,分配的是一塊連
續(xù)的內(nèi)存。當數(shù)據(jù)量比較多的時候才會改成quicklist。因為普通的鏈表需要的附加指針空間太大,會比較浪費空間,而且會加重內(nèi)存的碎
片化。比如這個列表里存的只是 int 類型的數(shù)據(jù),結(jié)構(gòu)上還需要兩個額外的指針prev和next。所以Redis將鏈表和ziplist結(jié)合起來組成了
quicklist。也就是將多個ziplist使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現(xiàn)太大的空間冗余。
   
4) 壓縮列表ziplist是一種為節(jié)約內(nèi)存而開發(fā)的順序型數(shù)據(jù)結(jié)構(gòu),它被用作列表鍵和哈希鍵的底層實現(xiàn)之一。

5) Hash,Redis的字典的值只能是字符串,而且Redis采用漸進式Rehash策略

6) set,Redis的集合相當于Java語言里面的HashSet,內(nèi)部的鍵值對是無序的唯一的,其內(nèi)部實現(xiàn)相當于一個特殊的字典,字典中所有的
value都是一個值NULL。set結(jié)構(gòu)可以用來存儲活動中獎的用戶ID,因為有去重功能,可以保證同一個用戶不會中獎兩次。
   
7) zset,它類似于Java的SortedSet和HashMap的結(jié)合體,一方面它是一個set,保證了內(nèi)部value的唯一性,另一方面它可以給每個value
賦予一個score,代表這個value的排序權(quán)重。它的內(nèi)部實現(xiàn)用的是一種叫著跳躍列表的數(shù)據(jù)結(jié)構(gòu)。zset還可以用來存儲學(xué)生的成績,value
值是學(xué)生的ID,score是他的考試成績。我們可以對成績按分數(shù)進行排序就可以得到他的名次。zset可以用來存粉絲列表,value值是粉絲的
用戶ID,score是關(guān)注時間。我們可以對粉絲列表按關(guān)注時間進行排序。
//https://yq.aliyun.com/articles/666398
typedef struct zset {

    // 字典,鍵為成員,值為分值
    // 用于支持 O(1) 復(fù)雜度的按成員取分值操作
    dict *dict;

    // 跳躍表,按分值排序成員
    // 用于支持平均復(fù)雜度為 O(log N) 的按分值定位成員操作
    // 以及范圍操作
    zskiplist *zsl;
} zset;

1、千帆競發(fā):Redis分布式鎖

1) 使用setnx命令實現(xiàn)分布式鎖
   
超時問題?
   Redis 的分布式鎖不能解決超時問題,如果在加鎖和釋放鎖之間的邏輯執(zhí)行的太長,以至于超出了鎖的超時限制,就會出現(xiàn)問題。因為這
時候鎖過期了,第二個線程重新持有了這把鎖,但是緊接著第一個線程執(zhí)行完了業(yè)務(wù)邏輯,就把鎖給釋放了,第三個線程就會在第二個線程
邏輯執(zhí)行完之間拿到了鎖。
   
解決辦法:
1.1) Redis分布式鎖不要用于較長時間任務(wù)。如果真的偶爾出現(xiàn)了,數(shù)據(jù)出現(xiàn)的小波錯亂可能需要人工介入解決。
1.2) 有一個更加安全的方案是為set指令的value參數(shù)設(shè)置為一個隨機數(shù),釋放鎖時先匹配隨機數(shù)是否一致,然后再刪除key。但是匹配value
和刪除key不是一個原子操作,這就需要使用Lua腳本來處理了,因為Lua腳本可以保證連續(xù)多個指令的原子性執(zhí)行。

2) 單機Redis實現(xiàn)分布式鎖的缺陷
   比如在Sentinel集群中,主節(jié)點掛掉時,從節(jié)點會取而代之,客戶端上卻并沒有明顯感知。原先第一個客戶端在主節(jié)點中申請成功了一把
鎖,但是這把鎖還沒有來得及同步到從節(jié)點,主節(jié)點突然掛掉了。然后從節(jié)點變成了主節(jié)點,這個新的節(jié)點內(nèi)部沒有這個鎖,所以當另一個客
戶端過來請求加鎖時,立即就批準了。這樣就會導(dǎo)致系統(tǒng)中同樣一把鎖被兩個客戶端同時持有,不安全性由此產(chǎn)生。
不過這種不安全也僅僅是在主從發(fā)生failover的情況下才會產(chǎn)生,而且持續(xù)時間極短,業(yè)務(wù)系統(tǒng)多數(shù)情況下可以容忍。
   
解決辦法:RedLock
   加鎖時,它會向過半節(jié)點發(fā)送 set(key, value, nx=True, ex=xxx) 指令,只要過半節(jié)點set成功,那就認為加鎖成功。釋放鎖時,需要
向所有節(jié)點發(fā)送del指令。不過Redlock算法還需要考慮出錯重試、時鐘漂移等很多細節(jié)問題,同時因為Redlock需要向多個節(jié)點進行讀寫,意
味著相比單實例 Redis 性能會下降一些。
如果你很在乎高可用性,希望掛了一臺redis完全不受影響,那就應(yīng)該考慮 redlock。不過代價也是有的,需要更多的 redis 實例,性能
也下降了,代碼上還需要引入額外的library,運維上也需要特殊對待,這些都是需要考慮的成本,使用前請再三斟酌。
   
3) 鎖沖突處理
   我們講了分布式鎖的問題,但是沒有提到客戶端在處理請求時加鎖沒加成功怎么辦。
   
一般有3種策略來處理加鎖失敗:
3.1) 直接拋出異常,通知用戶稍后重試
     這種方式比較適合由用戶直接發(fā)起的請求,用戶看到錯誤對話框后,會先閱讀對話框的內(nèi)容,再點擊重試,這樣就可以起到人工延時
的效果。如果考慮到用戶體驗,可以由前端的代碼替代用戶自己來進行延時重試控制。它本質(zhì)上是對當前請求的放棄,由用戶決定是否重新
發(fā)起新的請求。
     
3.2) sleep一會再重試,不推薦

3.3) 將請求轉(zhuǎn)移至延時隊列,過一會再試
     這種方式比較適合異步消息處理,將當前沖突的請求扔到另一個隊列延后處理以避開沖突。

2、緩兵之計:延時隊列

1)異步消息隊列   
   Redis的消息隊列不是專業(yè)的消息隊列,它沒有非常多的高級特性,沒有ack保證,如果對消息的可靠性有著極致的追求,那么它就不適合使用。
   Redis的list(列表)數(shù)據(jù)結(jié)構(gòu)常用來作為異步消息隊列使用,使用rpush/lpush操作入隊列,使用lpop和rpop來出隊列。
   
隊列空了怎么辦?
   可是如果隊列空了,客戶端就會陷入pop的死循環(huán),不停地pop,沒有數(shù)據(jù),接著再pop,又沒有數(shù)據(jù)。這就是浪費生命的空輪詢??蛰喸儾坏?
高了客戶端的CPU,redis的QPS也會被拉高,如果這樣空輪詢的客戶端有幾十來個,Redis的慢查詢可能會顯著增多。
   
解決辦法:
1.1) 使用sleep來解決這個問題,讓線程睡一會
2.1) 使用blpop/brpop,阻塞讀在隊列沒有數(shù)據(jù)的時候,會立即進入休眠狀態(tài),一旦數(shù)據(jù)到來,則立刻醒過來。消息的延遲幾乎為零。

空閑連接自動斷開怎么辦?
解決辦法:
   如果線程一直阻塞在哪里,Redis的客戶端連接就成了閑置連接,閑置過久,服務(wù)器一般會主動斷開連接,減少閑置資源占用。這個時候
blpop/brpop會拋出異常來。所以編寫客戶端消費者的時候要小心,注意捕獲異常,還要重試。
   
2) 延時隊列
   延時隊列可以通過Redis的zset(有序列表)來實現(xiàn)。我們將消息序列化成一個字符串作為zset的value,這個消息的處理時間作為score,
然后用多個線程輪詢zset獲取到期的任務(wù)進行處理,多個線程是為了保障可用性,萬一掛了一個線程還有其它線程可以繼續(xù)處理。因為有
多個線程,所以需要考慮并發(fā)爭搶任務(wù),確保任務(wù)不能被多次執(zhí)行。

如何進行Redis深度分析

//Redis實現(xiàn)延遲隊列
public class RedisDelayingQueue<T> {

    static class TaskItem<T> {
        public String id;
        public T msg;
    }

    private Type TaskType = new TypeReference<TaskItem<T>>(){}.getType();

    private Jedis jedis;

    private String queueKey;

    public RedisDelayingQueue(Jedis jedis, String queueKey){
        this.jedis = jedis;
        this.queueKey = queueKey;
    }

    public void delay(T msg){
        TaskItem taskItem = new TaskItem();
        taskItem.id = UUID.randomUUID().toString();
        taskItem.msg = msg;
        String s = JSON.toJSONString(taskItem);
        jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s);
    }

    public void loop(){
        while (!Thread.interrupted()){
            //只取一條數(shù)據(jù)
            Set values = jedis.zrangeByScore(queueKey,0,System.currentTimeMillis(),0,1);
            
            if(values.isEmpty()){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    break;
                }
                continue;
            }

            String s = (String) values.iterator().next();
            //zrem用于移除有序集中一個或多個成員
            if(jedis.zrem(queueKey, s) > 0){
                TaskItem task = JSON.parseObject(s, TaskType);
                this.handleMsg(task.msg);
            }
        }
    }

    private void handleMsg(Object msg) {
        System.out.println(msg);
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        RedisDelayingQueue queue = new RedisDelayingQueue(jedis, "test-queue");

        Thread producer = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++){
                    queue.delay("lwh" + i);
                }
            }
        };

        Thread consumer = new Thread(){
            @Override
            public void run() {
                queue.loop();
            }
        };

        producer.start();
        consumer.start();

        try {
            producer.join();
            Thread.sleep(6000);
            consumer.interrupt();
            consumer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3、節(jié)衣縮食: 位圖

1) 位圖 
   位圖不是特殊的數(shù)據(jù)結(jié)構(gòu),它的內(nèi)容其實就是普通的字符串,也就是 byte 數(shù)組。我們可以使用普通的 get/set 直接獲取和設(shè)置整個位
圖的內(nèi)容,也可以使用位圖操作 getbit/setbit等將 byte 數(shù)組看成「位數(shù)組」來處理。
   redis位圖可以實現(xiàn)零存整取、零存零取等?!噶愦妗咕褪鞘褂?nbsp;setbit 對位值進行逐個設(shè)置,「整存」就是使用字符串一次性填充所有
位數(shù)組,覆蓋掉舊值。
   命令形如
   1) setbit s 1 1
   2) getbit s
   3) get s
   4) set s h
   
2) 統(tǒng)計和查找
   Redis提供了位圖統(tǒng)計指令bitcount和位圖查找指令bitpos,bitcount用來統(tǒng)計指定位置范圍內(nèi)1的個數(shù),bitpos用來查找指定范圍內(nèi)出現(xiàn)
的第一個0或1。比如我們可以通過bitcount統(tǒng)計用戶一共簽到了多少天,通過bitpos指令查找用戶從哪一天開始第一次簽到。如果指定了范圍
參數(shù)[start,end],就可以統(tǒng)計在某個時間范圍內(nèi)用戶簽到了多少天,用戶自某天以后的哪天開始簽到。
   
3) bitfield

4、四兩撥千斤: HyperLogLog

1) HyperLogLog使用
   統(tǒng)計PV:每個網(wǎng)頁一個獨立的Redis計數(shù)器
   統(tǒng)計UV?
解決辦法:
1.1) set去重,頁面訪問量大的情況下,耗費太多存儲空間
1.2) 使用HyperLogLog,不精確去重,標準誤差0.81%
    
   HyperLogLog提供了兩個指令pfadd和pfcount,根據(jù)字面意義很好理解,一個是增加計數(shù),一個是獲取計數(shù)。pfadd用法和set集合的sadd
是一樣的,來一個用戶ID,就將用戶ID塞進去就是。pfcount和scard用法是一樣的,直接獲取計數(shù)值。
   pfadd test-log user1
   pfadd test-log user2
   
   pfcount test-log
   
   HyperLogLog 除了上面的pfadd和pfcount之外,還提供了第三個指令pfmerge,用于將多個pf計數(shù)值累加在一起形成一個新的pf值。比如在
網(wǎng)站中我們有兩個內(nèi)容差不多的頁面,運營說需要這兩個頁面的數(shù)據(jù)進行合并。其中頁面的UV訪問量也需要合并,那這個時候pfmerge就可以派
上用場了。

5、層巒疊嶂:布隆過濾器

1) 布隆過濾器
   講個使用場景,比如我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內(nèi)容,它每次推薦時要去重,去掉那些已經(jīng)看過的內(nèi)容。
問題來了,新聞客戶端推薦系統(tǒng)如何實現(xiàn)推送去重的?
   當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。
   
基本指令:布隆過濾器有二個基本指令,bf.add添加元素,bf.exists查詢元素是否存在,它的用法和set集合的sadd和sismember差不多。
注意bf.add只能一次添加一個元素,如果想要一次添加多個,就需要用到bf.madd指令。同樣如果需要一次查詢多個元素是否存在,就需要用到
bf.mexists指令。
   
   bf.add test-filter user1
   bf.add test-filter user2
   
   bf.exists test-filter user1
   
   Redis其實還提供了自定義參數(shù)的布隆過濾器,需要我們在add之前使用bf.reserve指令顯式創(chuàng)建。如果對應(yīng)的 key已經(jīng)存在,bf.reserve
會報錯。bf.reserve有三個參數(shù),分別是key,error_rate和initial_size。錯誤率越低,需要的空間越大。initial_size參數(shù)表示預(yù)計放入
的元素數(shù)量,當實際數(shù)量超出這個數(shù)值時,誤判率會上升。

2) 布隆過濾器的原理
   每個布隆過濾器對應(yīng)到Redis的數(shù)據(jù)結(jié)構(gòu)里面就是一個大型的位數(shù)組和幾個不一樣的無偏hash函數(shù)。所謂無偏就是能夠把元素的hash值算得
比較均勻。向布隆過濾器中添加key時,會使用多個hash函數(shù)對 key進行hash算得一個整數(shù)索引值然后對位數(shù)組長度進行取模運算得到一個位
置,每個hash函數(shù)都會算得一個不同的位置。再把位數(shù)組的這幾個位置都置為1就完成了add操作。

3) 布隆過濾器的其他應(yīng)用
   在爬蟲系統(tǒng)中,我們需要對URL進行去重,已經(jīng)爬過的網(wǎng)頁就可以不用爬了。但是URL太多了,幾千萬幾個億,如果用一個集合裝下這些URL
地址那是非常浪費空間的。這時候就可以考慮使用布隆過濾器。它可以大幅降低去重存儲消耗,只不過也會使得爬蟲系統(tǒng)錯過少量的頁面。布
隆過濾器在NoSQL數(shù)據(jù)庫領(lǐng)域使用非常廣泛,我們平時用到的HBase、Cassandra還有LevelDB、RocksDB內(nèi)部都有布隆過濾器結(jié)構(gòu),布隆過濾器
可以顯著降低數(shù)據(jù)庫的IO請求數(shù)量。當用戶來查詢某個row時,可以先通過內(nèi)存中的布隆過濾器過濾掉大量不存在的row請求,然后再去磁盤進
行查詢。
   郵箱系統(tǒng)的垃圾郵件過濾功能也普遍用到了布隆過濾器,因為用了這個過濾器,所以平時也會遇到某些正常的郵件被放進了垃圾郵件目錄中,
這個就是誤判所致,概率很低。

6、斷尾求生:簡單限流

   除了控制流量,限流還有一個應(yīng)用目的是用于控制用戶行為,避免垃圾請求。比如在UGC社區(qū),用戶的發(fā)帖、回復(fù)、點贊等行為都要嚴格受
控,一般要嚴格限定某行為在規(guī)定時間內(nèi)允許的次數(shù),超過了次數(shù)那就是非法行為。對非法行為,業(yè)務(wù)必須規(guī)定適當?shù)膽吞幉呗浴?/pre>

如何進行Redis深度分析

//這個限流需求中存在一個滑動時間窗口,想想zset數(shù)據(jù)結(jié)構(gòu)的score值,是不是可以通過score來圈出這個時間窗口來。而且我們只需要
//保留這個時間窗口,窗口之外的數(shù)據(jù)都可以砍掉。那這個zset的value填什么比較合適呢?它只需要保證唯一性即可,用uuid會比較浪費
//空間,那就改用毫秒時間戳吧。

//但這種方案也有缺點,因為它要記錄時間窗口內(nèi)所有的行為記錄,如果這個量很大,比如限定60s內(nèi)操作不得超過100w次這樣的參數(shù),它
//是不適合做這樣的限流的,因為會消耗大量的存儲空間。
public class SimpleRateLimiter {

    private final Jedis jedis;

    public SimpleRateLimiter(Jedis jedis){
        this.jedis = jedis;
    }

    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) throws IOException {
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();

        Pipeline pipeline = jedis.pipelined();
        //開啟一個事務(wù)
        pipeline.multi();
        //value和score都用毫秒時間戳
        pipeline.zadd(key, nowTs, "" + nowTs);
        //移除時間窗口之外的行為記錄,剩下的都是時間窗口內(nèi)的
        pipeline.zremrangeByScore(key, 0, nowTs - period * 1000);
        //獲得[nowTs - period * 1000, nowTs]的key的數(shù)量
        Response<Long> count = pipeline.zcard(key);
        //每次設(shè)置都更新key的過期時間
        pipeline.expire(key, period);

        //在事務(wù)中執(zhí)行上述命令
        pipeline.exec();
        pipeline.close();

        return count.get() <= maxCount;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Jedis jedis=new Jedis("localhost",6379);
        SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
        for (int i = 0; i < 20; i++) {
            //每個用戶在1秒內(nèi)最多能做五次動作
            System.out.println(limiter.isActionAllowed("lwh","reply",1,5));
        }
    }
}

7、一毛不拔:漏斗限流

   Redis4.0提供了一個限流Redis模塊,它叫redis-cell。該模塊也使用了漏斗算法,并提供了原子的限流指令。有了這個模塊,限流問題就
非常簡單了。
   
   cl.throttle lwh:reply 15 30 60 1
       
   上面這個指令的意思是允許「用戶lwh回復(fù)行為」的頻率為每 60s 最多 30 次(漏水速率),漏斗的初始容量為 15,也就是說一開始可以連
續(xù)回復(fù) 15 個帖子,然后才開始受漏水速率的影響。

8、近水樓臺:GeoHash

   Redis在3.2版本以后增加了地理位置GEO模塊,意味著我們可以使用Redis來實現(xiàn)摩拜單車「附近的 Mobike」、美團和餓了么「附近的
餐館」這樣的功能了。
   業(yè)界比較通用的地理位置距離排序算法是GeoHash算法,Redis也使用GeoHash算法。GeoHash算法將二維的經(jīng)緯度數(shù)據(jù)映射到一維的整數(shù),
這樣所有的元素都將在掛載到一條線上,距離靠近的二維坐標映射到一維后的點之間距離也會很接近。當我們想要計算「附近的人時」,首先
將目標位置映射到這條線上,然后在這個一維的線上獲取附近的點就行了。
   在Redis里面,經(jīng)緯度使用52位的整數(shù)進行編碼,放進了zset里面,zset的value是元素的key,score是GeoHash的52位整數(shù)值。zset的
score雖然是浮點數(shù),但是對于52位的整數(shù)值,它可以無損存儲。在使用Redis進行Geo查詢時,我們要時刻想到它的內(nèi)部結(jié)構(gòu)實際上只是一
個zset(skiplist)。通過zset的score 排序就可以得到坐標附近的其它元素 (實際情況要復(fù)雜一些,不過這樣理解足夠了),通過將score
還原成坐標值就可以得到元素的原始坐標。
   
   1) 添加,geoadd指令攜帶集合名稱以及多個經(jīng)緯度名稱三元組
       geoadd company 116.48105 39.996794 juejin
       geoadd company 116.514203 39.905409 ireader
   
   2) 距離,geodist指令可以用來計算兩個元素之間的距離
       geodist company juejin ireader km
       
   3) 獲取元素位置,geopos指令可以獲取集合中任意元素的經(jīng)緯度坐標
	   geopos company juejin
	   
   我們觀察到獲取的經(jīng)緯度坐標和geoadd進去的坐標有輕微的誤差,原因是geohash對二維坐標進行的一維映射是有損的,通過映射再還原
回來的值會出現(xiàn)較小的差別。對于「附近的人」這種功能來說,這點誤差根本不是事。
   
   4) 附近的公司,georadiusbymember指令是最為關(guān)鍵的指令,它可以用來查詢指定元素附近的其它元素
       //范圍20公里以內(nèi)最多3個元素按距離正排,它不會排除自身
   	   georadiusbymember company ireader 20 km count 3 asc
   	   
   	   //三個可選參數(shù) withcoord withdist withhash 用來攜帶附加參數(shù)
   	   //withdist可以顯示距離
   	   //withcoord顯示坐標
   	   georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc

   5) Redis還提供了根據(jù)坐標值來查詢附近的元素,這個指令更加有用,它可以根據(jù)用戶的定位來計算「附近的車」,「附近的餐館」等。
它的參數(shù)和georadiusbymember基本一致,除了將目標元素改成經(jīng)緯度坐標值。
	   georadius company 116.514202 39.905409 20 km withdist count 3 asc   
	
注意事項:
   在一個地圖應(yīng)用中,車的數(shù)據(jù)、餐館的數(shù)據(jù)、人的數(shù)據(jù)可能會有百萬千萬條,如果使用Redis的Geo數(shù)據(jù)結(jié)構(gòu),它們將全部放在一個zset
集合中。在Redis的集群環(huán)境中,集合可能會從一個節(jié)點遷移到另一個節(jié)點,如果單個key的數(shù)據(jù)過大,會對集群的遷移工作造成較大的影響,
在集群環(huán)境中單個key對應(yīng)的數(shù)據(jù)量不宜超過1M,否則會導(dǎo)致集群遷移出現(xiàn)
卡頓現(xiàn)象,影響線上服務(wù)的正常運行。

   所以,這里建議Geo的數(shù)據(jù)使用單獨的Redis實例部署,不使用集群環(huán)境。如果數(shù)據(jù)量過億甚至更大,就需要對Geo數(shù)據(jù)進行拆分,按國家
拆分、按省拆分,按市拆分,在人口特大城市甚至可以按區(qū)拆分。這樣就可以顯著降低單個zset集合的大小。

9、大海撈針:Scan

   在平時線上Redis維護工作中,有時候需要從Redis實例成千上萬的key中找出特定前綴的key列表來手動處理數(shù)據(jù),可能是修改它的值,也
可能是刪除key。這里就有一個問題,如何從海量的key中找出滿足特定前綴的key列表來?
   Redis提供了一個簡單暴力的指令keys用來列出所有滿足特定正則字符串規(guī)則的key。缺點:沒有offset、limit參數(shù),事件復(fù)雜度O(n),key
過多時會導(dǎo)致卡頓
   
   Redis為了解決這個問題,它在2.8版本中加入了大海撈針的指令——scan。scan相比keys具備有以下特點:
1)、復(fù)雜度雖然也是 O(n),但是它是通過游標分步進行的,不會阻塞線程
2)、提供limit參數(shù),可以控制每次返回結(jié)果的最大條數(shù),limit只是一個hint,返回的結(jié)果可多可少
3)、同keys一樣,它也提供模式匹配功能
4)、服務(wù)器不需要為游標保存狀態(tài),游標的唯一狀態(tài)就是scan返回給客戶端的游標整數(shù)
5)、返回的結(jié)果可能會有重復(fù),需要客戶端去重復(fù),這點非常重要
6)、遍歷的過程中如果有數(shù)據(jù)修改,改動后的數(shù)據(jù)能不能遍歷到是不確定的
7)、單次返回的結(jié)果是空的并不意味著遍歷結(jié)束,而要看返回的游標值是否為零

   scan提供了三個參數(shù),第一個是cursor整數(shù)值,第二個是key的正則模式,第三個是遍歷的limit hint。第一次遍歷時,cursor值為0,然
后將返回結(jié)果中第一個整數(shù)值作為下一次遍歷的cursor。一直遍歷到返回的cursor值為0時結(jié)束。
    scan 0 match key99* count 1000     --> 返回cursor13796作為下次遍歷的cursor
    scan 13976 match key99* count 1000

    scan 指令返回的游標就是第一維數(shù)組的位置索引,我們將這個位置索引稱為槽 (slot)。如果不考慮字典的擴容縮容,直接按數(shù)組下標挨
個遍歷就行了。limit參數(shù)就表示需要遍歷的槽位數(shù),之所以返回的結(jié)果可能多可能少,是因為不是所有的槽位上都會掛接鏈表,有些槽位可能
是空的,還有些槽位上掛接的鏈表上的元素可能會有多個。每一次遍歷都會將limit數(shù)量的槽位上掛接的所有鏈表元素進行模式匹配過濾后,一
次性返回給客戶端。
    
    scan 的遍歷順序非常特別。它不是從第一維數(shù)組的第0位一直遍歷到末尾,而是采用了高位進位加法來遍歷。之所以使用這樣特殊的方式
進行遍歷,是考慮到字典的擴容和縮容時避免槽位的遍歷重復(fù)和遺漏。
    
    在平時的業(yè)務(wù)開發(fā)中,要盡量避免大key的產(chǎn)生。有時候會因為業(yè)務(wù)人員使用不當,在Redis實例中會形成很大的對象,比如一個很大的
hash,一個很大的zset這都是經(jīng)常出現(xiàn)的。這樣的對象對Redis的集群數(shù)據(jù)遷移帶來了很大的問題,因為在集群環(huán)境下,如果某個key太大,
會數(shù)據(jù)導(dǎo)致遷移卡頓。另外在內(nèi)存分配上,如果一個key太大,那么當它需要擴容時,會一次性申請更大的一塊內(nèi)存,這也會導(dǎo)致卡頓。如果
這個大key被刪除,內(nèi)存會一次性回收,卡頓現(xiàn)象會再一次產(chǎn)生。
    不過Redis官方已經(jīng)在redis-cli指令中提供了大key掃描功能。第二條指令每隔 100 條 scan 指令就會休眠 0.1s,ops 就不會劇烈抬升,
但是掃描的時間會變長。
    redis-cli -h 127.0.0.1 -p 7001 –-bigkeys
    redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1

如何進行Redis深度分析

如何進行Redis深度分析

10、鞭辟入里:線程IO模型

1) 定時任務(wù)
   服務(wù)器處理要響應(yīng)IO事件外,還要處理其它事情。比如定時任務(wù)就是非常重要的一件事。如果線程阻塞在select系統(tǒng)調(diào)用上,定時任務(wù)將無
法得到準時調(diào)度。那Redis是如何解決這個問題的呢?
   Redis的定時任務(wù)會記錄在一個稱為最小堆的數(shù)據(jù)結(jié)構(gòu)中。這個堆中,最快要執(zhí)行的任務(wù)排在堆的最上方。在每個循環(huán)周期,Redis都會將最
小堆里面已經(jīng)到點的任務(wù)立即進行處理。處理完畢后,將最快要執(zhí)行的任務(wù)還需要的時間記錄下來,這個時間就是select系統(tǒng)調(diào)用的timeout參
數(shù)。因為Redis知道未來timeout時間內(nèi),沒有其它定時任務(wù)需要處理,所以可以安心睡眠timeout的時間。
   Nginx 和 Node 的事件處理原理和 Redis 也是類似的

11、交頭接耳:通信協(xié)議

   Redis的作者認為數(shù)據(jù)庫系統(tǒng)的瓶頸一般不在于網(wǎng)絡(luò)流量,而是數(shù)據(jù)庫自身內(nèi)部邏輯處理上。所以即使Redis使用了浪費流量的文本協(xié)議,
依然可以取得極高的訪問性能。
   RESP(Redis Serialization Protocol). RESP是Redis序列化協(xié)議的簡寫。它是一種直觀的文本協(xié)議,優(yōu)勢在于實現(xiàn)異常簡單,解析性能
極好。

12、未雨綢繆:持久化

   當父進程對其中一個頁面的數(shù)據(jù)進行修改時,會將被共享的頁面復(fù)制一份分離出來,然后對這個復(fù)制的頁面進行修改。這時子進程相應(yīng)的
頁面是沒有變化的,還是進程產(chǎn)生時那一瞬間的數(shù)據(jù)。子進程因為數(shù)據(jù)沒有變化,它能看到的內(nèi)存里的數(shù)據(jù)在進程產(chǎn)生的一瞬間就凝固了,
再也不會改變,這也是為什么 Redis 的持久化叫「快照」的原因。接下來子進程就可以非常安心的遍歷數(shù)據(jù)了進行序列化寫磁盤了。
   
   Redis4.0混合持久化
   重啟Redis時,我們很少使用rdb來恢復(fù)內(nèi)存狀態(tài),因為會丟失大量數(shù)據(jù)。我們通常使用AOF日志重放,但是重放AOF日志性能相對rdb來說要
慢很多,這樣在Redis實例很大的情況下,啟動需要花費很長的時間。Redis4.0為了解決這個問題,帶來了一個新的持久化選項——混合持久化。
將rdb文件的內(nèi)容和增量的AOF日志文件存在一起。這里的AOF日志不再是全量的日志,而是自持久化開始到持久化結(jié)束的這段時間發(fā)生的增量
AOF日志,通常這部分AOF日志很小。
   于是在Redis重啟的時候,可以先加載rdb的內(nèi)容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重啟效率因此大幅得
到提升。

13、開源節(jié)流:小對象壓縮

1) 小對象壓縮
   如果Redis內(nèi)部管理的集合數(shù)據(jù)結(jié)構(gòu)很小,它會使用緊湊存儲形式壓縮存儲。如果它存儲的是hash結(jié)構(gòu),那么key和 value會作為兩個entry
相鄰存在一起。如果它存儲的是zset,那么value和score會作為兩個entry相鄰存在一起。
   存儲界限 當集合對象的元素不斷增加,或者某個value值過大,這種小對象存儲也會被升級為標準結(jié)構(gòu)。
   
2) 內(nèi)存回收機制
   Redis并不總是可以將空閑內(nèi)存立即歸還給操作系統(tǒng)。
   如果當前Redis內(nèi)存有10G,當你刪除了1GB的key后,再去觀察內(nèi)存,你會發(fā)現(xiàn)內(nèi)存變化不會太大。原因是操作系統(tǒng)回收內(nèi)存是以頁為單位,
如果這個頁上只要有一個key還在使用,那么它就不能被回收。Redis雖然刪除了1GB的key,但是這些key分散到了很多頁面中,每個頁面都還有
其它key存在,這就導(dǎo)致了內(nèi)存不會立即被回收。
   不過,如果你執(zhí)行flushdb,然后再觀察內(nèi)存會發(fā)現(xiàn)內(nèi)存確實被回收了。原因是所有的key都干掉了,大部分之前使用的頁面都完全干凈了,
會立即被操作系統(tǒng)回收。
   Redis雖然無法保證立即回收已經(jīng)刪除的key的內(nèi)存,但是它會重用那些尚未回收的空閑內(nèi)存。這就好比電影院里雖然人走了,但是座位還在,
下一波觀眾來了,直接坐就行。而操作系統(tǒng)回收內(nèi)存就好比把座位都給搬走了。這個比喻是不是很6?

14、有備無患:主從同步

1) CAP理論
   C:Consistent,一致性
   A:Availbility,可用性
   P:Partition tolerance,分區(qū)容忍性
   
   分布式系統(tǒng)的節(jié)點往往都是分布在不同的機器上進行網(wǎng)絡(luò)隔離開的,這意味著必然會有網(wǎng)絡(luò)斷開的風險,這個網(wǎng)絡(luò)斷開的場景的專業(yè)詞匯
叫著「網(wǎng)絡(luò)分區(qū)」。
   在網(wǎng)絡(luò)分區(qū)發(fā)生時,兩個分布式節(jié)點之間無法進行通信,我們對一個節(jié)點進行的修改操作將無法同步到另外一個節(jié)點,所以數(shù)據(jù)的「一致
性」將無法滿足,因為兩個分布式節(jié)點的數(shù)據(jù)不再保持一致。除非我們犧牲「可用性」,也就是暫停分布式節(jié)點服務(wù),在網(wǎng)絡(luò)分區(qū)發(fā)生時,不
再提供修改數(shù)據(jù)的功能,直到網(wǎng)絡(luò)狀況完全恢復(fù)正常再繼續(xù)對外提供服務(wù)。
   一句話概括CAP原理就是——網(wǎng)絡(luò)分區(qū)發(fā)生時,一致性和可用性兩難全。

看完上述內(nèi)容,你們對如何進行Redis深度分析有進一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI