Redis主從複製
儘管 Redis 的效能很好,但是有時候依舊滿足不了應用的需要,比如過多的使用者進入主頁,導致 Redis 被頻繁訪問,此時就存在大量的讀操作。
對於一些熱門網站的某個時刻(比如促銷商品的時候)每秒成千上萬的請求是司空見慣的,這個時候大量的讀操作就會到達 Redis 伺服器,觸發許許多多的操作,顯然單靠一臺 Redis 伺服器是完全不夠用的。
一些服務網站對安全性有較高的要求,當主伺服器不能正常工作的時候,也需要從伺服器代替原來的主伺服器,作為災備,以保證系統可以繼續正常的工作。
因此更多的時候我們更希望可以讀/寫分離,讀/寫分離的前提是讀操作遠遠比寫操作頻繁得多,如果把資料都存放在多臺伺服器上那麼就可以從多臺伺服器中讀取資料,從而消除了單臺伺服器的壓力,讀/寫分離的技術已經廣泛用於資料庫中了。
主從同步基礎概念
網際網路系統一般是以主從架構為基礎的,所謂主從架構設計的思路大概是:
- 在多臺資料伺服器中,只有一臺主伺服器,而主伺服器只負責寫入資料,不負責讓外部程式讀取資料。
- 存在多臺從伺服器,從伺服器不寫入資料,只負責同步主伺服器的資料,並讓外部程式讀取資料。
- 主伺服器在寫入資料後,即刻將寫入資料的命令傳送給從伺服器,從而使得主從資料同步。
- 應用程式可以隨機讀取某一臺從伺服器的資料,這樣就分攤了讀資料的壓力。
- 當從伺服器不能工作的時候,整個系統將不受影響;當主伺服器不能工作的時候,可以方便地從從伺服器中選舉一臺來當主伺服器。
請注意上面的思路,用了“大概”這兩個字,因為這只是一種大概的思路,每一種資料儲存的軟體都會根據其自身的特點對上面的這幾點思路加以改造,但是萬變不離其宗,只要理解了這幾點就很好理解 Redis 的複製機制了。主從同步機制如圖 1 所示。
圖 1 主從同步機制
這個時候讀資料就可以隨機從伺服器上讀取,當從伺服器是多臺的時候,那麼單臺伺服器的壓力就大大降低了,這十分有利於系統效能的提高,當主伺服器出現不能工作的情況時,也可以切換為其中的一臺從伺服器繼續讓系統穩定執行,所以也有利於系統執行的安全性。當然由於 Redis 自身具備的特點,所以其也有實現主從同步的特殊方式。
Redis 主從同步配置
對 Redis 進行主從同步的配置分為主機與從機,主機是一臺,而從機可以是多臺。
首先,明確主機。當你能確定哪臺機子是主機的時候,關鍵的兩個配置是 dir 和 dbfilename 選項,當然必須保證這兩個檔案是可寫的。對於 Redis 的預設配置而言,dir 的預設值為“./”,而對於 dbfilename 的預設值為“dump.rbd”。換句話說,預設採用 Redis 當前目錄的 dump.rbd 檔案進行同步。對於主機而言,只要瞭解這多資訊,很簡單。
其次,在明確了從機之後,進行進一步配置所要關注的只有 slaveof 這個配置選項,它的配置格式是:
slaveof server port
其中,
- server 代表主機,
- port 代表埠。
當從機 Redis 服務重啟時,就會同步對應主機的資料了。當不想讓從機繼續複製主機的資料時,可以在從機的 Redis 命令客戶端傳送
slaveof no one
命令,這樣從機就不會再接收主伺服器的資料更新了。又或者原來主伺服器已經無法工作了,而你可能需要去複製新的主機,這個時候執行
slaveof sever port
就能讓從機複製另外一臺主機的資料了。
在實際的 Linux 環境中,配置檔案 redis.conf 中還有一個 bind 的配置,預設為 127.0.0.1,也就是隻允許本機訪問,把它修改為 bind 0.0.0.0,其他的伺服器就能夠訪問了。
Redis 主從同步的過程
Redis 主從同步的過程如圖 2 所示。
圖 2 Redis 主從同步
圖 2 中左邊的流程是主伺服器,而右邊的流程為從伺服器,這裡有必要進行更深層次的描述:
- 無論如何要先保證主伺服器的開啟,開啟主伺服器後,從伺服器通過命令或者重啟配置項可以同步到主伺服器。
- 當從伺服器啟動時,讀取同步的配置,根據配置決定是否使用當前資料響應客戶端,然後傳送 SYNC 命令。當主伺服器接收到同步命令的時候,就會執行 bgsave 命令備份資料,但是主伺服器並不會拒絕客戶端的讀/寫,而是將來自客戶端的寫命令寫入緩衝區。從伺服器未收到主伺服器備份的快照檔案的時候,會根據其配置決定使用現有資料響應客戶端或者拒絕。
- 當 bgsave 命令被主伺服器執行完後,開始向從伺服器傳送備份檔案,這個時候從伺服器就會丟棄所有現有的資料,開始載入傳送的快照檔案。
- 當主伺服器傳送完備份檔案後,從伺服器就會執行這些寫入命令。此時就會把 bgsave 執行之後的快取區內的寫命令也傳送給從伺服器,從服務完成備份檔案解析,就開始像往常一樣,接收命令,等待命令寫入。
- 緩衝區的命令傳送完成後,當主伺服器執行一條寫命令後,就同時往從伺服器傳送同步寫入命令,從伺服器就和主伺服器保持一致了。而此時當從伺服器完成主伺服器傳送的緩衝區命令後,就開始等待主伺服器的命令了。
以上 5 步就是 Redis 主從同步的過程。
只是在主伺服器同步到從伺服器的過程中,需要備份檔案,所以在配置的時候一般需要預留一些記憶體空間給主伺服器,用以騰出空間執行備份命令。一般來說主伺服器使用 50%~65% 的記憶體空間,以為主從複製留下可用的記憶體空間。
多從機同步機制,如圖 3 所示。
圖 3 多從機同步機制
如果出現多臺同步,可能會出現頻繁等待和頻繁操作 bgsave 命令的情況,導致主機在較長時間裡效能不佳,這個時候我們會考慮主從鏈進行同步的機制,以減少這種可能。
Redis哨兵(Sentinel)
主從切換技術的方法是:當主伺服器當機後,需要手動把一臺從伺服器切換為主從伺服器,這就需要人工干預,既費時費力,還會造成一段時間內服務不可用,這不是一種推薦的方式,因此筆者沒有介紹主從切換技術。
更多的時候,我們優先考慮哨兵模式,它是當前企業應用的主流方式。
哨兵模式概述
Redis 可以存在多臺伺服器,並且實現了主從複製的功能。哨兵模式是一種特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一個獨立的程式,作為程式,它會獨立執行。
其原理是哨兵通過傳送命令,等待 Redis 伺服器響應,從而監控執行的多個 Redis 例項,如圖 1 所示。
圖 1 Redis哨兵
這裡的哨兵有兩個作用:
- 通過傳送命令,讓 Redis 伺服器返回監測其執行狀態,包括主伺服器和從伺服器。
- 當哨兵監測到 master 當機,會自動將 slave 切換成 master,然後通過釋出訂閱模式通知到其他的從伺服器,修改配置檔案,讓它們切換主機。
只是現實中一個哨兵程式對 Redis 伺服器進行監控,也可能出現問題,為了處理這個問題,還可以使用多個哨兵的監控,而各個哨兵之間還會相互監控,這樣就變為了多個哨兵模式。多個哨兵不僅監控各個 Redis 伺服器,而且哨兵之間互相監控,看看哨兵們是否還“活”著,其關係如圖 2 所示。
論述一下故障切換(failover)的過程。假設主伺服器當機,哨兵 1 先監測到這個結果,當時系統並不會馬上進行 failover 操作,而僅僅是哨兵 1 主觀地認為主機已經不可用,這個現象被稱為主觀下線。
當後面的哨兵監測也監測到了主伺服器不可用,並且有了一定數量的哨兵認為主伺服器不可用,那麼哨兵之間就會形成一次投票,投票的結果由一個哨兵發起,進行 failover 操作,在 failover 操作的過程中切換成功後,就會通過釋出訂閱方式,讓各個哨兵把自己監控的伺服器實現切換主機,這個過程被稱為客觀下線。這樣對於客戶端而言,一切都是透明的。
圖 2 多哨兵監控Redis
搭建哨兵模式
配置 3 個哨兵和 1 主 2 從的 Redis 伺服器來演示這個過程。機器的分配,如表 1 所示:
服務型別 | 是否主伺服器 | IP地址 | 埠 |
---|---|---|---|
Redis | 是 | 192.168.11.128 | 6379 |
Redis | 否 | 192.168.11.129 | 6379 |
Redis | 否 | 192.168.11.130 | 6379 |
Sentinel | —— | 192.168.11.128 | 26379 |
Sentinel | —— | 192.168.11.129 | 26379 |
Sentinel | —— | 192.168.11.130 | 26379 |
它的結構就如同圖 2 所示,下面對它們進行配置。首先配置 Redis 的主從伺服器,修改伺服器的 redis.conf 檔案,下面的配置內容,僅僅是在原有檔案的基礎上修改的內容:
\#使得Redis伺服器可以跨網路訪問
bind 0.0.0.0
\#設定密碼
requiredpass "abcdefg"
\#指定主伺服器,注意:有關slaveof的配置只是配置從伺服器,而主伺服器不需要配置
slaveof 192.168.11.128 6379
\#主伺服器密碼,注意:有關slaveof的配置只是配置從伺服器,而主伺服器不需要配置
masterauth abcdefg
上述內容主要是配置 Redis 伺服器,從伺服器比主伺服器多一個 slaveof 的配置和密碼,這裡配置的 bind 使得 Redis 伺服器可以跨網段訪問。而對於外部的訪問還需要提供密碼,因此還提供了 requirepass 的配置,用以配置密碼,這樣就配置完了 3 臺伺服器。
配置 3 個哨兵,每一個哨兵的配置都是一樣的,在 Redis 安裝目錄下可以找到 sentinel.conf 檔案,然後對其進行修改。
下面對 3 個哨兵的這個檔案作出修改,同樣也是在原有的基礎上進行修改,如下所示。
\#禁止保護模式
protected-mode no
\#配置監聽的主伺服器,這裡 sentinel monitor 代表監控
\#mymaster代表伺服器名稱,可以自定義
\#192.168.11.128代表監控的主伺服器
\#6379代表埠
\#2代表只有兩個或者兩個以上的燒餅認為主伺服器不可用的時候,才會做故障切換操作
sentinel monitor mymaster 192.168.11.128 6379 2
\#sentinel auth-pass 定義服務的密碼
\#mymaster服務名稱
\#abcdefg Redis伺服器密碼
sentinel auth-pass mymaster abcdefg
上述關閉了保護模式,以便測試。sentinel monitor 是配置一個哨兵主要的內容,首先是自定義服務名稱 mymaster,然後對映伺服器和埠。最後的 2 是代表當存在兩個或者兩個以上的哨兵投票認可當前主伺服器不可用後,才會進行故障切換,這樣可以降低因出錯而切換主伺服器的概率。sentinel auth-pass 用於配置服務名稱及其密碼。
有了上述的修改,我們可以進入 Redis 的安裝目錄下的 src 目錄,通過以下命令啟動 Redis 伺服器和哨兵,如下所示:
\#啟動哨兵程式
./redis-sentinel ../sentinel.conf
\#啟動Redis伺服器程式
./redis-server ../redis.conf
只是這裡要注意伺服器啟動的順序,首先是主機(192.168.11.128)的 Redis 服務程式,然後啟動從機的服務程式,最後啟動 3 個哨兵的服務程式。這裡可以從哨兵啟動的輸出視窗看一下哨兵監控資訊,如圖 3 所示。
從圖 3 中,我們看到了一個哨兵對多臺 Redis 伺服器進行了監控。
在 Java 中使用哨兵模式
在 Java 中使用哨兵模式,加入關於哨兵的資訊即可,非常簡單,下面程式碼展示了這樣的一個過程。
//連線池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMaxIdle(5);
jedisPoolConfig.setMinIdle(5);
//哨兵資訊
Set<String> sentinels = new HashSet<String>(Arrays.asList(
"192.168.11.128:26379",
"192.168.11.129:26379",
"192.168.11.130:26379"
));
//建立連線池
//mymaster是我們配置給哨兵的服務名稱
//sentinels是哨兵資訊
//jedisPoolConfig是連線池配置
//abcdefg是連線Redis伺服器的密碼
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig, "abcdefg");
//獲取客戶端
Jedis jedis = pool.getResource();
//執行兩個命令
jedis.set("mykey", "myvalue");
String myvalue = jedis.get("mykey");
//列印資訊
System.out.println(myvalue);
圖 3 哨兵監控資訊
通過上述的程式碼就能夠連線 Redis 伺服器了,這個時候將啟動主機(192.168.11.128)提供服務。為了驗證哨兵的作用,我們可以把主機上的 Redis 伺服器關閉,馬上執行,你就可以發現報錯,那倒不是因為哨兵失效導致的,而是因為 Redis 哨兵預設超時 3 分鐘後才會進行投票切換主機,等超過 3 分鐘後再進行測試。
同樣的,通過配置也可以實現在 Spring 中使用哨兵的這些功能,程式碼如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大空閒數 -->
<property name="maxIdle" value="50" />
<!-- 最大連線數 -->
<property name="maxTotal" value="100" />
<!-- 最大等待時間 -->
<property name="maxWaitMillis" value="30000" />
</bean>
<!-- jdk序列化器,可儲存物件 -->
<bean id="jdkSerializationRedisSerializer"
class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
<!-- String序列化器 -->
<bean id="stringRedisSerializer"
class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<!-- 哨兵配置 -->
<bean id="sentinelConfig"
class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<!-- 服務名稱 -->
<property name="master">
<bean class="org.springframework.data.redis.connection.RedisNode">
<property name="name" value="mymaster" />
</bean>
</property>
<!-- 哨兵服務IP和埠 -->
<property name="sentinels">
<set>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.11.128" />
<constructor-arg name="port" value="26379" />
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.11.129" />
<constructor-arg name="port" value="26379" />
</bean>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.11.130" />
<constructor-arg name="port" value="26379" />
</bean>
</set>
</property>
</bean>
<!-- 連線池設定 -->
<bean id="connectionFectory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg name="sentinelConfig" ref="sentinelConfig" />
<constructor-arg name="poolConfig" ref="poolConfig" />
<property name="password" value="abcdefg" />
</bean>
<!-- 配置 RedisTemplate -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="keySerializer" ref="stringRedisSerializer" />
<property name="defaultSerializer" ref="stringRedisSerializer" />
<property name="valueSerializer" ref="jdkSerializationRedisSerializer" />
</bean>
</beans>
這樣就在 Spring 中配置好了哨兵和其他的內容,使用 Spring 測試哨兵,程式碼如下所示。
public static void testLuaFile() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
String retVal = (String)redisTemplate.execute((RedisOperations ops)->{
ops.boundValueOps("mykey").set("myvalue");
String value = (String)ops.boundValuesOps("mykey").get();
return value;
});
System.out.println(retVal);
}
這樣在實際的專案中,就可以使用哨兵模式來提高系統的可用性和穩定了。
哨兵模式的其他配置項
上述以最簡單的配置完成了哨兵模式。哨兵模式的配置項還是比較有用的,比如上述我們需要等待 3 分鐘後,Redis 哨兵程式才會做故障切換,有時候我們希望這個時間短一些,下面再對它們進行一些介紹,如表 2 所示。
配置項 | 引數型別 | 作用 |
---|---|---|
port | 整數 | 啟動哨兵程式埠 |
dir | 資料夾目錄 | 哨兵程式服務臨時資料夾,預設為 /tmp,要保證有可寫入的許可權 |
sentinel down-after-milliseconds | <服務名稱><亳秒數(整數)> | 指定哨兵在監測 Redis 服務時,當 Redis 服務在一個亳秒數內都無 法回答時,單個哨兵認為的主觀下線時間,預設為 30000(30秒) |
sentinel parallel-syncs | <服務名稱><伺服器數(整數)> | 指定可以有多少 Redis 服務同步新的主機,一般而言,這個數字越 小同步時間就越長,而越大,則對網路資源要求則越高 |
sentinel failover-timeout | <服務名稱><亳秒數(整數)> | 指定在故障切換允許的亳秒數,當超過這個亳秒數的時候,就認為 切換故障失敗,預設為 3 分鐘 |
sentinel notification-script | <服務名稱><指令碼路徑> | 指定 sentinel 檢測到該監控的 redis 例項指向的例項異常時,呼叫的 報警指令碼。該配置項可選,比較常用 |
sentinel down-after-milliseconds 配置項只是一個哨兵在超過其指定的毫秒數依舊沒有得到回答訊息後,會自己認為主機不可用。對於其他哨兵而言,並不會認為主機不可用。
哨兵會記錄這個訊息,當擁有認為主觀下線的哨兵到達 sentinel monitor 所配置的數量的時候,就會發起一次新的投票,然後切換主機,此時哨兵會重寫 Redis 的哨兵配置檔案,以適應新場景的需要