一文搞懂redis

Redis概述

介紹

  • redis是一個(gè)開源的key-value存儲(chǔ)系統(tǒng)
  • 和Memcached類似,它支持的value類型相對(duì)更多,包括String(字符串)、List(鏈表)、Hash(哈希)、Set(無需不重復(fù)集合)、ZSet(sorted set有一定順序的集合)
  • 與memecached一樣,redis數(shù)據(jù)都緩存在內(nèi)存中
  • 區(qū)別是redis會(huì)周期性的把更新的數(shù)據(jù)寫入到磁盤或者把修改操作追加到記錄文件
  • 并且再次基礎(chǔ)上實(shí)現(xiàn)了master-slaver(主從同步)

應(yīng)用場(chǎng)景

  • 配合關(guān)系型數(shù)據(jù)庫(kù)做告訴緩存,減少數(shù)據(jù)庫(kù)IO

  • 分布式架構(gòu),做session共享

  • 多樣的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)持久化數(shù)據(jù)

相關(guān)技術(shù)

  • redis是單線程 + 多路IO復(fù)用的技術(shù)
  • 與memcache三點(diǎn)不同:支持多種數(shù)據(jù)類型,支持持久化,單線程+ 多路IO復(fù)用
  • 多路IO復(fù)用是一種同步IO模型,實(shí)現(xiàn)一個(gè)線程可以處理多個(gè)IO請(qǐng)求。也就是在redis單線程的基礎(chǔ)上,實(shí)現(xiàn)了單線程redis同時(shí)處理多個(gè)IO請(qǐng)求。多路指的值多個(gè)網(wǎng)絡(luò)連接,復(fù)用是指的是復(fù)用一個(gè)redis線程。其實(shí)這里也不是同時(shí)處理,只是處理請(qǐng)求的耗時(shí)特別短,所以時(shí)間拉長(zhǎng)看的話,就是一個(gè)線程處理了多個(gè)IO請(qǐng)求。比如說redis實(shí)現(xiàn)了1s可以對(duì)11萬次。

redis的數(shù)據(jù)類型

String(字符串)

  • 概述
    • String是最基本的數(shù)據(jù)類型,可以理解為和memcached一樣,一個(gè)key對(duì)應(yīng)一個(gè)value。
    • String類型是二進(jìn)制安全的。意味著redis的String類型可以包含任何數(shù)據(jù)。比如jpg圖片和序列化對(duì)象。
    • String是redis最基本的數(shù)據(jù)類型,一個(gè)字符串的value最大可以是512M。
  • 數(shù)據(jù)結(jié)構(gòu)
    • String的數(shù)據(jù)結(jié)構(gòu)是動(dòng)態(tài)字符串, 是可以修改的字符串,內(nèi)部結(jié)構(gòu)類似Java的ArrayList,采用預(yù)分配冗余空間的方式減少內(nèi)存的頻繁分配。

List(列表)

  • 概述
    • 單鍵多值,redis的list列表是最簡(jiǎn)單的字符串列表,按照插入順序排序。可以添加一個(gè)元素在列表的頭部或者尾部。
    • 它的底層其實(shí)是一個(gè)雙向鏈表,對(duì)兩端的操作性能很高,通過索引下標(biāo)操作中間節(jié)點(diǎn)的性能較差。
  • 數(shù)據(jù)結(jié)構(gòu)
    • List的數(shù)據(jù)結(jié)構(gòu)為快速鏈表,也就是quickList。
    • 首先在鏈表元素較少的情況下使用一段連續(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)空間。
    • redis將鏈表和ziplist結(jié)合起來組成了quicklist。也就是將多個(gè)ziplist使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會(huì)浪費(fèi)太大的空間。

Set(無須不重復(fù)集合)

  • 概述
    • redis的set集合對(duì)外提供的功能和list類似,是一個(gè)列表的功能,特殊之處是set可以自動(dòng)排重,當(dāng)你需要存儲(chǔ)一個(gè)列表數(shù)據(jù),又不希望有重復(fù)數(shù)據(jù)的時(shí)候,set集合是一個(gè)很好的選擇。并且set集合提供了判斷某個(gè)成員是否在set集合內(nèi)的重要接口,這也是list所不能提供的。
    • redis的set是String類型的無序集合。它底層是一個(gè)value為null的哈希表,所以添加、查找、刪除的時(shí)間復(fù)雜度都是o(1)
    • 一個(gè)算法,隨著數(shù)據(jù)的增加,如果時(shí)間復(fù)雜度是o(1),查找數(shù)據(jù)的時(shí)間不變。
  • 數(shù)據(jù)結(jié)構(gòu)
    • set的數(shù)據(jù)結(jié)構(gòu)是dict字典,字典是用哈希表實(shí)現(xiàn)的。
    • Java中的HashSet內(nèi)部也是會(huì)用HashMap實(shí)現(xiàn)的,只不過所有的value都指向同一個(gè)對(duì)象。redis的set也是一樣的,它的內(nèi)部也是hash結(jié)構(gòu),所有的數(shù)據(jù)都指向同一個(gè)內(nèi)部值。(意思是HashSet所有的key都有一個(gè)默認(rèn)的value,你是HashSet是不允許key重復(fù)的,所以只允許有一個(gè)為null的key)

Hash(哈希)

  • 概述
    • redis的hash是一個(gè)鍵值對(duì)集合(類似Java中的Map<String, Object>)。
    • redis的hash是一個(gè)String類型的key和value的映射表,hash特別適合存儲(chǔ)對(duì)象。
  • 數(shù)據(jù)結(jié)構(gòu)
    • Hash對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)是兩種,一種是ziplist(壓縮列表),hashtable(哈希表)。當(dāng)key-value長(zhǎng)度較短且個(gè)數(shù)較少的時(shí)候,使用ziplist,否則使用hashtable。

Zset(Sorted set)

  • 概述
    • redis的zset集合和set集合非常的相似,是一個(gè)沒有重復(fù)元素的字符串集合。
    • 不同之處是zset每個(gè)成員都關(guān)聯(lián)了一個(gè)score(評(píng)分),這個(gè)score(評(píng)分)是被用來按照從最低到最高排序的標(biāo)準(zhǔn),集合中的成員是唯一的,但是score是可以重復(fù)的。
    • 因?yàn)樵厥怯行虻模砸部梢院芸旄鶕?jù)score或者position來獲取一個(gè)范圍的元素。
    • 訪問一個(gè)有序集合也是非常快的,因此你能用有序集合作為一個(gè)沒有重復(fù)成員的智能列表。
  • 數(shù)據(jù)結(jié)構(gòu)
    • zset(sorted set)是redis提供的一個(gè)非常特別的數(shù)據(jù)結(jié)構(gòu),一方面他等價(jià)于Java的Map<String, double>,可以給每一個(gè)成員賦值一個(gè)權(quán)重score,另一方面它有類似treeSet,內(nèi)部元素會(huì)按照score權(quán)重進(jìn)行排序,可得到每個(gè)成員的名次,還可以通過score的范圍獲取元素列表。
    • zset使用了兩個(gè)數(shù)據(jù)結(jié)構(gòu):
      • hash,hash的作用就是關(guān)聯(lián)value和權(quán)重score,保障元素value的唯一性,可以通過value找到對(duì)應(yīng)的score值。
      • 跳躍表,跳躍表的目的在于給元素value排序,根據(jù)score的范圍獲取元素列表。

Bitmaps

  • 概述

    • Redis提供了Bitmaps這個(gè)“數(shù)據(jù)類型”可以實(shí)現(xiàn)對(duì)位的操作:
      • Bitmaps本身不是一種數(shù)據(jù)類型, 實(shí)際上它就是字符串(key-value) , 但是它可以對(duì)字符串的位進(jìn)行操作。

      • Bitmaps單獨(dú)提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一個(gè)以位為單位的數(shù)組, 數(shù)組的每個(gè)單元只能存儲(chǔ)0和1, 數(shù)組的下標(biāo)在Bitmaps中叫做偏移量。

  • Bitmaps與set對(duì)比

    • 假設(shè)網(wǎng)站有1億用戶, 每天獨(dú)立訪問的用戶有5千萬, 如果每天用集合類型和Bitmaps分別存儲(chǔ)活躍用戶可以得到表:
    set和Bitmaps存儲(chǔ)一天活躍用戶對(duì)比
    數(shù)據(jù)類型 每個(gè)用戶id占用空間 需要存儲(chǔ)的用戶量 全部?jī)?nèi)存量
    集合 64位 50000000 64位*50000000 = 400MB
    Bitmaps 1位 100000000 1位*100000000 = 12.5MB

    很明顯, 這種情況下使用Bitmaps能節(jié)省很多的內(nèi)存空間, 尤其是隨著時(shí)間推移節(jié)省的內(nèi)存還是非常可觀的。

    set和Bitmaps存儲(chǔ)獨(dú)立用戶空間對(duì)比
    數(shù)據(jù)類型 一天 一個(gè)月 一年
    集合 400MB 12GB 144GB
    Bitmaps 12.5MB 375MB 4.5GB

    但Bitmaps并不是萬金油, 假如該網(wǎng)站每天的獨(dú)立訪問用戶很少, 例如只有10萬(大量的僵尸用戶) , 那么兩者的對(duì)比如下表所示, 很顯然, 這時(shí)候使用Bitmaps就不太合適了, 因?yàn)榛旧洗蟛糠治欢际?。

    set和Bitmaps存儲(chǔ)一天活躍用戶對(duì)比(用戶比較少)
    數(shù)據(jù)類型 每個(gè)userid占用空間 需要存儲(chǔ)的用戶量 全部?jī)?nèi)存量
    集合 64位 100000 64位*100000 = 800KB
    Bitmaps 1位 100000000 1位*100000000 = 12.5MB

    HyperLogLog

    在工作當(dāng)中,我們經(jīng)常會(huì)遇到與統(tǒng)計(jì)相關(guān)的功能需求,比如統(tǒng)計(jì)網(wǎng)站PV(PageView頁(yè)面訪問量),可以使用Redis的incr、incrby輕松實(shí)現(xiàn)。但像UV(UniqueVisitor獨(dú)立訪客)、獨(dú)立IP數(shù)、搜索記錄數(shù)等需要去重和計(jì)數(shù)的問題如何解決?這種求集合中不重復(fù)元素個(gè)數(shù)的問題稱為基數(shù)問題。

    解決基數(shù)問題有很多種方案:

    1.數(shù)據(jù)存儲(chǔ)在MySQL表中,使用distinct count計(jì)算不重復(fù)個(gè)數(shù)。

    2.使用Redis提供的hash、set、bitmaps等數(shù)據(jù)結(jié)構(gòu)來處理。

    以上的方案結(jié)果精確,但隨著數(shù)據(jù)不斷增加,導(dǎo)致占用空間越來越大,對(duì)于非常大的數(shù)據(jù)集是不切實(shí)際的。能否能夠降低一定的精度來平衡存儲(chǔ)空間?Redis推出了HyperLogLog。

    • Redis HyperLogLog 是用來做基數(shù)統(tǒng)計(jì)的算法,HyperLogLog 的優(yōu)點(diǎn)是:在輸入元素的數(shù)量或者體積非常非常大時(shí),計(jì)算基數(shù)所需的空間總是固定的、并且是很小的。

    • 在 Redis 里面,每個(gè) HyperLogLog 鍵只需要花費(fèi) 12 KB 內(nèi)存,就可以計(jì)算接近 2^64 個(gè)不同元素的基數(shù)。這和計(jì)算基數(shù)時(shí),元素越多耗費(fèi)內(nèi)存就越多的集合形成鮮明對(duì)比。

    • 但是,因?yàn)?HyperLogLog 只會(huì)根據(jù)輸入元素來計(jì)算基數(shù),而不會(huì)儲(chǔ)存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個(gè)元素。

    Geospatial

    Redis 3.2 中增加了對(duì)GEO類型的支持。GEO,Geographic,地理信息的縮寫。該類型,就是元素的2維坐標(biāo),在地圖上就是經(jīng)緯度。redis基于該類型,提供了經(jīng)緯度設(shè)置,查詢,范圍查詢,距離查詢,經(jīng)緯度Hash等常見操作。

Redis的發(fā)布和訂閱

什么是發(fā)布和訂閱

  • Redis 發(fā)布訂閱 (pub/sub) 是一種消息通信模式:發(fā)送者 (pub) 發(fā)送消息,訂閱者 (sub) 接收消息。
  • Redis 客戶端可以訂閱任意數(shù)量的頻道。

Redis事務(wù)、鎖機(jī)制秒殺

Redis事務(wù)定義

  • Redis事務(wù)是一個(gè)單獨(dú)的隔離操作:事務(wù)中的所有命令都會(huì)序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會(huì)被其他客戶端發(fā)送來的命令請(qǐng)求所打斷。
  • Redis事務(wù)的主要作用就是串聯(lián)多個(gè)命令防止別的命令插隊(duì)。

Multi、Exec、discard

Redis事務(wù)中有Multi、Exec和discard三個(gè)指令,在Redis中,從輸入Multi命令開始,輸入的命令都會(huì)依次進(jìn)入命令隊(duì)列中,但不會(huì)執(zhí)行,直到輸入Exec后,Redis會(huì)將之前的命令隊(duì)列中的命令依次執(zhí)行。而組隊(duì)的過程中可以通過discard來放棄組隊(duì)。

為什么要做成事務(wù)

想想一個(gè)場(chǎng)景:有很多人有你的賬戶,同時(shí)去參加雙十一搶購(gòu)。

事務(wù)沖突的問題

  • 例子
    • 一個(gè)請(qǐng)求想給金額減8000;

    • 一個(gè)請(qǐng)求想給金額減5000;

    • 一個(gè)請(qǐng)求想給金額減1000。

      最終我們可以發(fā)現(xiàn),總共金額是10000,如果請(qǐng)求全部執(zhí)行,那最后的金額變?yōu)?4000,很明顯不合理。

悲觀鎖

悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)block直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

樂觀鎖

樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號(hào)等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機(jī)制實(shí)現(xiàn)事務(wù)的。

  • WATCH key [key …]
    • 在執(zhí)行multi之前,先執(zhí)行watch key1 [key2],可以監(jiān)視一個(gè)(或多個(gè)) key ,如果在事務(wù)執(zhí)行之前這個(gè)(或這些) key被其他命令所改動(dòng),那么事務(wù)將被打斷。
  • unwatch
    • 取消 WATCH 命令對(duì)所有 key 的監(jiān)視。如果在執(zhí)行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被執(zhí)行了的話,那么就不需要再執(zhí)行UNWATCH 了。

Redis事務(wù)三特性

  • 單獨(dú)的隔離操作 :事務(wù)中的所有命令都會(huì)序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會(huì)被其他客戶端發(fā)送來的命令請(qǐng)求所打斷。

  • 沒有隔離級(jí)別的概念 :隊(duì)列中的命令沒有提交之前都不會(huì)實(shí)際被執(zhí)行,因?yàn)槭聞?wù)提交前任何指令都不會(huì)被實(shí)際執(zhí)行。

  • 不保證原子性 :事務(wù)中如果有一條命令執(zhí)行失敗,其后的命令仍然會(huì)被執(zhí)行,沒有回滾 。

redis事務(wù)案例

/**
  * redis事務(wù)
  */
@Test
public void test6() {
    //返回值o是由SessionCallback內(nèi)部方法execute方法返回
    Object o = redisTemplate.execute(new SessionCallback<Object>() {
        @Override
        public  Object execute(RedisOperations redisOperations) throws DataAccessException {
            redisOperations.multi();
            //1、設(shè)置key
            redisOperations.opsForValue().set("vKey", "hi redis.");
            //2、獲取key
            redisOperations.opsForValue().get("vKey");
            //3、刪除key
            redisOperations.delete("vKey");
            final List<Object> result = redisOperations.exec();
            //返回vKey的值
            return result.get(1).toString();
        }
    });
    //輸出"hi redis."
    System.out.println(o.toString());
}

/**
redis樂觀鎖使用
*/
@Test
public void test() {
    Object o = redisTemplate.execute(new SessionCallback<Object>() {
        int qua = 0;
        /**
             * Executes all the given operations inside the same session.
             *
             * @param operations Redis operations
             * @return return value
             */
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            try {
                operations.opsForValue().set("quality", 20);
                operations.watch("quality");
                operations.multi();
                operations.opsForValue().increment("quality", -30);
                operations.exec();//執(zhí)行事務(wù)
            } catch (Exception e) {
                operations.unwatch();//解鎖
            } finally {
                qua = (int)operations.opsForValue().get("quality");
                if (qua < 0) {
                    operations.opsForValue().increment("quality", 30);//還原庫(kù)存
                }
                qua = (int)operations.opsForValue().get("quality");
            }
            return qua;
        }
    });
    System.out.println((Integer) o);
}

//通常我們并不使用這種方式加鎖,我們常使用分布式鎖解決此問題

連接超時(shí),通過連接池解決

節(jié)省每次連接redis服務(wù)帶來的消耗,把連接好的實(shí)例反復(fù)利用。

  • 連接池參數(shù):

    • MaxTotal:控制一個(gè)pool可分配多少個(gè)jedis實(shí)例,通過pool.getResource()來獲取;如果賦值為-1,則表示不限制;如果pool已經(jīng)分配了MaxTotal個(gè)jedis實(shí)例,則此時(shí)pool的狀態(tài)為exhausted。

    • maxIdle:控制一個(gè)pool最多有多少個(gè)狀態(tài)為idle(空閑)的jedis實(shí)例;

    • MaxWaitMillis:表示當(dāng)borrow一個(gè)jedis實(shí)例時(shí),最大的等待毫秒數(shù),如果超過等待時(shí)間,則直接拋JedisConnectionException;

    • testOnBorrow:獲得一個(gè)jedis實(shí)例的時(shí)候是否檢查連接可用性(ping());如果為true,則得到的jedis實(shí)例均是可用的。

  • 解決庫(kù)存遺留問題

    • 將復(fù)雜的或者多步的redis操作,寫為一個(gè)腳本,一次提交給redis執(zhí)行,減少反復(fù)連接redis的次數(shù),提升性能。
    • LUA腳本是類似redis事務(wù),有一定的原子性,不會(huì)被其他命令插隊(duì),可以完成一些redis事務(wù)性的操作。
    • 但是注意redis的lua腳本功能,只有在Redis 2.6以上的版本才可以使用。
    • 利用lua腳本淘汰用戶,解決超賣問題,redis 2.6版本以后,通過lua腳本解決爭(zhēng)搶問題,實(shí)際上是redis 利用其單線程的特性,用任務(wù)隊(duì)列的方式解決多任務(wù)并發(fā)問題。

Redis持久化之RDB

Redis 提供了2個(gè)不同形式的持久化方式:

  • RDB(Redis DataBase)
  • AOF(Append Of File)

RDB

  • 簡(jiǎn)介

    • 指定的時(shí)間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤, 也就是行話講的Snapshot快照,它恢復(fù)時(shí)是將快照文件直接讀到內(nèi)存里。
  • 備份是如何執(zhí)行的

    • Redis會(huì)單獨(dú)創(chuàng)建(fork)一個(gè)子進(jìn)程來進(jìn)行持久化,首先會(huì)將數(shù)據(jù)寫入到一個(gè)臨時(shí)文件中,待持久化過程都結(jié)束了,再用這個(gè)臨時(shí)文件替換上次持久化好的文件。整個(gè)過程中,主進(jìn)程是不進(jìn)行任何IO操作的,這就確保了極高的性能。如果需要進(jìn)行大規(guī)模數(shù)據(jù)的恢復(fù),且對(duì)于數(shù)據(jù)恢復(fù)的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺點(diǎn)是最后一次持久化后的數(shù)據(jù)可能丟失。
  • Fork

    • Fork的作用是復(fù)制一個(gè)與當(dāng)前進(jìn)程一樣的進(jìn)程。新進(jìn)程的所有數(shù)據(jù)(變量、環(huán)境變量、程序計(jì)數(shù)器等) 數(shù)值都和原進(jìn)程一致,但是是一個(gè)全新的進(jìn)程,并作為原進(jìn)程的子進(jìn)程。
    • 在Linux程序中,fork()會(huì)產(chǎn)生一個(gè)和父進(jìn)程完全相同的子進(jìn)程,但子進(jìn)程在此后多會(huì)exec系統(tǒng)調(diào)用,出于效率考慮,Linux中引入了“寫時(shí)復(fù)制技術(shù)”。
    • 一般情況父進(jìn)程和子進(jìn)程會(huì)共用同一段物理內(nèi)存,只有進(jìn)程空間的各段的內(nèi)容要發(fā)生變化時(shí),才會(huì)將父進(jìn)程的內(nèi)容復(fù)制一份給子進(jìn)程。
  • dump.rdb文件

    • 在redis.conf中配置文件名稱,默認(rèn)為dump.rdb。
    • rdb文件的保存路徑,也可以修改。默認(rèn)為Redis啟動(dòng)時(shí)命令行所在的目錄下 “dir ./”
  • 如何觸發(fā)RDB快照;保持策略

    • 配置文件中默認(rèn)的快照配置
  • 命令save VS bgsave

    • save :save時(shí)只管保存,其它不管,全部阻塞。手動(dòng)保存,不建議。
    • bgsave:Redis會(huì)在后臺(tái)異步進(jìn)行快照操作, 快照同時(shí)還可以響應(yīng)客戶端請(qǐng)求。
    • 可以通過lastsave 命令獲取最后一次成功執(zhí)行快照的時(shí)間。
  • flushall命令

    • 執(zhí)行flushall命令,也會(huì)產(chǎn)生dump.rdb文件,但里面是空的,無意義。
  • 優(yōu)勢(shì)

    • 適合大規(guī)模的數(shù)據(jù)恢復(fù)
    • 對(duì)數(shù)據(jù)完整性和一致性要求不高更適合使用
    • 節(jié)省磁盤空間
    • 恢復(fù)速度快
  • 劣勢(shì)

    • Fork的時(shí)候,內(nèi)存中的數(shù)據(jù)被克隆了一份,大致2倍的膨脹性需要考慮。
    • 雖然Redis在fork時(shí)使用了寫時(shí)拷貝技術(shù),但是如果數(shù)據(jù)龐大時(shí)還是比較消耗性能。
    • 在備份周期在一定間隔時(shí)間做一次備份,所以如果Redis意外down掉的話,就會(huì)丟失最后一次快照后的所有修改。
  • 如何停止

    • 動(dòng)態(tài)停止RDB:redis-cli config set save “”#save后給空值,表示禁用保存策略。

Redis持久化之AOF

AOF(Append Only File)

以日志的形式來記錄每個(gè)寫操作(增量保存),將Redis執(zhí)行過的所有寫指令記錄下來(讀操作不記錄), 只許追加文件但不可以改寫文件,redis啟動(dòng)之初會(huì)讀取該文件重新構(gòu)建數(shù)據(jù),換言之,redis 重啟的話就根據(jù)日志文件的內(nèi)容將寫指令從前到后執(zhí)行一次以完成數(shù)據(jù)的恢復(fù)工作。

  • AOF持久化流程

    • 客戶端的請(qǐng)求寫命令會(huì)被append追加到AOF緩沖區(qū)內(nèi);
    • AOF緩沖區(qū)根據(jù)AOF持久化策略[always,everysec,no]將操作sync同步到磁盤的AOF文件中;
    • AOF文件大小超過重寫策略或手動(dòng)重寫時(shí),會(huì)對(duì)AOF文件rewrite重寫,壓縮AOF文件容量;
    • Redis服務(wù)重啟時(shí),會(huì)重新load加載AOF文件中的寫操作達(dá)到數(shù)據(jù)恢復(fù)的目的。
  • AOF默認(rèn)不開啟

    • 可以在redis.conf中配置文件名稱默認(rèn)為 appendonly.aof文件中開啟,AOF文件的保存路徑,同RDB的路徑一致。
  • AOF和RDB同時(shí)開啟,redis聽誰(shuí)的?

    • AOF和RDB同時(shí)開啟,系統(tǒng)默認(rèn)取AOF的數(shù)據(jù)(數(shù)據(jù)不會(huì)存在丟失)。這里的意思是redis重啟的時(shí)候,優(yōu)先加載AOF文件,因?yàn)橥ǔG闆r下AOF比RDB的數(shù)據(jù)完整性更好。
  • AOF啟動(dòng)、修復(fù)、恢復(fù)

    • AOF的備份機(jī)制和性能雖然和RDB不同,但是備份和恢復(fù)的操作同RDB一樣,都是拷貝備份文件,需要恢復(fù)時(shí)再拷貝到Redis工作目錄下,啟動(dòng)系統(tǒng)即加載。
    • 正常恢復(fù)
      • 修改默認(rèn)的appendonly no,改為yes。
      • 將有數(shù)據(jù)的aof文件復(fù)制一份保存到對(duì)應(yīng)目錄(查看目錄:config get dir)。
      • 恢復(fù):重啟redis然后重新加載。
    • 異常恢復(fù)
      • 修改默認(rèn)的appendonly no,改為yes。
      • 如遇到AOF文件損壞,通過/usr/local/bin/redis-check-aof–fix appendonly.aof進(jìn)行恢復(fù)。
      • 備份被寫壞的AOF文件。
      • 恢復(fù):重啟redis,然后重新加載。
  • AOF同步頻率設(shè)置

    • appendfsync always:始終同步,每次Redis的寫入都會(huì)立刻記入日志;性能較差但數(shù)據(jù)完整性比較好。
    • appendfsync everysec:每秒同步,每秒記入日志一次,如果宕機(jī),本秒的數(shù)據(jù)可能丟失。
    • appendfsync no:redis不主動(dòng)進(jìn)行同步,把同步時(shí)機(jī)交給操作系統(tǒng)。

Rewrite壓縮

  • Rewrite壓縮是什么

    • AOF采用文件追加方式,文件會(huì)越來越大為避免出現(xiàn)此種情況,新增了重寫機(jī)制,當(dāng)AOF文件的大小超過所設(shè)定的閾值時(shí),Redis就會(huì)啟動(dòng)AOF文件的內(nèi)容壓縮,只保留可以恢復(fù)數(shù)據(jù)的最小指令集,可以使用命令bgrewriteaof。
  • 重寫原理,如何實(shí)現(xiàn)重寫

    • AOF文件持續(xù)增長(zhǎng)而過大時(shí),會(huì)fork出一條新進(jìn)程來將文件重寫(也是先寫臨時(shí)文件最后再rename),redis4.0版本后的重寫,是指把rdb 的快照,以二進(jìn)制的形式附在新的aof頭部,作為已有的歷史數(shù)據(jù),替換掉原來的流水賬操作。
  • no-appendfsync-on-rewrite:

    • 如果 no-appendfsync-on-rewrite=yes ,不寫入aof文件只寫入緩存,用戶請(qǐng)求不會(huì)阻塞,但是在這段時(shí)間如果宕機(jī)會(huì)丟失這段時(shí)間的緩存數(shù)據(jù)。(降低數(shù)據(jù)安全性,提高性能)
    • 如果 no-appendfsync-on-rewrite=no,還是會(huì)把數(shù)據(jù)往磁盤里刷,但是遇到重寫操作,可能會(huì)發(fā)生阻塞。(數(shù)據(jù)安全,但是性能降低)

觸發(fā)機(jī)制,何時(shí)重寫

Redis會(huì)記錄上次重寫時(shí)的AOF大小,默認(rèn)配置是當(dāng)AOF文件大小是上次rewrite后大小的一倍且文件大于64M時(shí)觸發(fā)。

重寫雖然可以節(jié)約大量磁盤空間,減少恢復(fù)時(shí)間。但是每次重寫還是有一定的負(fù)擔(dān)的,因此設(shè)定Redis要滿足一定條件才會(huì)進(jìn)行重寫。

  • auto-aof-rewrite-percentage:設(shè)置重寫的基準(zhǔn)值,文件達(dá)到100%時(shí)開始重寫(文件是原來重寫后文件的2倍時(shí)觸發(fā))。
  • auto-aof-rewrite-min-size:設(shè)置重寫的基準(zhǔn)值,最小文件64MB。達(dá)到這個(gè)值開始重寫。
  • 系統(tǒng)載入時(shí)或者上次重寫完畢時(shí),Redis會(huì)記錄此時(shí)AOF大小,設(shè)為base_size
  • 如果Redis的AOF當(dāng)前大小>= base_size +base_size*100% (默認(rèn))且當(dāng)前大小>=64mb(默認(rèn))的情況下,Redis會(huì)對(duì)AOF進(jìn)行重寫。
  • 例如:文件達(dá)到70MB開始重寫,降到50MB,下次什么時(shí)候開始重寫?100MB

重寫流程

  • bgrewriteaof觸發(fā)重寫,判斷是否當(dāng)前有bgsave或bgrewriteaof在運(yùn)行,如果有,則等待該命令結(jié)束后再繼續(xù)執(zhí)行;
  • 主進(jìn)程fork出子進(jìn)程執(zhí)行重寫操作,保證主進(jìn)程不會(huì)阻塞;
  • 子進(jìn)程遍歷redis內(nèi)存中數(shù)據(jù)到臨時(shí)文件,客戶端的寫請(qǐng)求同時(shí)寫入aof_buf緩沖區(qū)和aof_rewrite_buf重寫緩沖區(qū),保證原AOF文件完整以及新AOF文件生成期間的新的數(shù)據(jù)修改動(dòng)作不會(huì)丟失;
  • 子進(jìn)程寫完新的AOF文件后,向主進(jìn)程發(fā)信號(hào),父進(jìn)程更新統(tǒng)計(jì)信息。主進(jìn)程把a(bǔ)of_rewrite_buf中的數(shù)據(jù)寫入到新的AOF文件;
  • 使用新的AOF文件覆蓋舊的AOF文件,完成AOF重寫。

優(yōu)勢(shì)

  • 備份機(jī)制更穩(wěn)健,丟失數(shù)據(jù)概率更低。
  • 可讀的日志文本,通過操作AOF穩(wěn)健,可以處理誤操作。

劣勢(shì)

  • 比起RDB占用更多的磁盤空間。
  • 恢復(fù)備份速度要慢。
  • 每次讀寫都同步的話,有一定的性能壓力。
  • 存在個(gè)別Bug,造成恢復(fù)不能。

總結(jié)(Which one)

用哪個(gè)好

  • 官方推薦兩個(gè)都啟用:

    • 如果對(duì)數(shù)據(jù)不敏感,可以選單獨(dú)用RDB。
    • 不建議單獨(dú)用 AOF,因?yàn)榭赡軙?huì)出現(xiàn)Bug。
    • 如果只是做純內(nèi)存緩存,可以都不用。
  • 官網(wǎng)建議

    • RDB持久化方式能夠在指定的時(shí)間間隔能對(duì)你的數(shù)據(jù)進(jìn)行快照存儲(chǔ)。

    • AOF持久化方式記錄每次對(duì)服務(wù)器寫的操作,當(dāng)服務(wù)器重啟的時(shí)候會(huì)重新執(zhí)行這些命令來恢復(fù)原始的數(shù)據(jù),AOF命令以redis協(xié)議追加保存每次寫的操作到文件末尾。

    • Redis還能對(duì)AOF文件進(jìn)行后臺(tái)重寫,使得AOF文件的體積不至于過大。

    • 只做緩存:如果你只希望你的數(shù)據(jù)在服務(wù)器運(yùn)行的時(shí)候存在,你也可以不使用任何持久化方式。

    • 同時(shí)開啟兩種持久化方式:在這種情況下,當(dāng)redis重啟的時(shí)候會(huì)優(yōu)先載入AOF文件來恢復(fù)原始的數(shù)據(jù),因?yàn)樵谕ǔG闆r下AOF文件保存的數(shù)據(jù)集要比RDB文件保存的數(shù)據(jù)集要完整。

    • RDB的數(shù)據(jù)不實(shí)時(shí),同時(shí)使用兩者時(shí)服務(wù)器重啟也只會(huì)找AOF文件。那要不要只使用AOF呢?

    • 建議不要,因?yàn)镽DB更適合用于備份數(shù)據(jù)庫(kù)(AOF在不斷變化不好備份),快速重啟,而且不會(huì)有AOF可能潛在的bug,留著作為一個(gè)萬一的手段。

    • 性能建議:

      • 因?yàn)镽DB文件只用作后備用途,建議只在Slave上持久化RDB文件,而且只要15分鐘備份一次就夠了,只保留save 9001這條規(guī)則。
      • 如果使用AOF,好處是在最惡劣情況下也只會(huì)丟失不超過兩秒數(shù)據(jù),啟動(dòng)腳本較簡(jiǎn)單,只load自己的AOF文件就可以了。
        aof代價(jià):一是帶來了持續(xù)的IO,二是AOF rewrite的最后,將rewrite過程中產(chǎn)生的新數(shù)據(jù)寫到新文件造成的阻塞幾乎是不可避免的。
      • 只要硬盤許可,應(yīng)該盡量減少AOF rewrite的頻率,AOF重寫的基礎(chǔ)大小默認(rèn)值64M太小了,可以設(shè)到5G以上。默認(rèn)超過原大小100%大小時(shí)重寫可以改到適當(dāng)?shù)臄?shù)值。

Redis主從復(fù)制

主機(jī)數(shù)據(jù)更新后根據(jù)配置和策略, 自動(dòng)同步到備機(jī)的master/slaver機(jī)制,Master以寫為主,Slave以讀為主主從復(fù)制節(jié)點(diǎn)間數(shù)據(jù)是全量的。

作用:

  • 讀寫分離,性能擴(kuò)展

  • 容災(zāi)快速恢復(fù)

復(fù)制原理

  • Slave啟動(dòng)成功連接到master后會(huì)發(fā)送一個(gè)sync命令;

  • Master接到命令啟動(dòng)后臺(tái)的存盤進(jìn)程,同時(shí)收集所有接收到的用于修改數(shù)據(jù)集命令,在后臺(tái)進(jìn)程執(zhí)行完畢之后,master將傳送整個(gè)數(shù)據(jù)文件到slave,以完成一次完全同步。

  • 全量復(fù)制:slave服務(wù)器在接收到數(shù)據(jù)庫(kù)文件數(shù)據(jù)后,將其存盤并加載到內(nèi)存中。

  • 增量復(fù)制:Master繼續(xù)將新的所有收集到的修改命令依次傳給slave,完成同步。

  • 但是只要是重新連接master,一次完全同步(全量復(fù)制)將被自動(dòng)執(zhí)行。

哨兵模式(sentinel)

反客為主:當(dāng)一個(gè)master宕機(jī)后,后面的slave可以立刻升為master,其后面的slave不用做任何修改。用 slaveof no one 指令將從機(jī)變?yōu)橹鳈C(jī)。而哨兵模式是反客為主的自動(dòng)版,能夠后臺(tái)監(jiān)控主機(jī)是否故障,如果故障了根據(jù)投票數(shù)自動(dòng)將從庫(kù)轉(zhuǎn)換為主庫(kù)。

當(dāng)主機(jī)掛掉,從機(jī)選舉產(chǎn)生新的主機(jī)

  • 哪個(gè)從機(jī)會(huì)被選舉為主機(jī)呢?根據(jù)優(yōu)先級(jí)別:slave-priority 。
  • 原主機(jī)重啟后會(huì)變?yōu)閺臋C(jī)。

復(fù)制延時(shí)

由于所有的寫操作都是先在Master上操作,然后同步更新到Slave上,所以從Master同步到Slave機(jī)器有一定的延遲,當(dāng)系統(tǒng)很繁忙的時(shí)

候,延遲問題會(huì)更加嚴(yán)重,Slave機(jī)器數(shù)量的增加也會(huì)使這個(gè)問題更加嚴(yán)重。

故障恢復(fù)

  • 優(yōu)先級(jí):在redis.conf中默認(rèn) slave-priority 100,值越小優(yōu)先級(jí)越高。

  • 偏移量:指獲得原主機(jī)數(shù)據(jù)最全的概率。

  • runid:每個(gè)redis實(shí)例啟動(dòng)后都會(huì)隨機(jī)生成一個(gè)40位的runid。

Redis集群(cluster模式)

Redis 集群(包括很多小集群)實(shí)現(xiàn)了對(duì)Redis的水平擴(kuò)容,即啟動(dòng)N個(gè)redis節(jié)點(diǎn),將整個(gè)數(shù)據(jù)庫(kù)分布存儲(chǔ)在這N個(gè)節(jié)點(diǎn)中,每個(gè)節(jié)點(diǎn)存儲(chǔ)總數(shù)據(jù)的1/N,即一個(gè)小集群存儲(chǔ)1/N的數(shù)據(jù),每個(gè)小集群里面維護(hù)好自己的1/N的數(shù)據(jù)。

Redis 集群通過分區(qū)(partition)來提供一定程度的可用性(availability): 即使集群中有一部分節(jié)點(diǎn)失效或者無法進(jìn)行通訊, 集群也可以繼續(xù)處理命令請(qǐng)求。

該模式的redis集群特點(diǎn)是:分治、分片。

問題

  • 容量不夠,redis如何進(jìn)行擴(kuò)容?

  • 并發(fā)寫操作, redis如何分?jǐn)偅?/p>

  • 另外,主從模式,薪火相傳模式,主機(jī)宕機(jī),導(dǎo)致ip地址發(fā)生變化,應(yīng)用程序中配置需要修改對(duì)應(yīng)的主機(jī)地址、端口等信息。

  • 之前通過代理主機(jī)來解決,但是redis3.0中提供了解決方案。就是無中心化集群配置。

集群連接

普通方式登錄:可能直接進(jìn)入讀主機(jī),存儲(chǔ)數(shù)據(jù)時(shí),會(huì)出現(xiàn)MOVED重定向操作,所以,應(yīng)該以集群方式登錄。

集群登錄:redis-cli -c -p 6379 采用集群策略連接,設(shè)置數(shù)據(jù)會(huì)自動(dòng)切換到相應(yīng)的寫主機(jī).

redis cluster 如何分配這六個(gè)節(jié)點(diǎn)?

  • 一個(gè)集群至少要有三個(gè)主節(jié)點(diǎn)。
  • 選項(xiàng) –cluster-replicas 1 :表示我們希望為集群中的每個(gè)主節(jié)點(diǎn)創(chuàng)建一個(gè)從節(jié)點(diǎn)。
  • 分配原則盡量保證每個(gè)主數(shù)據(jù)庫(kù)運(yùn)行在不同的IP地址,每個(gè)從庫(kù)和主庫(kù)不在一個(gè)IP地址上。

什么是slots

一個(gè) Redis 集群包含 16384 個(gè)插槽(hash slot),數(shù)據(jù)庫(kù)中的每個(gè)鍵都屬于這 16384 個(gè)插槽的其中一個(gè)。集群使用公式 CRC16(key) % 16384 來計(jì)算鍵 key 屬于哪個(gè)槽, 其中 CRC16(key) 語(yǔ)句用于計(jì)算鍵 key 的 CRC16 校驗(yàn)和 。

集群中的每個(gè)節(jié)點(diǎn)負(fù)責(zé)處理一部分插槽。 舉個(gè)例子, 如果一個(gè)集群可以有主節(jié)點(diǎn), 其中:

  • 節(jié)點(diǎn) A 負(fù)責(zé)處理 0 號(hào)至 5460 號(hào)插槽。
  • 節(jié)點(diǎn) B 負(fù)責(zé)處理 5461 號(hào)至 10922 號(hào)插槽。
  • 節(jié)點(diǎn) C 負(fù)責(zé)處理 10923 號(hào)至 16383 號(hào)插槽。

在集群中錄入值

在redis-cli每次錄入、查詢鍵值,redis都會(huì)計(jì)算出該key應(yīng)該送往的插槽,如果不是該客戶端對(duì)應(yīng)服務(wù)器的插槽,redis會(huì)報(bào)錯(cuò),并告知應(yīng)前往的redis實(shí)例地址和端口。

redis-cli客戶端提供了 –c 參數(shù)實(shí)現(xiàn)自動(dòng)重定向。如 redis-cli -c –p 6379 登入后,再錄入、查詢鍵值對(duì)可以自動(dòng)重定向。不在一個(gè)slot下的鍵值,是不能使用mget,mset等多鍵操作。

故障恢復(fù)

  • 如果主節(jié)點(diǎn)下線?從節(jié)點(diǎn)能否自動(dòng)升為主節(jié)點(diǎn)?注意:15秒超時(shí)

  • 主節(jié)點(diǎn)恢復(fù)后,主從關(guān)系會(huì)如何?主節(jié)點(diǎn)回來變成從機(jī)。

如果所有某一段插槽的主從節(jié)點(diǎn)都宕掉,redis服務(wù)是否還能繼續(xù)?

  • 如果某一段插槽的主從都掛掉,而cluster-require-full-coverage 為yes ,那么整個(gè)集群都掛掉。

  • 如果某一段插槽的主從都掛掉,而cluster-require-full-coverage 為no ,那么,該插槽數(shù)據(jù)全都不能使用,也無法存儲(chǔ)。

Redis 集群優(yōu)點(diǎn)

  • 實(shí)現(xiàn)擴(kuò)容

  • 分?jǐn)倝毫?/p>

  • 無中心配置相對(duì)簡(jiǎn)單

Redis 集群不足

  • 多鍵操作是不被支持的。

  • 多鍵的Redis事務(wù)是不被支持的,lua腳本不被支持。

  • 由于集群方案出現(xiàn)較晚,很多公司已經(jīng)采用了其他的集群方案,而代理或者客戶端分片的方案想要遷移至redis cluster,需要整體遷移而不是逐步過渡,復(fù)雜度較大。

Redis應(yīng)用問題解決

緩存穿透

  • 問題描述

    • key對(duì)應(yīng)的數(shù)據(jù)在數(shù)據(jù)源并不存在,每次針對(duì)此key的請(qǐng)求從緩存獲取不到,請(qǐng)求都會(huì)壓到數(shù)據(jù)源(數(shù)據(jù)庫(kù)),從而可能壓垮數(shù)據(jù)源。比如用一個(gè)不存在的用戶id獲取用戶信息,不論緩存還是數(shù)據(jù)庫(kù)都沒有,若黑客利用此漏洞進(jìn)行攻擊可能壓垮數(shù)據(jù)庫(kù)。
  • 緩存穿透發(fā)生的條件:

    • 應(yīng)用服務(wù)器壓力變大

    • redis命中率降低

    • 一直查詢數(shù)據(jù)庫(kù),使得數(shù)據(jù)庫(kù)壓力太大而壓垮

      其實(shí)redis在這個(gè)過程中一直平穩(wěn)運(yùn)行,崩潰的是我們的數(shù)據(jù)庫(kù)(如MySQL)。

      緩存穿透發(fā)生的原因:黑客或者其他非正常用戶頻繁進(jìn)行很多非正常的url訪問,使得redis查詢不到數(shù)據(jù)庫(kù)。

  • 解決方案

    • 對(duì)空值緩存:如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)是否不存在),我們?nèi)匀话堰@個(gè)空結(jié)果(null)進(jìn)行緩存,設(shè)置空結(jié)果的過期時(shí)間會(huì)很短,最長(zhǎng)不超過五分鐘。
    • 設(shè)置可訪問的名單(白名單):使用bitmaps類型定義一個(gè)可以訪問的名單,名單id作為bitmaps的偏移量,每次訪問和bitmap里面的id進(jìn)行比較,如果訪問id不在bitmaps里面,進(jìn)行攔截,不允許訪問。
    • 采用布隆過濾器:布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實(shí)際上是一個(gè)很長(zhǎng)的二進(jìn)制向量(位圖)和一系列隨機(jī)映射函數(shù)(哈希函數(shù))。布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都遠(yuǎn)遠(yuǎn)超過一般的算法,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難。
    • 進(jìn)行實(shí)時(shí)監(jiān)控:當(dāng)發(fā)現(xiàn)Redis的命中率開始急速降低,需要排查訪問對(duì)象和訪問的數(shù)據(jù),和運(yùn)維人員配合,可以設(shè)置黑名單限制服務(wù)。

緩存擊穿

  • 問題描述

  • key對(duì)應(yīng)的數(shù)據(jù)存在,但在redis中過期,此時(shí)若有大量并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般都會(huì)從后端數(shù)據(jù)庫(kù)加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端數(shù)據(jù)庫(kù)壓垮。

  • 緩存擊穿的現(xiàn)象:

    • 數(shù)據(jù)庫(kù)訪問壓力瞬時(shí)增加,數(shù)據(jù)庫(kù)崩潰
    • redis里面沒有出現(xiàn)大量key過期
    • redis正常運(yùn)行

    緩存擊穿發(fā)生的原因:redis某個(gè)key過期了,大量訪問使用這個(gè)key(熱門key)。

  • 解決方案

    key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問,是一種非常“熱點(diǎn)”的數(shù)據(jù)。

    • 預(yù)先設(shè)置熱門數(shù)據(jù):在redis高峰訪問之前,把一些熱門數(shù)據(jù)提前存入到redis里面,加大這些熱門數(shù)據(jù)key的時(shí)長(zhǎng)。
    • 實(shí)時(shí)調(diào)整:現(xiàn)場(chǎng)監(jiān)控哪些數(shù)據(jù)熱門,實(shí)時(shí)調(diào)整key的過期時(shí)長(zhǎng)。
    • 使用鎖:
      • 就是在緩存失效的時(shí)候(判斷拿出來的值為空),不是立即去load db。
      • 先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX)去set一個(gè)mutex key。
      • 當(dāng)操作返回成功時(shí),再進(jìn)行l(wèi)oad db的操作,并回設(shè)緩存,最后刪除mutex key;
      • 當(dāng)操作返回失敗,證明有線程在load db,當(dāng)前線程睡眠一段時(shí)間再重試整個(gè)get緩存的方法。

緩存雪崩

  • 問題描述
    • key對(duì)應(yīng)的數(shù)據(jù)存在,但在redis中過期,此時(shí)若有大量并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般都會(huì)從后端數(shù)據(jù)庫(kù)加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端數(shù)據(jù)庫(kù)壓垮。
    • 緩存雪崩與緩存擊穿的區(qū)別在于這里針對(duì)很多key緩存,前者則是某一個(gè)key正常訪問。
  • 解決方案
    • 構(gòu)建多級(jí)緩存架構(gòu):nginx緩存 + redis緩存 +其他緩存(ehcache等)。
    • 使用鎖或隊(duì)列:用加鎖或者隊(duì)列的方式來保證不會(huì)有大量的線程對(duì)數(shù)據(jù)庫(kù)一次性進(jìn)行讀寫,從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上,該方法不適用高并發(fā)情況。
    • 設(shè)置過期標(biāo)志更新緩存:記錄緩存數(shù)據(jù)是否過期(設(shè)置提前量),如果過期會(huì)觸發(fā)通知另外的線程在后臺(tái)去更新實(shí)際key的緩存。
    • 將緩存失效時(shí)間分散開:比如可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件。

分布式鎖

  • 問題描述

    • 隨著業(yè)務(wù)發(fā)展的需要,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程的特點(diǎn)以及分布在不同機(jī)器上,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個(gè)問題就需要一種跨JVM的互斥機(jī)制來控制共享資源的訪問,這就是分布式鎖要解決的問題!
  • 分布式鎖主流的實(shí)現(xiàn)方案:

    • 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖
    • 基于緩存(Redis等)
    • 基于Zookeeper
  • 根據(jù)實(shí)現(xiàn)方式,分布式鎖還可以分為類CAS自旋式分布式鎖以及event事件類型分布式鎖:

    • 類CAS自旋式分布式鎖:詢問的方式,類似java并發(fā)編程中的線程獲詢問的方式嘗試加鎖,如mysql、redis。
    • 另外一類是event事件通知進(jìn)程后續(xù)鎖的變化,輪詢向外的過程,如zookeeper、etcd。
  • 每一種分布式鎖解決方案都有各自的優(yōu)缺點(diǎn):

    • 性能:redis最高
    • 可靠性:zookeeper最高
  • 解決方案:使用redis實(shí)現(xiàn)分布式鎖

    • setnx:通過該命令嘗試獲得鎖,沒有獲得鎖的線程會(huì)不斷等待嘗試。
    • set key ex 3000nx:設(shè)置過期時(shí)間,自動(dòng)釋放鎖,解決當(dāng)某一個(gè)業(yè)務(wù)異常而導(dǎo)致鎖無法釋放的問題。但是當(dāng)業(yè)務(wù)運(yùn)行超過過期時(shí)間時(shí),開辟監(jiān)控線程增加該業(yè)務(wù)的運(yùn)行時(shí)間,直到運(yùn)行結(jié)束,釋放鎖。
    • uuid:設(shè)置uuid,釋放前獲取這個(gè)值,判斷是否自己的鎖,防止誤刪鎖,造成沒鎖的情況。

RedLock

Redlock是一種算法,Redlock也就是 Redis Distributed Lock,可用實(shí)現(xiàn)多節(jié)點(diǎn)redis的分布式鎖。RedLock官方推薦,Redisson完成了對(duì)Redlock算法封裝。

  • 此種方式具有以下特性:

    • 互斥訪問:即永遠(yuǎn)只有一個(gè) client 能拿到鎖。
    • 避免死鎖:最終 client 都可能拿到鎖,不會(huì)出現(xiàn)死鎖的情況,即使鎖定資源的服務(wù)崩潰或者分區(qū),仍然能釋放鎖。
    • 容錯(cuò)性:只要大部分 Redis 節(jié)點(diǎn)存活(一半以上),就可以正常提供服務(wù)

redis分布式鎖實(shí)現(xiàn)

private int score = 100;
boolean b = false;

@Autowired
RedissonClient redissonClient;

@Test
public void test() {
    for (int i = 0; i < 3; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                redisLock();
            }
        }).start();
    }
}

private void redisLock() {
    // 問題代碼
    //        while (true) {
    //            if (score > 0) {
    //                System.out.println(score);
    //                score -= 1;
    //            }
    //        }
    
    // 分布式鎖解決庫(kù)存問題
    RLock redLock = redissonClient.getLock("RED_LOCK_KEY");
    System.out.println("redLock=" + redLock);
    try {
        // 嘗試加鎖,最多等待500ms,上鎖以后10s自動(dòng)解鎖
        b = redLock.tryLock(5000, 10000, TimeUnit.MILLISECONDS);
        System.out.println("b=" + b);
        while (true) {
            if (b) {
                //獲取鎖成功
                if (score > 0) {
                    System.out.println(score);
                    score -= 1;
                }
            } else {
                break;
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        if (b) {
            redLock.unlock();
        }
    }
}

redis緩存與數(shù)據(jù)庫(kù)雙寫一致性問題

其實(shí)從理論上來講,給緩存設(shè)置過期時(shí)間才是保證最終一致性的最終解決方案。
所有的寫操作我們以數(shù)據(jù)庫(kù)為準(zhǔn),對(duì)于緩存操作我們盡最大努力即可。

redis的數(shù)據(jù)過期清除策略和內(nèi)存淘汰策略

  • 數(shù)據(jù)過期策略:定期刪除 + 惰性刪除
定期刪除:redis默認(rèn)每隔100s就隨機(jī)抽取一些設(shè)置了過期時(shí)間的數(shù)據(jù)的key,檢查其是否過期,如果過期,就刪除。
至于為什么是隨機(jī)抽取,是因?yàn)榧尤雛edis存儲(chǔ)了幾十萬了key,如果每隔100s就遍歷所有的key,是很消耗性能的。

為什么不用定時(shí)刪除呢?定時(shí)刪除時(shí)用一個(gè)定時(shí)器監(jiān)視key,過期則自動(dòng)刪除。雖然內(nèi)存得到及時(shí)釋放,
但是很消耗性能。

惰性刪除:定期刪除可能導(dǎo)致很多過期的key沒有刪除。這時(shí)候就需要惰性刪除,
惰性刪除時(shí)當(dāng)獲取某個(gè)key的時(shí)候,redis會(huì)檢查這個(gè)key是否過期,如果過期了,
就會(huì)被刪除。
  • 定期刪除和惰性刪除存在的問題:
    • 如果一個(gè)過期的key定期刪除沒有刪除,然后也沒有再次請(qǐng)求這個(gè)key,也就是惰性刪除也沒生效,這時(shí)候這個(gè)key就會(huì)一直留在內(nèi)存中,如果內(nèi)存中堆積了大量的這樣的key,就會(huì)導(dǎo)致內(nèi)存消耗殆盡,這時(shí)候就用到了內(nèi)存淘汰策略。

redis的內(nèi)存淘汰策略

noevicition:不進(jìn)行淘汰數(shù)據(jù)。一旦緩存被寫滿,再有寫的請(qǐng)求進(jìn)來,redis就不在提供寫的服務(wù),直接返回報(bào)錯(cuò)。
volatile-ttl:在設(shè)置了過期時(shí)間的鍵值對(duì)中,移除即將過期的鍵值對(duì)
volatile-random:在設(shè)置了過期時(shí)間的鍵值對(duì)中,隨機(jī)一處某個(gè)key
volatile-lru:在設(shè)置了過期時(shí)間的鍵值對(duì)中,移除最近很少使用的鍵值對(duì)
volatile-lfu:在設(shè)置了過期時(shí)間的鍵值對(duì)中,移除最近最不頻繁使用的鍵值對(duì)
allkeys-random:在所有鍵值對(duì)中,隨機(jī)移除某個(gè)key
allkeys-lru:在所有的鍵值對(duì)中,移除最近最少使用的鍵值對(duì)。
allkeys-lfu:在所有的鍵值對(duì)中移除最近最少使用的鍵值對(duì)
  • 通常情況下默認(rèn)使用alkeys-lru策略,這樣可以充分利用LRU這一經(jīng)典緩存算法的優(yōu)勢(shì),把最近長(zhǎng)訪問的數(shù)據(jù)留在緩存中,提升應(yīng)用范文性能。
  • 如果你的業(yè)務(wù)中有明顯的冷熱分區(qū),建議使用allkeys-lru策略
  • 如果沒有明顯得冷熱分區(qū),建議使用allkeys-random策略,隨機(jī)淘汰數(shù)據(jù)就行
  • 對(duì)于沒有設(shè)置過期時(shí)間的鍵值對(duì),volatile開頭的策略和noevicition是一樣的效果。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評(píng)論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,184評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評(píng)論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,794評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,096評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,291評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,513評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評(píng)論 1 291
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,026評(píng)論 3 396
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,253評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容