真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

如何進(jìn)行Redis深度分析

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

創(chuàng)新互聯(lián)公司專注于惠來(lái)企業(yè)網(wǎng)站建設(shè),成都響應(yīng)式網(wǎng)站建設(shè)公司,購(gòu)物商城網(wǎng)站建設(shè)。惠來(lái)網(wǎng)站建設(shè)公司,為惠來(lái)等地區(qū)提供建站服務(wù)。全流程按需制作網(wǎng)站,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務(wù)

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

1) 當(dāng)字符串長(zhǎng)度小于1M時(shí),擴(kuò)容都是加倍現(xiàn)有的空間,如果超過(guò)1M,擴(kuò)容時(shí)一次只會(huì)多擴(kuò)1M的空間。需要注意的是字符串最大長(zhǎng)度為512M。

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

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

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

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

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

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

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

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

3.3) 將請(qǐng)求轉(zhuǎn)移至延時(shí)隊(duì)列,過(guò)一會(huì)再試
     這種方式比較適合異步消息處理,將當(dāng)前沖突的請(qǐng)求扔到另一個(gè)隊(duì)列延后處理以避開沖突。

2、緩兵之計(jì):延時(shí)隊(duì)列

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

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

如何進(jìn)行Redis深度分析

//Redis實(shí)現(xiàn)延遲隊(duì)列
public class RedisDelayingQueue {

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

    private Type TaskType = new TypeReference>(){}.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用于移除有序集中一個(gè)或多個(gè)成員
            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)容其實(shí)就是普通的字符串,也就是 byte 數(shù)組。我們可以使用普通的 get/set 直接獲取和設(shè)置整個(gè)位
圖的內(nèi)容,也可以使用位圖操作 getbit/setbit等將 byte 數(shù)組看成「位數(shù)組」來(lái)處理。
   redis位圖可以實(shí)現(xiàn)零存整取、零存零取等?!噶愦妗咕褪鞘褂?nbsp;setbit 對(duì)位值進(jìn)行逐個(gè)設(shè)置,「整存」就是使用字符串一次性填充所有
位數(shù)組,覆蓋掉舊值。
   命令形如
   1) setbit s 1 1
   2) getbit s
   3) get s
   4) set s h
   
2) 統(tǒng)計(jì)和查找
   Redis提供了位圖統(tǒng)計(jì)指令bitcount和位圖查找指令bitpos,bitcount用來(lái)統(tǒng)計(jì)指定位置范圍內(nèi)1的個(gè)數(shù),bitpos用來(lái)查找指定范圍內(nèi)出現(xiàn)
的第一個(gè)0或1。比如我們可以通過(guò)bitcount統(tǒng)計(jì)用戶一共簽到了多少天,通過(guò)bitpos指令查找用戶從哪一天開始第一次簽到。如果指定了范圍
參數(shù)[start,end],就可以統(tǒng)計(jì)在某個(gè)時(shí)間范圍內(nèi)用戶簽到了多少天,用戶自某天以后的哪天開始簽到。
   
3) bitfield

4、四兩撥千斤: HyperLogLog

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

5、層巒疊嶂:布隆過(guò)濾器

1) 布隆過(guò)濾器
   講個(gè)使用場(chǎng)景,比如我們?cè)谑褂眯侣効蛻舳丝葱侣剷r(shí),它會(huì)給我們不停地推薦新的內(nèi)容,它每次推薦時(shí)要去重,去掉那些已經(jīng)看過(guò)的內(nèi)容。
問(wèn)題來(lái)了,新聞客戶端推薦系統(tǒng)如何實(shí)現(xiàn)推送去重的?
   當(dāng)布隆過(guò)濾器說(shuō)某個(gè)值存在時(shí),這個(gè)值可能不存在;當(dāng)它說(shuō)不存在時(shí),那就肯定不存在。
   
基本指令:布隆過(guò)濾器有二個(gè)基本指令,bf.add添加元素,bf.exists查詢?cè)厥欠翊嬖冢挠梅ê蛃et集合的sadd和sismember差不多。
注意bf.add只能一次添加一個(gè)元素,如果想要一次添加多個(gè),就需要用到bf.madd指令。同樣如果需要一次查詢多個(gè)元素是否存在,就需要用到
bf.mexists指令。
   
   bf.add test-filter user1
   bf.add test-filter user2
   
   bf.exists test-filter user1
   
   Redis其實(shí)還提供了自定義參數(shù)的布隆過(guò)濾器,需要我們?cè)赼dd之前使用bf.reserve指令顯式創(chuàng)建。如果對(duì)應(yīng)的 key已經(jīng)存在,bf.reserve
會(huì)報(bào)錯(cuò)。bf.reserve有三個(gè)參數(shù),分別是key,error_rate和initial_size。錯(cuò)誤率越低,需要的空間越大。initial_size參數(shù)表示預(yù)計(jì)放入
的元素?cái)?shù)量,當(dāng)實(shí)際數(shù)量超出這個(gè)數(shù)值時(shí),誤判率會(huì)上升。

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

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

6、斷尾求生:簡(jiǎn)單限流

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

如何進(jìn)行Redis深度分析

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

//但這種方案也有缺點(diǎn),因?yàn)樗涗洉r(shí)間窗口內(nèi)所有的行為記錄,如果這個(gè)量很大,比如限定60s內(nèi)操作不得超過(guò)100w次這樣的參數(shù),它
//是不適合做這樣的限流的,因?yàn)闀?huì)消耗大量的存儲(chǔ)空間。
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();
        //開啟一個(gè)事務(wù)
        pipeline.multi();
        //value和score都用毫秒時(shí)間戳
        pipeline.zadd(key, nowTs, "" + nowTs);
        //移除時(shí)間窗口之外的行為記錄,剩下的都是時(shí)間窗口內(nèi)的
        pipeline.zremrangeByScore(key, 0, nowTs - period * 1000);
        //獲得[nowTs - period * 1000, nowTs]的key的數(shù)量
        Response count = pipeline.zcard(key);
        //每次設(shè)置都更新key的過(guò)期時(shí)間
        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++) {
            //每個(gè)用戶在1秒內(nèi)最多能做五次動(dòng)作
            System.out.println(limiter.isActionAllowed("lwh","reply",1,5));
        }
    }
}

7、一毛不拔:漏斗限流

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

8、近水樓臺(tái):GeoHash

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

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

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

9、大海撈針:Scan

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

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

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

如何進(jìn)行Redis深度分析

如何進(jìn)行Redis深度分析

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

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

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

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

12、未雨綢繆:持久化

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

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

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

14、有備無(wú)患:主從同步

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

看完上述內(nèi)容,你們對(duì)如何進(jìn)行Redis深度分析有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。


新聞標(biāo)題:如何進(jìn)行Redis深度分析
本文網(wǎng)址:http://weahome.cn/article/jjgeop.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部