如何保證 Redis 的高併發和高可用?討論redis的單點,高可用,集羣

如何保證 Redis 的高併發和高可用?討論redis的單點,高可用,集羣。

打開GitHub搜索redis,邊可以看到,該項目的介紹是這樣的:

Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, HyperLogLogs, Bitmaps.

我們可以提取其特性的關鍵字:

  • in-memory database ,內存數據庫
  • support:Strings , lists, sets ,hashes ,hyperloglogs, bitmaps
    也就是高性能,支持數據類型多。

高可用有兩個含義:一是數據儘量不丟失,二是保證服務儘可能可用。 AOF 和 RDB 數據持久化保證了數據儘量不丟失,那麼多節點來保證服務儘可能提供服務。

一般在實際生產中,服務不會部署成單節點,主要是有三個原因.

  • 容易出現單點故障,導致服務不可用
  • 單節點處理所有的請求,吞吐量有限
  • 單節點容量有限

爲了實現高可用,通常的做法是,將數據庫複製多個副本以部署在不同的服務器上,其中一臺掛了也可以繼續提供服務。Redis 實現高可用有三種部署模式:主從模式,哨兵模式,集羣模式。

本文討論redis的單點,高可用,集羣。

前言:Redis提供了哪些高可用方案?

Redis基於一個Master主節點多Slave從節點的模式和Redis持久化機制,將一份數據保持在多個實例中實現增加副本冗餘量,又使用哨兵機制實現主備切換, 在master故障時,自動檢測,將某個slave切換爲master,最終實現Redis高可用 。

Redis主從複製

Redis主從複製,主從庫模式一個Master主節點多Slave從節點的模式,將一份數據保存在多Slave個實例中,增加副本冗餘量,當某些出現宕機後,Redis服務還可以使用。

但是這會存在數據不一致問題,那redis的副本集是如何數據一致性?

Redis爲了保證數據副本的一致,主從庫之間採用讀寫分離的方式:

讀操作:主庫、從庫都可以執行處理;

寫操作:先在主庫執行,再由主庫將寫操作同步給從庫。

使用讀寫分離方式的好處,可以避免當主從庫都可以處理寫操作時,主從庫處理寫操作加鎖等一系列鉅額的開銷。

採用讀寫分離方式,寫操作只會在主庫中進行後同步到從庫中,那主從庫是如何同步數據的呢?

主從庫是同步數據方式有兩種:

  • 全量同步:通常是主從服務器剛剛連接的時候,會先進行全量同步
  • 增量同步:一般在全同步結束後,進行增量同步,比如主從庫間網絡斷,再進行數據同步。

全量同步

主從庫間第一次全量同步,具體分成三個階段:

  • 當一個從庫啓動時,從庫給主庫發送psync命令進行數據同步(psync命令包含:主庫的runID和複製進度offset兩個參數),
  • 當主庫接收到psync 命令後將會保存RDB 文件併發送給從庫,發送期間會使用緩存區(replication buffer)記錄後續的所有寫操作 ,從庫收到數據後,會先清空當前數據庫,然後加載從主庫獲取的RDB 文件,
  • 當主庫完成 RDB 文件發送後,也會把將保存發送RDB文件期間寫操作的replication buffer發給從庫,從庫再重新執行這些操作。這樣一來,主從庫就實現同步了。

另,爲了分擔主庫生成 RDB 文件和傳輸 RDB 文件壓力,提高效率,可以使用“主 - 從 - 從”模式將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯的方式分散到從庫上。

增量同步

增量同步,基於環形緩衝區repl_backlog_buffer緩存區實現。

在環形緩衝區,主庫會記錄自己寫到的位置master_repl_offset,從庫則會記錄自己已經讀到的位置slave_repl_offset, 主庫並通過master_repl_offset和slave_repl_offset的差值的數據同步到從庫。

主從庫間網絡斷了, 主從庫會採用增量複製的方式繼續同步,主庫會把斷連期間收到的寫操作命令,寫入replication buffer,同時也會把這些操作命令也寫入repl_backlog_buffer這個緩衝區,然後主庫並通過master_repl_offset和slave_repl_offset的差值數據同步到從庫。

因爲repl_backlog_buffer是一個環形緩衝區,當在緩衝區寫滿後,主庫會繼續寫入,此時,會出現什麼情況呢?

覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫間的數據不一致。因此需要關注repl_backlog_size參數,調整合適的緩衝空間大小,避免數據覆蓋,主從數據不一致。

主從複製,除了會出現數據不一致外,甚至可能出現主庫宕機的情況,Redis會有主從自主切換機制,那如何實現的呢?

Redis哨兵機制

當主庫掛了,redis寫操作和數據同步無法進行,爲了避免這樣情況,可以在主庫掛了後重新在從庫中選舉出一個新主庫,並通知到客戶端,redis提供了哨兵機制,哨兵爲運行在特殊模式下的 Redis 進程。

Redis會有主從自主切換機制,那如何實現的呢?

哨兵機制是實現主從庫自動切換的關鍵機制,其主要分爲三個階段:

  • 監控:哨兵進程會週期性地給所有的主從庫發送 PING 命令,檢測它們是否仍然在線運行。
  • 選主(選擇主庫):主庫掛了以後,哨兵基於一定規則評分選選舉出一個從庫實例新的主庫 。
  • 通知 : 哨兵會將新主庫的信息發送給其他從庫,讓它們和新主庫建立連接,並進行數據複製。同時,哨兵會把新主庫的信息廣播通知給客戶端,讓它們把請求操作發到新主庫上。

其中,在監控中如何判斷主庫是否處於下線狀態

哨兵對主庫的下線判斷分爲:

  • 主觀下線:哨兵進程會使用 PING 命令檢測它自己和主、從庫的網絡連接情況,用來判斷實例的狀態,如果單哨兵發現主庫或從庫對 PING 命令的響應超時了,那麼,哨兵就會先把它標記爲“主觀下線”
  • 客觀下線:在哨兵集羣中,基於少數服從多數,多數實例都判定主庫已“主觀下線”,則認爲主庫“客觀下線”。

爲什麼會有這兩種"主觀下線"和“客觀下線”的下線狀態呢?

由於單機哨兵很容易產生誤判,誤判後主從切換會產生一系列的額外開銷,爲了減少誤判,避免這些不必要的開銷,採用哨兵集羣,引入多個哨兵實例一起來判斷,就可以避免單個哨兵因爲自身網絡狀況不好,而誤判主庫下線的情況,

基於少數服從多數原則, 當有 N 個哨兵實例時,最好要有 N/2 + 1 個實例判斷主庫爲“主觀下線”,才能最終判定主庫爲“客觀下線” (可以自定義設置闕值)

那麼哨兵之間是如何互相通信的呢?

哨兵集羣中哨兵實例之間可以相互發現,基於Redis提供的發佈 / 訂閱機制(pub/sub機制),

哨兵可以在主庫中發佈/訂閱消息,在主庫上有一個名爲“_sentinel_:hello”的頻道,不同哨兵就是通過它來相互發現,實現互相通信的,而且只有訂閱了同一個頻道的應用,才能通過發佈的消息進行信息交換。

哨兵 1連接相關信息(IP端口)發佈到“_sentinel_:hello”頻道上,哨兵 2 和 3 訂閱了該頻道。

哨兵 2 和 3 就可以從這個頻道直接獲取哨兵 1連接信息,以這樣的方式哨兵集羣就形成了,實現各個哨兵互相通信。

哨兵集羣中各個實現通信後,就可以判定主庫是否已客觀下線。

在已判定主庫已下線後,又如何選舉出新的主庫

新主庫選舉按照一定條件篩選出的符合條件的從庫,並按照一定規則對其進行打分,最高分者爲新主庫。

通常一定條件包括:

從庫的當前在線狀態

判斷它之前的網絡連接狀態,通過down-after-milliseconds * num(斷開連接次數),當斷開連接次數超過閾值,不適合爲新主庫。

一定規則包括

從庫優先級 , 通過slave-priority配置項,給不同的從庫設置不同優先級,優先級最高的從庫得分高

從庫複製進度,和舊主庫同步程度最接近的從庫得分高,通過repl_backlog_buffer緩衝區記錄主庫master_repl_offset和從庫slave_repl_offset相差最小高分

從庫 ID 號 , ID 號小的從庫得分高。

全都都基於在只有在一定規則中的某一輪評出最高分從庫就選舉結束,哨兵發起主從切換。

leader哨兵

選舉完新的主庫後,不能每個哨兵都發起主從切換,需要選舉成leader哨兵,那如何選舉leader哨兵執行主從切換?

選舉leader哨兵,也是基於少數服從多數原則"投票仲裁"選舉出來,

當任何一個從庫判定主庫“主觀下線”後,發送命令s-master-down-by-addr命令發送想要成爲Leader的信號,

其他哨兵根據與主機連接情況作出相對的響應,贊成票Y,反對票N,而且如果有多個哨兵發起請求,每個哨兵的贊成票只能投給其中一個,其他只能爲反對票。

想要成爲Leader 的哨兵,要滿足兩個條件:

第一,獲得半數以上的贊成票;

第二,獲得的票數同時還需要大於等於哨兵配置文件中的quorum值。

選舉完leader哨兵並新主庫切換完畢之後,那麼leader哨兵怎麼通知客戶端

還是基於哨兵自身的 pub/sub 功能,實現了客戶端和哨兵之間的事件通知,客戶端訂閱哨兵自身消息頻道 ,而且哨兵提供的消息訂閱頻道有很多,不同頻道包含了:

事件相關頻道

主庫下線事件+sdown(實例進入“主觀下線”狀態)

-sdown(實例退出“主觀下線”狀態)

+odown(實例進入“客觀下線”狀態)

-odown(實例退出“客觀下線”狀態)

新主庫切換+ switch-master(主庫地址發生變化)

其中,當客戶端從哨兵訂閱消息主從庫切換,當主庫切換後,端戶端就會接收到新主庫的連接信息:

switch-master 複製代碼

在這樣的方式哨兵就可以通知客戶端切換了新庫。

基於上述的機制和原理Redis實現了高可用,但也會帶了一些潛在的風險,比如數據缺失。

數據問題

Redis實現高可用,但實現期間可能產出一些風險:

主備切換的過程, 異步複製導致的數據丟失

  • 腦裂導致的數據丟失
  • 主備切換的過程,異步複製導致數據不一致

數據丟失-主從異步複製

因爲master將數據複製給slave是異步實現的,在複製過程中,這可能存在master有部分數據還沒複製到slave,master就宕機了,此時這些部分數據就丟失了。

總結:主庫的數據還沒有同步到從庫,結果主庫發生了故障,未同步的數據就丟失了。

數據丟失-腦裂

何爲腦裂?當一個集羣中的 master 恰好網絡故障,導致與 sentinal 通信不上了,sentinal會認爲master下線,且sentinal選舉出一個slave 作爲新的 master,此時就存在兩個 master了。

此時,可能存在client還沒來得及切換到新的master,還繼續寫向舊master的數據,當master再次恢復的時候,會被作爲一個slave掛到新的master 上去,自己的數據將會清空,重新從新的master 複製數據,這樣就會導致數據缺失。

總結:主庫的數據還沒有同步到從庫,結果主庫發生了故障,等從庫升級爲主庫後,未同步的數據就丟失了。

數據丟失解決方案

數據丟失可以通過合理地配置參數 min-slaves-to-write 和 min-slaves-max-lag 解決,比如

min-slaves-to-write1

min-slaves-max-lag10

如上兩個配置:要求至少有 1 個 slave,數據複製和同步的延遲不能超過 10 秒,如果超過 1 個 slave,數據複製和同步的延遲都超過了 10 秒鐘,那麼這個時候,master 就不會再接收任何請求了。

數據不一致

在主從異步複製過程,當從庫因爲網絡延遲或執行復雜度高命令阻塞導致滯後執行同步命令,這樣就會導致數據不一致

解決方案: 可以開發一個外部程序來監控主從庫間的複製進度(master_repl_offset和slave_repl_offset),通過監控master_repl_offset與slave_repl_offset差值得知複製進度,當複製進度不符合預期設置的Client不再從該從庫讀取數據。

image

一 .redis 安裝及配置

1 redis 安裝

打開redis的官網 http://redis.io

下載一個最新版本的安裝包,如 redis-version.tar.gz
解壓 tar zxvf redis-version.tar.gz
執行 make (執行此命令可能會報錯,例如確實gcc,一個個解決即可)
如果是 mac 電腦,安裝redis將十分簡單執行brew install redis即可。

安裝好redis之後,我們先不慌使用,先進行一些配置。打開redis.conf文件,我們主要關注以下配置:

port 6379             # 指定端口爲 6379,也可自行修改 
daemonize yes         # 指定後臺運行

1.1 redis 單點

安裝好redis之後,我們來運行一下。啓動redis的命令爲 :

redishome/bin/redis-server path/to/redis.config

假設我們沒有配置後臺運行(即:daemonize no),那麼我們會看到如下啓動日誌:

93825:C 20 Jan 2019 11:43:22.640 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
93825:C 20 Jan 2019 11:43:22.640 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=93825, just started
93825:C 20 Jan 2019 11:43:22.640 # Configuration loaded
93825:S 20 Jan 2019 11:43:22.641 * Increased maximum number of open files to 10032 (it was originally set to 256).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.3 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6380
 |    `-._   `._    /     _.-'    |     PID: 93825
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'   

無論是否配置了後臺運行,啓動成功之後,我們可以新開一個命令行窗口來操作試試。

1.1.2 在命令窗口操作redis

使用命令:telnet localhost 6379 來連接redis,或者你可以直接使用代碼來連接測試。連接之後,看到如下信息:

Connected to localhost.
Escape character is '^]'.

我們輸入幾個命令試試:

set hello world     設置key-value
get hello           獲取key值
expire hello 10     設置10秒過期
ttl hello           查看過期時間
del hello           刪除key

剛纔我們是使用命令行來操作redis的,下面我們來使用代碼操作一下redis,以Java爲例,我們使用一個開源的 java - redis客戶端。

1.1.3 使用jedis客戶端操作redis

打開GitHub,搜索redis,進入到項目主頁之後,我們可以看到使用方法:

1 加入jedis依賴

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

2 編寫代碼如下

Jedis jedis = new Jedis("localhost",6379);

jedis.set("hello", "world");

String value = jedis.get("hello");

System.out.println(value); // get world

jedis.del("hello");

System.out.println(jedis.get("hello"));// get null

1.1.4 使用spring-redis操作

上面jedis操作redis的例子很簡單,除了使用jedis之外,還可以使用spring-redis。步驟如下

配置redis

<bean id="jedisConnFactory"
    class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
    p:use-pool="true"/>

<!-- redis template definition -->
<bean id="redisTemplate"
    class="org.springframework.data.redis.core.RedisTemplate"
    p:connection-factory-ref="jedisConnFactory"/>

編寫代碼

public class Example {

    // inject the actual template
    @Autowired
    private RedisTemplate<String, String> template;

    // inject the template as ListOperations
    // can also inject as Value, Set, ZSet, and HashOperations
    @Resource(name="redisTemplate")
    private ListOperations<String, String> listOps;

    public void addLink(String userId, URL url) {
        listOps.leftPush(userId, url.toExternalForm());
        // or use template directly
        redisTemplate.boundListOps(userId).leftPush(url.toExternalForm());
    }
}

1.1.5 使用Lettuce操作redis

Lettuce是一個基於netty的 非阻塞的 redis客戶端。支持Java8以及響應式。其官網爲 https://lettuce.io/。Lettuce也可以和spring搭配使用。
使用Lettuce需要加入如下maven依賴:

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

基本的 get,set示例代碼如下:

public class LettuceTest {

    public static void main(String[] args) {
        RedisURI uri = new RedisURI();
        uri.setHost("myredishost");
        uri.setPort(6379);
        uri.setDatabase(0);
        RedisClient redisClient = RedisClient.create(uri);
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> syncCommands = connection.sync();

        syncCommands.set("testKey", "Hello, Redis!");
        System.out.println(syncCommands.get("testKey"));
        
        connection.close();
        redisClient.shutdown();
    }
}

二 redis 主從

上面我們啓動了一臺redis,並對其進行操作。當然這只是實驗性的玩玩。假設我們生產環境使用了一臺redis,redis掛了怎麼辦?如果等到運維重啓redis,並恢復好數據,可能需要花費很長時間。那麼在這期間,我們的服務是不可用的,這應該是不能容忍的。假設我們做了主從,主庫掛了之後,運維讓從庫接管,那麼服務可以繼續運行,正所謂有備無患。

redis主從配置非常簡單,過程如下(ps 演示情況下主從配置在一臺電腦上):

複製兩個redis配置文件(啓動兩個redis,只需要一份redis程序,兩個不同的redis配置文件即可)

mkdir redis-master-slave
cp path/to/redis/conf/redis.conf path/to/redis-master-slave master.conf
cp path/to/redis/conf/redis.conf path/to/redis-master-slave slave.conf

1 修改配置

## master.conf
port 6379

## master.conf
port 6380
slaveof 127.0.0.1 6379

分別啓動兩個redis

redis-server path/to/redis-master-slave/master.conf
redis-server path/to/redis-master-slave/slave.conf

啓動之後,打開兩個命令行窗口,分別執行telnet localhost 6379 telnet localhost 6380
然後分別在兩個窗口中執行 info 命令,可以看到

# Replication
role:master

# Replication
role:slave
master_host:127.0.0.1
master_port:6379

主從配置沒問題。

然後在master 窗口執行 set 之後,到slave窗口執行get,可以get到,說明主從同步成功。

這時,我們如果在slave窗口執行 set ,會報錯:

-READONLY You can't write against a read only replica.

因爲從節點是隻讀的。

三 哨兵sentinel

上面我們介紹了主從,從庫作爲一個“傀儡”,可以在需要的時候“頂上來”,”接盤“。我們配置的主從是爲了”有備無患“,在主redis掛了之後,可以立馬切換到從redis上,可能只需要花幾分鐘的時間,但是仍然是需要人爲操作。假設主redis在晚上23點掛了,10分鐘之後你接到電話,老闆讓你趕緊修復,於是你從被窩爬起來整,豈不是很頭疼。假如你關機了,又其他人知道服務器密碼,那系統豈不是要停機一晚上?太可怕了。

這個時候redis sentinel 就派上用場了。sentinel 通常翻譯成哨兵,就是放哨的,這裏它就是用來監控主從節點的健康情況。客戶端連接redis主從的時候,先連接 sentinel,sentinel會告訴客戶端主redis的地址是多少,然後客戶端連接上redis並進行後續的操作。當主節點掛掉的時候,客戶端就得不到連接了因而報錯了,客戶端重新想sentinel詢問主master的地址,然後客戶端得到了[新選舉出來的主redis],然後又可以愉快的操作了。

3.2 哨兵sentinel配置

爲了說明sentinel的用處,我們做個試驗。配置3個redis(1主2從),1個哨兵。步驟如下:

mkdir redis-sentinel
cd redis-sentinel
cp redis/path/conf/redis.conf path/to/redis-sentinel/redis01.conf
cp redis/path/conf/redis.conf path/to/redis-sentinel/redis02.conf
cp redis/path/conf/redis.conf path/to/redis-sentinel/redis03.conf
touch sentinel.conf

上我們創建了 3個redis配置文件,1個哨兵配置文件。我們將 redis01設置爲master,將redis02,redis03設置爲slave。

vim redis01.conf
port 63791

vim redis02.conf
port 63792
slaveof 127.0.0.1 63791

vim redis03.conf
port 63793
slaveof 127.0.0.1 63791

vim sentinel.conf
daemonize yes
port 26379
sentinel monitor mymaster 127.0.0.1 63793 1   # 下面解釋含義

上面的主從配置都熟悉,只有哨兵配置 sentinel.conf,需要解釋一下:

mymaster        爲主節點名字,可以隨便取,後面程序裏邊連接的時候要用到
127.0.0.1 63793 爲主節點的 ip,port
1               後面的數字 1 表示選舉主節點的時候,投票數。1表示有一個sentinel同意即可升級爲master

3.3 啓動哨兵,使用jedis連接哨兵操作redis

上面我們配置好了redis主從,1主2從,以及1個哨兵。下面我們分別啓動redis,並啓動哨兵

redis-server path/to/redis-sentinel/redis01.conf
redis-server path/to/redis-sentinel/redis02.conf
redis-server path/to/redis-sentinel/redis03.conf

redis-server path/to/redis-sentinel/sentinel.conf --sentinel

啓動之後,可以分別連接到 3個redis上,執行info查看主從信息。

3.4 編寫程序&運行

下面使用程序來連接哨兵,並操作redis。

   public static void main(String[] args) throws Exception{

        Set<String> hosts = new HashSet<>();
        hosts.add("127.0.0.1:26379");
        //hosts.add("127.0.0.1:36379"); 配置多個哨兵
       
        JedisSentinelPool pool = new JedisSentinelPool("mymaster",hosts);
        Jedis jedis = null;

        for(int i=0 ;i<20;i++){
            Thread.sleep(2000);

            try{

                jedis = pool.getResource();
                String v = randomString();
                jedis.set("hello",v);

                System.out.println(v+"-->"+jedis.get("hello").equals(v));

            }catch (Exception e){
                System.out.println(" [ exception happened]" + e);
            }
        }
    }

程序非常簡單,循環運行20次,連接哨兵,將隨機字符串 set到redis,get結果。打印信息,異常捕獲。

3.5模擬主節點宕機情況

運行上面的程序(注意,在實驗這個效果的時候,可以將sleep時間加長或者for循環增多,以防程序提前停止,不便看整體效果),然後將主redis關掉,模擬redis掛掉的情況。現在主redis爲redis01,端口爲63791

redis-cli -p 63791 shutdown

這個時候如果sentinel沒有設置後臺運行,可以在命令行窗口看到 master切換的情況日誌。

# Sentinel ID is fd0634dc9876ec60da65db5ff1e50ebbeefdf5ce
# +monitor master mymaster 127.0.0.1 63791 quorum 1
* +slave slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791
* +slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791
# +sdown master mymaster 127.0.0.1 63791
# +odown master mymaster 127.0.0.1 63791 #quorum 1/1
# +new-epoch 1
# +try-failover master mymaster 127.0.0.1 63791
# +vote-for-leader fd0634dc9876ec60da65db5ff1e50ebbeefdf5ce 1
# +elected-leader master mymaster 127.0.0.1 63791
# +failover-state-select-slave master mymaster 127.0.0.1 63791
# +selected-slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791
* +failover-state-send-slaveof-noone slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791
* +failover-state-wait-promotion slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791
# +promoted-slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791
# +failover-state-reconf-slaves master mymaster 127.0.0.1 63791
* +slave-reconf-sent slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791
* +slave-reconf-inprog slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791
* +slave-reconf-done slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791
# +failover-end master mymaster 127.0.0.1 63791
# +switch-master mymaster 127.0.0.1 63791 127.0.0.1 63793
* +slave slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63793
* +slave slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793
# +sdown slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793
# -sdown slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793
* +convert-to-slave slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793

上面的日誌較多,仔細找找可以看到下面幾行主要的:

初始情況下,1主2從
# +monitor master mymaster 127.0.0.1 63791 quorum 1
* +slave slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791
* +slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791
發現主掛了,準備 故障轉移
# +try-failover master mymaster 127.0.0.1 63791
將主切換到了 63793 即redis03 
# +switch-master mymaster 127.0.0.1 63791 127.0.0.1 63793

這個日誌比較晦澀,從代碼運行效果看,如下:

14:45:20.675 [main] INFO redis.clients.jedis.JedisSentinelPool - Trying to find master from available Sentinels...
14:45:25.731 [main] DEBUG redis.clients.jedis.JedisSentinelPool - Connecting to Sentinel 192.168.1.106:26379
14:45:25.770 [main] DEBUG redis.clients.jedis.JedisSentinelPool - Found Redis master at 127.0.0.1:63792
14:45:25.771 [main] INFO redis.clients.jedis.JedisSentinelPool - Redis master running at 127.0.0.1:63792, starting Sentinel listeners...
14:45:25.871 [main] INFO redis.clients.jedis.JedisSentinelPool - Created JedisPool to master at 127.0.0.1:63792
ejahaeegig-->true
deeeadejjf-->true
 [ exception happened]redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 [ exception happened]........
 14:46:02.737 [MasterListener-mymaster-[192.168.1.106:26379]] DEBUG redis.clients.jedis.JedisSentinelPool - Sentinel 192.168.1.106:26379 published: mymaster 127.0.0.1 63792 127.0.0.1 63793.
14:46:02.738 [MasterListener-mymaster-[192.168.1.106:26379]] INFO redis.clients.jedis.JedisSentinelPool - Created JedisPool to master at 127.0.0.1:63793
haiihiihbb-->true
ifgebdcicd-->true
aajhbjagag-->true

Process finished with exit code 0

從結果看出

開始正常操作redis,並設置了兩次。
主redis掛了,jedis得不到連接,報錯了JedisConnectionException:Could not get a resource from the pool
主redis沒選好之前,程序持續報錯。
主redis選好了,程序正常運行,最後結束。
我們看到最後一次運行設置的值是aajhbjagag,我們可以連接剩下的2臺redis中的任意一臺,get hello,結果肯定是一致的。

四 redis cluster

redis 單點,redis主從,並增加了高可用的 sentinel 哨兵模式。我們所做的這些工作只是保證了數據備份以及高可用,目前爲止我們的程序一直都是向1臺redis寫數據,其他的redis只是備份而已。實際場景中,單個redis節點可能不滿足要求,因爲:

單個redis併發有限

單個redis接收所有的數據,最終回導致內存太大,內存太大回導致rdb文件過大,從很大的rdb文件中同步恢復數據會很慢。
所有,我們需要redis cluster 即redis集羣。

Redis 集羣是一個提供在多個Redis間節點間共享數據的程序集。

Redis集羣並不支持處理多個keys的命令,因爲這需要在不同的節點間移動數據,從而達不到像Redis那樣的性能,在高負載的情況下可能會導致不可預料的錯誤.

Redis 集羣通過分區來提供一定程度的可用性,在實際環境中當某個節點宕機或者不可達的情況下繼續處理命令. Redis 集羣的優勢:

自動分割數據到不同的節點上。
整個集羣的部分節點失敗或者不可達的情況下能夠繼續處理命令。
爲了配置一個redis cluster,我們需要準備至少6臺redis,爲啥至少6臺呢?我們可以在redis的官方文檔中找到如下一句話:

Note that the minimal cluster that works as expected requires to contain at least three master nodes. 

因爲最小的redis集羣,需要至少3個主節點,既然有3個主節點,而一個主節點搭配至少一個從節點,因此至少得6臺redis。然而對我來說,就是複製6個redis配置文件。本實驗的redis集羣搭建依然在一臺電腦上模擬。

4.1 配置 redis cluster 集羣

上面提到,配置redis集羣需要至少6個redis節點。因此我們需要準備及配置的節點如下:

主:redis01  從 redis02    slaveof redis01
主:redis03  從 redis04    slaveof redis03
主:redis05  從 redis06    slaveof redis05

mkdir redis-cluster
cd redis-cluster
mkdir redis01 到 redis06 6個文件夾
cp redis.conf 到 redis01 ... redis06
修改端口
分別配置3組主從關係

4.2啓動redis集羣

上面的配置完成之後,分別啓動6個redis實例。配置正確的情況下,都可以啓動成功。然後運行如下命令創建集羣:

redis-5.0.3/src/redis-cli --cluster create 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375 127.0.0.1:6376 --cluster-replicas 1

注意,這裏使用的是ip:port,而不是 domain:port ,因爲我在使用 localhost:6371 之類的寫法執行的時候碰到錯誤:

ERR Invalid node address specified: localhost:6371

執行成功之後,連接一臺redis,執行 cluster info 會看到類似如下信息:

cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:1515
cluster_stats_messages_pong_sent:1506
cluster_stats_messages_sent:3021
cluster_stats_messages_ping_received:1501
cluster_stats_messages_pong_received:1515
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:3021

我們可以看到cluster_state:ok,cluster_slots_ok:16384,cluster_size:3。

4.3 使用jedis連接redis cluster 集羣

上面我們配置了一個redis集羣,包含6個redis節點,3主3從。下面我們來使用jedis來連接redis集羣。代碼如下:

    public static void main(String[] args) {

        Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>();
        //Jedis Cluster will attempt to discover cluster nodes automatically
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6371));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6372));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6373));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6374));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6375));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6376));
        JedisCluster jc = new JedisCluster(jedisClusterNodes);
        jc.set("foo", "bar");
        String value = jc.get("foo");
        System.out.println(" ===> " + value);
    }

上面我們設置了信息set foo bar,但是不知道被設置到那一臺redis上去了。請讀者思考一下,我們是集羣模式,所以數據被分散放到不同的槽中了,Redis 集羣有16384個哈希槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽.集羣的每個節點負責一部分hash槽,舉個例子,比如當前集羣有3個節點,那麼:

節點 A 包含 0 到 5500號哈希槽.
節點 B 包含5501 到 11000 號哈希槽.
節點 C 包含11001 到 16384號哈希槽.
看到這裏你應該還是不知道set foo bar 放到哪臺redis上去了,不妨嘗試連接任意一臺redis探索一下,你會知道的。

總結

主從模式的優缺點

優點

做到讀寫分離,提高服務器性能。Salve可以分載Master的讀操作壓力,當然寫服務依然必須由Master來完成;
當Master節點服務掛了,可以讓Slave變成Master節點繼續提供服務;

缺點

在主從模式中,一旦Master節點由於故障不能提供服務,需要人工將Slave節點晉升爲Master節點,同時還要通知應用方更新Master節點地址。顯然,大多數業務場景都不能接受這種故障處理方式;
redis的Master節點和Slave節點中的數據是一樣的,降低的內存的可用性,而且存儲能力也有限。
主從複製寫還都是在Master節點,所以寫的壓力並沒有減少。
因此,主從複製其實並不能滿足我們高可用的要求。

哨兵模式的優缺點

優點

哨兵模式是基於主從模式的,所有主從的優點,哨兵模式都具有。
主從可以自動切換,系統更健壯,可用性更高。

缺點

具有主從模式的缺點,每臺機器上的數據是一樣的,內存的可用性較低。
還要多維護一套哨兵模式,實現起來也變的更加複雜增加維護成本。
Redis較難支持在線擴容,在集羣容量達到上限時在線擴容會變得很複雜。

集羣

哨兵模式基於主從模式,實現讀寫分離,它還可以自動切換,系統可用性更高。但是它每個節點存儲的數據是一樣的,浪費內存。因此,在Redis3.0後Cluster集羣應運而生,

它實現了Redis的分佈式存儲。對數據進行分片,也就是說每臺Redis節點上存儲不同的內容,來解決在線擴容的問題。

在整個redis cluster架構中,如果出現以下情況

  • 新加入節點
  • slot遷移
  • 節點宕機
  • slave選舉成爲master

我們希望這些變化能夠讓整個集羣中的每個節點都能夠儘快發現,傳播到整個集羣並且集羣中所有節點達成一致,那麼各個節點之間就需要相互連通並且攜帶相關狀態數據進行傳播,按照正常的邏輯是採用廣播的方式想集羣中的所有節點發送消息,有點是集羣中的數據同步較快,但是每條消息都需要發送給所有節點,對CPU和帶寬的消耗過大,所以這裏採用了gossip協議。

它的特點是,在節點數量有限的網絡中,每個節點都會“隨機”(不是真正隨機,而是根據規則選擇通信節點)與部分節點通信,經過一番雜亂無章的通信後,每個節點的狀態在一定時間內會達成一致

優點: gossip協議的優點在於元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續,打到所有節點上去更新有一定的延時,降低了壓力; 去中心化、可擴展、

容錯、一致性收斂、簡單。 由於不能保證某個時刻所有節點都收到消息,但是理論上最終所有節點都會收到消息,因此它是一個最終一致性協議。

缺點: 元數據更新有延時可能導致集羣的一些操作會有一些滯後。 消息的延遲 , 消息冗餘 。

回到我們最初的問題:

如何保證 Redis 的高併發和高可用?

一般來說,使用 Redis 主要是用作緩存,如果數據量大,一臺機器肯定是不夠的,肯定要考慮如何用 Redis 來加多臺機器,保證 Redis 是高併發的,還有就是如何讓 Redis 保證自己不是掛掉以後就直接死掉了,即 Redis 高可用。

對於高可用,通過 Redis 主從架構 + 哨兵可以實現高可用,一主多從,任何一個實例宕機,可以進行主備切換。一般來說,很多項目其實就足夠了,單主用來寫入數據,單機幾萬 QPS,多從用來查詢數據,多個從實例可以提供每秒 10w 的 QPS。

對於高併發,那麼就需要 Redis 集羣,多主多從,使用 Redis 集羣之後,可以提供每秒幾十萬的讀寫併發。

by : 一隻阿木木

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章