天天看點

Redis中間件學習筆記(四)(應用場景面試)

15.Redis叢集

15.1.問題

容量不夠,redis如何進行擴容?

并發寫操作, redis如何分攤?

另外,主從模式,薪火相傳模式,主機當機,導緻ip位址發生變化,應用程式中配置需要修改對應的主機位址、端口等資訊。

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

15.2.什麼是叢集

Redis 叢集實作了對Redis的水準擴容,即啟動N個redis節點,将整個資料庫分布存儲在這N個節點中,每個節點存儲總資料的1/N。

Redis 叢集通過分區(partition)來提供一定程度的可用性(availability): 即使叢集中有一部分節點失效或者無法進行通訊, 叢集也可以繼續處理指令請求。

15.3.删除持久化資料

将rdb,aof檔案都删除掉。

15.4.制作6個執行個體,6379,6380,6381,6389,6390,6391

15.4.1.配置基本資訊

開啟daemonize yes

Pid檔案名字

指定端口

Log檔案名字

Dump.rdb名字

Appendonly 關掉或者換名字

15.4.2.redis cluster配置修改

cluster-enabled yes 打開叢集模式

cluster-config-file nodes-6379.conf 設定節點配置檔案名

cluster-node-timeout 15000 設定節點失聯時間,超過該時間(毫秒),叢集自動進行主從切換。

include /home/bigdata/redis.conf
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
           

15.4.3.修改好redis6379.conf檔案,拷貝多個redis.conf檔案

15.4.4.使用查找替換修改另外5個檔案

例如:

:%s/6379/6380

15.4.5.啟動6個redis服務

Redis中間件學習筆記(四)(應用場景面試)

15.5.将六個節點合成一個叢集

組合之前,請確定所有redis執行個體啟動後,nodes-xxxx.conf檔案都生成正常。

  • 合體:

    cd /opt/redis-6.2.1/src

redis-cli --cluster create --cluster-replicas 1 182.92.206.42:6379 182.92.206.42:6380 182.92.206.42:6381 182.92.206.42:6389 182.92.206.42:6390 182.92.206.42:6391
           

此處不要用127.0.0.1, 請用真實IP位址

–replicas 1 采用最簡單的方式配置叢集,一台主機,一台從機,正好三組。

  • 普通方式登入

    可能直接進入讀主機,存儲資料時,會出現MOVED重定向操作。是以,應該以叢集方式登入。

15.6.-c 采用叢集政策連接配接,設定資料會自動切換到相應的寫主機

15.7.通過 cluster nodes 指令檢視叢集資訊

15.8.redis cluster 如何配置設定這六個節點?

一個叢集至少要有三個主節點。

選項 --cluster-replicas 1 表示我們希望為叢集中的每個主節點建立一個從節點。

配置設定原則盡量保證每個主資料庫運作在不同的IP位址,每個從庫和主庫不在一個IP位址上。

15.9.什麼是slots

[OK] All nodes agree about slots configuration.

>>> Check for open slots…

>>> Check slots coverage…

[OK] All 16384 slots covered.

一個 Redis 叢集包含 16384 個插槽(hash slot), 資料庫中的每個鍵都屬于這 16384 個插槽的其中一個,

叢集使用公式 CRC16(key) % 16384 來計算鍵 key 屬于哪個槽, 其中 CRC16(key) 語句用于計算鍵 key 的 CRC16 校驗和 。

叢集中的每個節點負責處理一部分插槽。 舉個例子, 如果一個叢集可以有主節點, 其中:

節點 A 負責處理 0 号至 5460 号插槽。

節點 B 負責處理 5461 号至 10922 号插槽。

節點 C 負責處理 10923 号至 16383 号插槽。

15.10.在叢集中錄入值

在redis-cli每次錄入、查詢鍵值,redis都會計算出該key應該送往的插槽,如果不是該用戶端對應伺服器的插槽,redis會報錯,并告知應前往的redis執行個體位址和端口。

redis-cli用戶端提供了 –c 參數實作自動重定向。

如 redis-cli -c –p 6379 登入後,再錄入、查詢鍵值對可以自動重定向。

不在一個slot下的鍵值,是不能使用mget,mset等多鍵操作。

可以通過{}來定義組的概念,進而使key中{}内相同内容的鍵值對放到一個slot中去。

15.11.查詢叢集中的值

CLUSTER GETKEYSINSLOT 傳回 count 個 slot 槽中的鍵。

15.12.故障恢複

如果主節點下線?從節點能否自動升為主節點?注意:15秒逾時

主節點恢複後,主從關系會如何?主節點回來變成從機。

如果所有某一段插槽的主從節點都宕掉,redis服務是否還能繼續?

如果某一段插槽的主從都挂掉,而cluster-require-full-coverage 為yes ,那麼 ,整個叢集都挂掉

如果某一段插槽的主從都挂掉,而cluster-require-full-coverage 為no ,那麼,該插槽資料全都不能使用,也無法存儲。

redis.conf中的參數 cluster-require-full-coverage

15.13.叢集的Jedis開發`

即使連接配接的不是主機,叢集會自動切換主機存儲。主機寫,從機讀。

無中心化主從叢集。無論從哪台主機寫的資料,其他主機上都能讀到資料。

public class JedisClusterTest {
  public static void main(String[] args) { 
     Set<HostAndPort>set =new HashSet<HostAndPort>();
     set.add(new HostAndPort("192.168.31.211",6379));
     JedisCluster jedisCluster=new JedisCluster(set);
     jedisCluster.set("k1", "v1");
     System.out.println(jedisCluster.get("k1"));
  }
}
           

15.14.Redis 叢集提供了以下好處

實作擴容

分攤壓力

無中心配置相對簡單

15.15.Redis 叢集的不足

多鍵操作是不被支援的

多鍵的Redis事務是不被支援的。lua腳本不被支援

由于叢集方案出現較晚,很多公司已經采用了其他的叢集方案,而代理或者用戶端分片的方案想要遷移至redis cluster,需要整體遷移而不是逐漸過渡,複雜度較大。

16.Redis應用問題解決

16.1.緩存穿透

16.1.1.問題描述

​ key對應的資料在資料源并不存在,每次針對此key的請求從緩存擷取不到,請求都會壓到資料源,進而可能壓垮資料源。比如用一個不存在的使用者id擷取使用者資訊,不論緩存還是資料庫都沒有,若黑客利用此漏洞進行攻擊可能壓垮資料庫。

Redis中間件學習筆記(四)(應用場景面試)

16.1.2.解決方案

​ 一個一定不存在緩存及查詢不到的資料,由于緩存是不命中時被動寫的,并且出于容錯考慮,如果從存儲層查不到資料則不寫入緩存,這将導緻這個不存在的資料每次請求都要到存儲層去查詢,失去了緩存的意義。

解決方案:

(1) **對空值緩存:**如果一個查詢傳回的資料為空(不管是資料是否不存在),我們仍然把這個空結果(null)進行緩存,設定空結果的過期時間會很短,最長不超過五分鐘。

(2) 設定可通路的名單(白名單):

使用bitmaps類型定義一個可以通路的名單,名單id作為bitmaps的偏移量,每次通路和bitmap裡面的id進行比較,如果通路id不在bitmaps裡面,進行攔截,不允許通路。

(3) 采用布隆過濾器:(布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量(位圖)和一系列随機映射函數(哈希函數)。

布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識别率和删除困難。)

将所有可能存在的資料哈希到一個足夠大的bitmaps中,一個一定不存在的資料會被 這個bitmaps攔截掉,進而避免了對底層存儲系統的查詢壓力。

(4) **進行實時監控:**當發現Redis的命中率開始急速降低,需要排查通路對象和通路的資料,和運維人員配合,可以設定黑名單限制服務

16.2.緩存擊穿

16.2.1.問題描述

​ key對應的資料存在,但在redis中過期,此時若有大量并發請求過來**,這些請求發現緩存過期一般都會從後端DB加載資料并回設到緩存,這個時候大并發的請求可能會瞬間把後端DB壓垮。**

Redis中間件學習筆記(四)(應用場景面試)

16.2.2.解決方案

​ key可能會在某些時間點被超高并發地通路,是一種非常“熱點”的資料。這個時候,需要考慮一個問題:緩存被“擊穿”的問題。

​ 解決問題:

**(1)預先設定熱門資料:**在redis高峰通路之前,把一些熱門資料提前存入到redis裡面,加大這些熱門資料key的時長

**(2)實時調整:**現場監控哪些資料熱門,實時調整key的過期時長

(3)使用鎖:

(1) 就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db。

(2) 先使用緩存工具的某些帶成功操作傳回值的操作(比如Redis的SETNX)去set一個mutex key

(3) 當操作傳回成功時,再進行load db的操作,并回設緩存,最後删除mutex key;

(4) 當操作傳回失敗,證明有線程在load db,目前線程睡眠一段時間再重試整個get緩存的方法。

Redis中間件學習筆記(四)(應用場景面試)

16.3.緩存雪崩

16.3.1.問題描述

key對應的資料存在,但在redis中過期,此時若有大量并發請求過來,這些請求發現緩存過期一般都會從後端DB加載資料并回設到緩存,這個時候大并發的請求可能會瞬間把後端DB壓垮。

緩存雪崩與緩存擊穿的差別在于這裡針對很多key緩存,前者則是某一個key

正常通路

Redis中間件學習筆記(四)(應用場景面試)

緩存失效瞬間

Redis中間件學習筆記(四)(應用場景面試)

16.3.2.解決方案

緩存失效時的雪崩效應對底層系統的沖擊非常可怕!

解決方案:

(1) **建構多級緩存架構:**nginx緩存 + redis緩存 +其他緩存(ehcache等)

(2) 使用鎖或隊列:

用加鎖或者隊列的方式保證來保證不會有大量的線程對資料庫一次性進行讀寫,進而避免失效時大量的并發請求落到底層存儲系統上。不适用高并發情況

(3) 設定過期标志更新緩存:

記錄緩存資料是否過期(設定提前量),如果過期會觸發通知另外的線程在背景去更新實際key的緩存。

(4) 将緩存失效時間分散開:

比如我們可以在原有的失效時間基礎上增加一個随機值,比如1-5分鐘随機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件。

16.4.分布式鎖

16.4.1.問題描述

​ 随着業務發展的需要,原單體單機部署的系統被演化成分布式叢集系統後,由于分布式系統多線程、多程序并且分布在不同機器上,這将使原單機部署情況下的并發控制鎖政策失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的通路,這就是分布式鎖要解決的問題!

分布式鎖主流的實作方案:

  1. 基于資料庫實作分布式鎖
  2. 基于緩存(Redis等)
  3. 基于Zookeeper

每一種分布式鎖解決方案都有各自的優缺點:

  1. 性能:redis最高
  2. 可靠性:zookeeper最高

這裡,我們就基于redis實作分布式鎖。

16.4.2.解決方案:使用redis實作分布式鎖

redis:指令

# set sku:1:info “OK” NX PX 10000

EX second :設定鍵的過期時間為 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

PX millisecond :設定鍵的過期時間為 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。

NX :隻在鍵不存在時,才對鍵進行設定操作。 SET key value NX 效果等同于 SETNX key value 。

XX :隻在鍵已經存在時,才對鍵進行設定操作。

Redis中間件學習筆記(四)(應用場景面試)
  1. 多個用戶端同時擷取鎖(setnx)
  2. 擷取成功,執行業務邏輯{從db擷取資料,放入緩存},執行完成釋放鎖(del)
  3. 其他用戶端等待重試

16.4.3.編寫代碼

Redis:

set num 0

@GetMapping("testLock")
public void testLock(){
    //1擷取鎖,setne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.second);
    //2擷取鎖成功、查詢num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判斷num為空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就轉成成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4釋放鎖,del
        redisTemplate.delete("lock");

    }else{
        //3擷取鎖失敗、每隔0.1秒再擷取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

重新開機,服務叢集,通過網關壓力測試:

ab -n 1000 -c 100 http://192.168.140.1:8080/test/testLock

基本實作。

問題:setnx剛好擷取到鎖,業務邏輯出現異常,導緻鎖無法釋放

解決:設定過期時間,自動釋放鎖。

16.4.4.優化之設定鎖的過期時間

設定過期時間有兩種方式:

  1. 首先想到通過expire設定過期時間(缺乏原子性:如果在setnx和expire之間出現異常,鎖也無法釋放)
  2. 在set時指定過期時間(推薦)
    Redis中間件學習筆記(四)(應用場景面試)
    設定過期時間:

    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.second);

    壓力測試肯定也沒有問題。自行測試

    問題:可能會釋放其他伺服器的鎖。

Redis中間件學習筆記(四)(應用場景面試)

解決:setnx擷取鎖時,設定一個指定的唯一值(例如:uuid);釋放前擷取這個值,判斷是否自己的鎖

16.4.5.優化之UUID防誤删

Redis中間件學習筆記(四)(應用場景面試)

代碼解決:

Redis中間件學習筆記(四)(應用場景面試)

問題:删除操作缺乏原子性。

Redis中間件學習筆記(四)(應用場景面試)

16.4.6.優化之LUA腳本保證删除的原子性

@GetMapping("testLockLua")
public void testLockLua() {
    //1 聲明一個uuid ,将做為一個value 放入我們的key所對應的值中
    String uuid = UUID.randomUUID().toString();
    //2 定義一個鎖:lua 腳本可以使用同一把鎖,來實作删除!
    String skuId = "25"; // 通路skuId 為25号的商品 100008348542
    String locKey = "lock:" + skuId; // 鎖住的是每個商品的資料

    // 3 擷取鎖
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

    // 第一種: lock 與過期時間中間不寫任何的代碼。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//設定過期時間
    // 如果true
    if (lock) {
        // 執行的業務邏輯開始
        // 擷取緩存中的num 資料
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接傳回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果說在這出現了異常! 那麼delete 就删除失敗! 也就是說鎖永遠存在!
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入緩存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用lua腳本來鎖*/
        // 定義lua 腳本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis執行lua執行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 設定一下傳回值類型 為Long
        // 因為删除判斷的時候,傳回的0,給其封裝為資料類型。如果不封裝那麼預設傳回String 類型,
        // 那麼傳回字元串與0 會有發生錯誤。
        redisScript.setResultType(Long.class);
        // 第一個要是script 腳本 ,第二個需要判斷的key,第三個就是key所對應的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
    } else {
        // 其他線程等待
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之後,調用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

Lua 腳本詳解:

Redis中間件學習筆記(四)(應用場景面試)

項目中正确使用:

  1. 定義key,key應該是為每個sku定義的,也就是每個sku有一把鎖。
    String locKey ="lock:"+skuId; // 鎖住的是每個商品的資料
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid,3,TimeUnit.SECONDS);
               
    Redis中間件學習筆記(四)(應用場景面試)

16.4.7.總結

1、加鎖

// 1. 從redis中擷取鎖,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
      .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);
           

2、使用lua釋放鎖

// 2. 釋放鎖 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 設定lua腳本傳回的資料類型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 設定lua腳本傳回類型為Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
           

3、重試

Thread.sleep(500);
testLock();
           

為了確定分布式鎖可用,我們至少要確定鎖的實作同時滿足以下四個條件:

  • 互斥性。在任意時刻,隻有一個用戶端能持有鎖。
  • 不會發生死鎖。即使有一個用戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他用戶端能加鎖。
  • 解鈴還須系鈴人。加鎖和解鎖必須是同一個用戶端,用戶端自己不能把别人加的鎖給解了。
  • 加鎖和解鎖必須具有原子性。

17.Redis6.0新功能

17.1.ACL

17.1.1.簡介

Redis ACL是Access Control List(通路控制清單)的縮寫,該功能允許根據可以執行的指令和可以通路的鍵來限制某些連接配接。

在Redis 5版本之前,Redis 安全規則隻有密碼控制 還有通過rename 來調整高危指令比如 flushdb , KEYS* , shutdown 等。Redis 6 則提供ACL的功能對使用者進行更細粒度的權限控制 :

(1)接入權限:使用者名和密碼

(2)可以執行的指令

(3)可以操作的 KEY

參考官網:https://redis.io/topics/acl

17.1.2.指令

1、使用acl list指令展現使用者權限清單

(1)資料說明

Redis中間件學習筆記(四)(應用場景面試)

2、使用acl cat指令

(1)檢視添權重限指令類别

(2)加參數類型名可以檢視類型下具體指令

3、使用acl whoami指令檢視目前使用者

4、使用aclsetuser指令建立和編輯使用者ACL

(1)ACL規則

下面是有效ACL規則的清單。某些規則隻是用于激活或删除标志,或對使用者ACL執行給定更改的單個單詞。其他規則是字元字首,它們與指令或類别名稱、鍵模式等連接配接在一起。

ACL規則
類型 參數 說明
啟動和禁用使用者 on 激活某使用者賬号
off 禁用某使用者賬号。注意,已驗證的連接配接仍然可以工作。如果預設使用者被标記為off,則新連接配接将在未進行身份驗證的情況下啟動,并要求使用者使用AUTH選項發送AUTH或HELLO,以便以某種方式進行身份驗證。
權限的添加删除 + 将指令添加到使用者可以調用的指令清單中
- 從使用者可執行指令清單移除指令
+@ 添加該類别中使用者要調用的所有指令,有效類别為@admin、@set、@sortedset…等,通過調用ACL CAT指令檢視完整清單。特殊類别@all表示所有指令,包括目前存在于伺服器中的指令,以及将來将通過子產品加載的指令。
-@ 從使用者可調用指令中移除類别
allcommands [email protected]的别名
nocommand [email protected]的别名
可操作鍵的添加或删除 ~ 添加可作為使用者可操作的鍵的模式。例如~*允許所有的鍵

(2)通過指令建立新使用者預設權限

acl setuser user1

在上面的示例中,我根本沒有指定任何規則。如果使用者不存在,這将使用just created的預設屬性來建立使用者。如果使用者已經存在,則上面的指令将不執行任何操作。

(3)設定有使用者名、密碼、ACL權限、并啟用的使用者

acl setuser user2 on >password ~cached:* +get

(4)切換使用者,驗證權限

17.2.IO多線程

17.2.1.簡介

Redis6終于支撐多線程了,告别單線程了嗎?No!!

IO多線程其實指用戶端互動部分的網絡IO互動處理子產品多線程,而非執行指令多線程。Redis6執行指令依然是單線程。

17.2.2.原理架構

Redis 6 加入多線程,但跟 Memcached 這種從 IO處理到資料通路多線程的實作模式有些差異。Redis 的多線程部分隻是用來處理網絡資料的讀寫和協定解析,執行指令仍然是單線程。之是以這麼設計是不想因為多線程而變得複雜,需要去控制 key、lua、事務,LPUSH/LPOP 等等的并發問題。整體的設計大體如下:

Redis中間件學習筆記(四)(應用場景面試)

另外,多線程IO預設也是不開啟的,需要再配置檔案中配置

io-threads-do-reads  yes 
io-threads 4
           

17.3.工具支援 Cluster

之前老版Redis想要搭叢集需要單獨安裝ruby環境,Redis 5 将 redis-trib.rb 的功能內建到 redis-cli 。另外官方 redis-benchmark 工具開始支援 cluster 模式了,通過多線程的方式對多個分片進行壓測。

17.4.Redis新功能持續關注

Redis6新功能還有:

1、RESP3新的 Redis 通信協定:優化服務端與用戶端之間通信

2、Client side caching用戶端緩存:基于 RESP3 協定實作的用戶端緩存功能。為了進一步提升緩存的性能,将用戶端經常通路的資料cache到用戶端。減少TCP網絡互動。

3、Proxy叢集代理模式:Proxy 功能,讓 Cluster 擁有像單執行個體一樣的接入方式,降低大家使用cluster的門檻。不過需要注意的是代理不改變 Cluster 的功能限制,不支援的指令還是不會支援,比如跨 slot 的多Key操作。

4、Modules API

Redis 6中子產品API開發進展非常大,因為Redis Labs為了開發複雜的功能,從一開始就用上Redis子產品。Redis可以變成一個架構,利用Modules來建構不同系統,而不需要從頭開始寫然後還要BSD許可。Redis一開始就是一個向編寫各種系統開放的平台。

繼續閱讀