Redis解讀(5):Redis深入理解及生產高可用
Redis單線程如何處理高并發(fā)
1.阻塞IO 與 非阻塞 IO
Java 在 JDK1.4 中引入 NIO,但是也有很多人在使用阻塞 IO,這兩種 IO 有什么區(qū)別?
在阻塞模式下,如果你從數據流中讀取不到指定大小的數據兩,IO 就會阻塞。比如已知會有 10 個字節(jié)發(fā)送過來,但是我目前只收到 4 個,還剩六個,此時就會發(fā)生阻塞。如果是非阻塞模式,雖然此時只收到 4 個字節(jié),但是讀到 4 個字節(jié)就會立即返回,不會傻傻等著,等另外 6 個字節(jié)來的時候,再去繼續(xù)讀取。
所以阻塞 IO 性能低于 非阻塞 IO。
如果有一個 Web 服務器,使用阻塞 IO 來處理請求,那么每一個請求都需要開啟一個新的線程;但是如果使用了非阻塞 IO,基本上一個小小線程池就夠用了,因為不會發(fā)生阻塞,每一個線程都能夠高效利用。
2.Redis 的線程模型
首先一點,Redis 是單線程。單線程如何解決高并發(fā)問題的?
實際上,能夠處理高并發(fā)的單線程應用不僅僅是 Redis,除了 Redis 之外,還有 NodeJS、Nginx 等等也是單線程。
Redis 雖然是單線程,但是運行很快,主要有如下幾方面原因:
- Redis 中的所有數據都是基于內存的,所有的計算也都是內存級別的計算,所以快。
- Redis 是單線程的,所以有一些時間復雜度高的指令,可能會導致 Redis 卡頓,例如 keys。
- Redis 在處理并發(fā)的客戶端連接時,使用了非阻塞 IO。
- 在使用非阻塞 IO 時,有一個問題,就是線程如何知道剩下的數據來了?這里就涉及到一個新的概念叫做多路復用,本質上就是一個事件輪詢 API。
- Redis 會給每一個客戶端指令通過隊列來排隊進行順序處理,Redis 做出響應時,也會有一個響應的隊列。
Redis的通信協(xié)議
Redis 通信使用了文本協(xié)議,文本協(xié)議比較費流量,但是 Redis 作者認為數據庫的瓶頸不在于網絡流量,而在于內部邏輯,所以采用了這樣一個費流量的文本協(xié)議。
這個文本協(xié)議叫做 Redis Serialization Protocol,簡稱 RESP
Redis 協(xié)議將傳輸的數據結構分為 5 種最小單元,單元結束時,加上回車換行符 \r\n。
- 單行字符串以 + 開始,例如 +javaboy.org\r\n
- 多行字符串以 $ 開始,后面加上字符串長度,例如 $11\r\njavaboy.org\r\n
- 整數值以: 開始,例如 :1024\r\n
- 錯誤消息以 - 開始
- 數組以 * 開始,后面加上數組長度
需要注意的是,如果是客戶端連接服務端,只能使用第 5 種
1.準備工作
做兩件事情:
為了方便客戶端連接 Redis,我們關閉 Redis 種的保護模式(在 redis.conf 文件中)
protected-mode no
同時關閉密碼:
# requirepass xxxx
配置完成后,重啟Redis
2.實戰(zhàn)
接下來,我們通過 Socket+RESP 來定義兩個最最常見的命令 set 和 get
package org.taoguoguo.socket;
import java.io.IOException;
import java.net.Socket;
/**
* @author taoguoguo
* @description RedisClient
* @website http://www.rzrgm.cn/doondo
* @create 2021-04-26 10:04
*/
public class RedisClient {
private Socket socket;
public RedisClient() {
try {
socket = new Socket("192.168.199.229",6379);
} catch (IOException e) {
e.printStackTrace();
System.out.println("Redis連接失敗");
}
}
/**
* 執(zhí)行 Redis 中的 set 命令 [set,key,value]
* @param key
* @param value
* @return
*/
public String set(String key, String value) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("*3")
.append("\r\n")
.append("$")
.append("set".length())
.append("\r\n")
.append("set")
.append("\r\n")
.append("$")
.append(key.getBytes().length)
.append("\r\n")
.append(key)
.append("\r\n")
.append("$")
.append(value.getBytes().length)
.append("\r\n")
.append(value)
.append("\r\n");
socket.getOutputStream().write(sb.toString().getBytes());
byte[] buf = new byte[1024];
socket.getInputStream().read(buf);
return new String(buf);
}
public String get(String key) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("*2")
.append("\r\n")
.append("$")
.append("get".length())
.append("\r\n")
.append("get")
.append("\r\n")
.append("$")
.append(key.getBytes().length)
.append("\r\n")
.append(key)
.append("\r\n");
socket.getOutputStream().write(sb.toString().getBytes());
byte[] buf = new byte[1024];
socket.getInputStream().read(buf);
return new String(buf);
}
public static void main(String[] args) throws IOException {
RedisClient redisClient = new RedisClient();
redisClient.set("k1", "v1");
String k1 = redisClient.get("k1");
System.out.println("k1的值: " + k1);
}
}
Redis持久化
Redis 是一個緩存工具,也叫做 NoSQL 數據庫,既然是數據庫,必然支持數據的持久化操作。在 Redis中,數據庫持久化一共有兩種方案:
-
快照方式
快照采用一次全量備份,快照采用內存數據二進制序列化的形式,在存儲上非常的簡促,非常省空間。
-
AOF 日志
AOF日志是連續(xù)的增量備份,AOF記錄內存修改的指定的記錄文本,日志在長期的記錄過程中會變得越來越大,所以數據庫重啟時,如果需要加載AOF日志進行指令重放,時間就會比較漫長,因為原理是通過日志把你曾經執(zhí)行過的命令挨個再執(zhí)行一遍,所以耗費時間長,所以一般我們需要定期對AOF日志進行重寫瘦身。
1.RDB快照
1.1 原理
redis 是一個單線程程序,那這個程序要同時負責多個客戶端的并發(fā)讀寫操作,還有內存數據的讀寫。這么多指令同時做是如何執(zhí)行的呢?可能互相之間會有影響,Redis是如何實現的呢?
Redis 使用操作系統(tǒng)的多進程機制來實現快照持久化:Redis 在持久化時,會調用 glibc 函數 fork 一個子進程,然后將快照持久化操作完全交給子進程去處理,而父進程則繼續(xù)處理客戶端請求。在這個過程中,子進程能夠看到的內存中的數據在子進程產生的一瞬間就固定下來了,再也不會改變,也就是為什么 Redis 持久化叫做 快照。
1.2 具體配置
在 Redis 中,默認情況下,快照持久化的方式就是開啟的。
默認情況下會產生一個 dump.rdb 文件,這個文件就是備份下來的文件。當 Redis 啟動時,會自動的去加載這個 rdb 文件,從該文件中恢復數據。如果刪除這個文件,重啟 redis,之前 dump.rdb 中持久化的數據就丟失了。
具體的配置,在 redis.conf 文件中
#快照頻率 You can set these explicitly by uncommenting the three following lines
#900秒內至少有1個鍵被更改進行快照
save 900 1
#300秒至少有10個鍵被更改進行快照
save 300 100
#60秒內至少有10000個鍵被更改進行快照
save 60 10000
#快照執(zhí)行出錯后,是否繼續(xù)處理客戶端的寫命令
stop-writes-on-bgsave-error yes
# 是否對快照文件進行壓縮
rdbcompression yes
# 表示生成的快照文件名
dbfilename dump.rdb
# 表示生成的快照文件位置
dir ./
1.3 備份流程
- 在 Redis 運行過程中,我們可以向 Redis 發(fā)送一條 save 命令來創(chuàng)建一個快照。但是需要注意,save 是一個阻塞命令,Redis 在收到 save 命令開始處理備份操作之后,在處理完成之前,將不再處理其他的請求。其他命令會被掛起,所以 save 使用的并不多。
- 我們一般可以使用 bgsave,bgsave 會 fork 一個子進程去處理備份的事情,不影響父進程處理客戶端請求。
- 我們定義的備份規(guī)則,如果有規(guī)則滿足,也會自動觸發(fā) bgsave。
- 另外,當我們執(zhí)行 shutdown 命令時,也會觸發(fā) save 命令,備份工作完成后,Redis 才會關閉。
- 用 Redis 搭建主從復制時,在 從機連上主機之后,會自動發(fā)送一條 sync 同步命令,主機收到命令之后,首先執(zhí)行 bgsave 對數據進行快照,然后才會給從機發(fā)送快照數據進行同步。
2.AOF日志
與快照持久化不同,AOF 持久化是將被執(zhí)行的命令追加到 aof 文件末尾,在恢復時,只需要把記錄下來的命令從頭到尾執(zhí)行一遍即可。
默認情況下,AOF 是沒有開啟的。我們需要手動開啟
# 開啟 aof 配置
appendonly yes
# AOF 文件名
appendfilename "appendonly.aof"
# 備份的時機,下面的配置表示每秒鐘備份一次
appendfsync everysec
# 表示 aof 文件在壓縮時,是否還繼續(xù)進行同步操作
no-appendfsync-on-rewrite no
# 表示當目前 aof 文件大小超過上一次重寫時的 aof 文件大小的百分之多少的時候,再次進行重寫
auto-aof-rewrite-percentage 100
# 如果之前沒有重寫過,則以啟動時的 aof 大小為依據,同時要求 aof 文件至少要大于 64M
auto-aof-rewrite-min-size 64mb
同時為了避免快照備份的影響,記得將快照備份關閉:
save ""
#save 900 1
#save 300 10
#save 60 10000
手動重寫AOF文件
BGREWRITEAOF
#在滿足AOF規(guī)則時,會自動重寫 BGREWRITEAOF 命令
3.如何選擇哪種快照方式
在實際生產環(huán)境中,根據數據量、應用對數據的安全要求、預算限制和業(yè)務場景等不同情況,會有各種各樣的持久化策略;
- 如果 Redis 僅僅做緩存服務器,一般來說不必太過于太在乎兩者數據,不是說做緩存一定不用這兩者,可能也會用到
- 如果同時兩種持久化方式RDB快照和AOF日志都開啟了,當Redis重啟時會優(yōu)先載入AOF的文件來恢復原始的數據,因為在通常情況下,AOF的文件保存的數據集要比RDB文件保存的數據集要完整,RDB數據不完整時,服務器重啟也只會優(yōu)先找AOF文件。
- 那有小伙伴就疑惑了,那我直接用AOF得了。但 Redis 作者實際不推薦這種做法,因為RDB快照更適合用于備份數據庫、快速重啟等。
- 同時由于RDB文件通常用于后備用途,所以一般在從機上做RDB文件備份,并且通常15分鐘備份一次即可。
- 使用AOF的好處是,最壞情況下也只會丟失大概一秒鐘的數據,并且腳本簡單,只需要load自己的AOF文件。但代價是帶來了持續(xù)的IO,因為需要不停的去讀寫文件。AOF還有一個很大的劣勢,就是在重寫過程中產生的新數據和新文件,造成的阻塞幾乎是不可避免的。所以如果硬盤許可時,應當盡量避免AOF的頻率。應當結合設備性能和具體項目中的數據進行配置AOF文件大小,通常要設置幾個G以上。
- 使用Redis主從結構也可以實現高性能、高可用。
Redis事務
正常來說,一個可以商用的數據庫往往都有比較完善的事務支持,Redis 當然也不例外。相對于 關系型數據庫中的事務模型,Redis 中的事務要簡單很多。因為簡單,所以 Redis 中的事務模型不太嚴格,所以我們不能像使用關系型數據庫中的事務那樣來使用 Redis。
在關系型數據庫中,和事務相關的三個指令分別是:
- begin 開啟事務
- commit 提交事務
- rollback 事務回滾
在 Redis 中,當然也有對應的指令:
- multi 開啟事務
- exec 執(zhí)行事務
- discard 放棄事務
1.原子性
Redis 中的事務并不能算作原子性。它僅僅具備隔離性,也就是說當前的事務可以不被其他事務打斷
由于每一次事務操作涉及到的指令還是比較多的,為了提高執(zhí)行效率,我們在使用客戶端的時候,可以通過 pipeline 來優(yōu)化指令的執(zhí)行。
Redis 中還有一個 watch 指令,watch 可以用來監(jiān)控一個 key,通過這種監(jiān)控,我們可以確保在 exec之前,watch 的鍵的沒有被修改過。相當于樂觀鎖,A用戶操作這個鍵時,B用戶不可修改該鍵,否則事務提交失敗。
操作示例:
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> clear
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) OK
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379>
2.Java 實現
package trans;
import org.taoguoguo.redis.Redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
/**
* @author taoguoguo
* @description RedisTransaction
* @website http://www.rzrgm.cn/doondo
* @create 2021-04-26 14:59
*/
public class RedisTransaction {
public static void main(String[] args) {
new Redis().execute(jedis -> {
new RedisTransaction().saveMoney(jedis, "taoguoguo", 1000);
});
}
public Integer saveMoney(Jedis jedis, String userId, Integer money) {
while (true) {
jedis.watch(userId);
int v = Integer.parseInt(jedis.get(userId)) + money;
Transaction tx = jedis.multi();
tx.set(userId, String.valueOf(v));
List<Object> exec = tx.exec();
if (exec != null) {
break;
}
}
return Integer.parseInt(jedis.get(userId));
}
}
Redis 主從同步
1.CAP
在分布式環(huán)境下,CAP 原理是一個非常基礎的東西,所有的分布式存儲系統(tǒng),都只能在 CAP 中選擇兩項實現。
- c:consistent 一致性
- a:availability 可用性
- p:partition tolerance 分布式容忍性
在一個分布式系統(tǒng)中,這三個只能滿足兩個:在一個分布式系統(tǒng)中,P 肯定是要實現的,如果P都不實現那就不是分布式系統(tǒng)了。c 和 a 只能選擇其中一個。大部分情況下,大多數網站架構選擇了 ap,在某段時間內可能數據不一致,但會努力最終一致。
CAP場景:假設我現在兩臺Redis服務器,分別在長沙和株洲。我要保證可用性,那么如果長沙和株洲網絡通信斷了,那么數據就會有延遲同步,就不能保證數據的一致性。如果我們保證數據的一致性,長沙和株洲的網絡通信斷了,暫時不提供服務,等到網絡恢復,啟動服務數據還是一致的,所以三個只能滿足兩個。
在 Redis 中,實際上就是保證最終一致性。
Redis 中,當搭建了主從服務之后,如果主從之間的連接斷開了,Redis 依然是可以操作的,相當于它滿足可用性,但是此時主從兩個節(jié)點中的數據會有差異,相當于犧牲了一致性。但是 Redis 保證最終一致,就是說當網絡恢復的時候,從機會追趕主機,盡量保持數據一致。
2.主從復制
主從復制可以在一定程度上擴展 redis 性能,redis 的主從復制和關系型數據庫的主從復制類似,從機能夠精確的復制主機上的內容。實現了主從復制之后,一方面能夠實現數據的讀寫分離,降低master的壓力,另一方面也能實現數據的備份。
2.1配置方式
假設我有三個redis實例,地址分別如下:
192.168.199.228:6379
192.168.199.228:6380
192.168.199.228:6381
即同一臺服務器上三個實例,配置方式如下:
- 將 redis.conf 文件更名為 redis6379.conf,方便我們區(qū)分,然后把 redis6379.conf 再復制兩份,分別為 redis6380.conf 和 redis6381.conf。如下:

-
打開 redis6379.conf,將如下配置均加上 6379,(默認是6379的不用修改,如果不同機器也可以不用改),如下:
#如果是多機多節(jié)點 那不同ip 端口可以相同 就可以不用改 port 6379 pidfile /var/run/redis_6379.pid logfile "6379.log" dbfilename dump6379.rdb appendfilename "appendonly6379.aof" -
同理,分別打開 redis6380.conf 和 redis6381.conf 兩個配置文件,將第二步涉及到 6379 的分別改為 6380 和 6381。
#1.編輯6380.conf vim 6380.conf #2.輸入 / 查找符號,然后刪除 輸入替換正則進行全量替換 :%s/6379/6380/g #3.保存退出 :wq! #6481.conf同理修改 -
輸入如下命令,啟動三個redis實例:
[root@localhost redis-4.0.8]# redis-server redis6379.conf [root@localhost redis-4.0.8]# redis-server redis6380.conf [root@localhost redis-4.0.8]# redis-server redis6381.conf -
輸入如下命令,分別進入三個實例的控制臺:
[root@localhost redis-4.0.8]# redis-cli -p 6379 -a xxxxxx [root@localhost redis-4.0.8]# redis-cli -p 6380 -a xxxxxx [root@localhost redis-4.0.8]# redis-cli -p 6381 -a xxxxxx此時我就成功配置了三個redis實例了。
-
假設在這三個實例中,6379 是主機,即 master,6380 和 6381 是從機,即 slave,那么如何配置這種實例關系呢,很簡單,分別在 6380 和 6381 上執(zhí)行如下命令:
#在從機節(jié)點上分別 使用 SLAVEOF 附屬主機 使用該命令,redis節(jié)點重啟后,本身依舊為主機,不回作為從機附屬 127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 OK 127.0.0.1:6381> SLAVEOF 127.0.0.1 6379 OK #要注意的是 雖然附屬了 我們此時在主機設置數據,從機還是同步不到。為什么呢?因為我們的主機有密碼,從機每次連接都需要密碼,否則訪問失敗。且在生產環(huán)境上我們出于安全性考慮,也都是要設置密碼的。修改從機的redis.conf文件,我這邊以redis6380.conf為例子,6381節(jié)點同理。 #1.編輯對應節(jié)點配置文件 [root@192 redis-6.2.1]# vim redis6380.conf #2.配置主機認證密碼 masterauth xxxxxx # If the master is password protected (using the "requirepass" configuration # directive below) it is possible to tell the replica to authenticate before # starting the replication synchronization process, otherwise the master will # refuse the replica request. masterauth 123456 #3.重啟對應節(jié)點 然后重新附屬主機 127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 OK #4.使用 infp replication 查看主從關系 127.0.0.1:6379> INFO replication # Replication role:master connected_slaves:2 slave0:ip=127.0.0.1,port=6380,state=online,offset=56,lag=1 slave1:ip=127.0.0.1,port=6381,state=online,offset=56,lag=0 master_replid:26ca818360d6510b717e471f3f0a6f5985b6225d master_replid2:0000000000000000000000000000000000000000 master_repl_offset:56 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:56我們可以看到 6379 是一個主機,上面掛了兩個從機,兩個從機的地址、端口等信息都展現出來了。如
果我們在 6380 上執(zhí)行 INFO replication,顯示信息如下127.0.0.1:6380> INFO replication # Replication role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up master_last_io_seconds_ago:6 master_sync_in_progress:0 slave_repl_offset:630 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:26ca818360d6510b717e471f3f0a6f5985b6225d master_replid2:0000000000000000000000000000000000000000 master_repl_offset:630 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:630我們可以看到 6380 是一個從機,從機的信息以及它的主機的信息都展示出來了。
-
此時,我們在主機中存儲一條數據,在從機中就可以 get 到這條數據了。
2.2主從復制注意點
-
如果主機已經運行了一段時間了,并且了已經存儲了一些數據了,此時從機連上來,那么從機會將
主機上所有的數據進行備份,而不是從連接的那個時間點開始備份 -
使用
SLAVEOF IP Port進行主從配置,節(jié)點重啟后,從機自身依舊是Master身份,不會自動附屬主機,如果想要重啟后自動附屬主機形成主從關系,需要修改對應節(jié)點 redis.conf 文件中 replicaof <masterip> <masterport> 建立主從,配置如下:# # +------------------+ +---------------+ # | Master | ---> | Replica | # | (receive writes) | | (exact copy) | # +------------------+ +---------------+ # # 1) Redis replication is asynchronous, but you can configure a master to # stop accepting writes if it appears to be not connected with at least # a given number of replicas. # 2) Redis replicas are able to perform a partial resynchronization with the # master if the replication link is lost for a relatively small amount of # time. You may want to configure the replication backlog size (see the next # sections of this file) with a sensible value depending on your needs. # 3) Replication is automatic and does not need user intervention. After a # network partition replicas automatically try to reconnect to masters # and resynchronize with them. # # replicaof <masterip> <masterport> replicaof 127.0.0.1 6379 -
配置了主從復制之后,主機上可讀可寫,但是從機只能讀取不能寫入(可以通過修改redis.conf 中 slave-read-only 的值讓從機也可以執(zhí)行寫操作),一般都是主機讀寫,從機可讀,很少需求會用到從機寫。
-
在整個主從結構運行過程中,如果主機不幸掛掉,重啟之后,他依然是主機,主從復制操作也能夠繼續(xù)進行。
2.3主從復制原理
每一個 master 都有一個 replication ID,這是一個較大的偽隨機字符串,標記了一個給定的數據集。每個 master 也持有一個偏移量,master 將自己產生的復制流發(fā)送給 slave 時,發(fā)送多少個字節(jié)的數據,自身的偏移量就會增加多少,目的是當有新的操作修改自己的數據集時,它可以以此更新 slave 的狀態(tài)。復制偏移量即使在沒有一個 slave 連接到 master 時,也會自增,所以基本上每一對給定的Replication ID, offset 都會標識一個 master 數據集的確切版本。當 slave 連接到 master 時,它們使用PSYNC 命令來發(fā)送它們記錄的舊的 master replication ID 和它們至今為止處理的偏移量。通過這種方式,master 能夠僅發(fā)送 slave 所需的增量部分。但是如果 master 的緩沖區(qū)中沒有足夠的命令積壓緩沖記錄,或者如果 slave 引用了不再知道的歷史記錄(replication ID),則會轉而進行一個全量重同步:在這種情況下,slave 會得到一個完整的數據集副本,從頭開始(參考redis官網)。
簡單來說,就是以下幾個步驟:
- slave 啟動成功連接到 master 后會發(fā)送一個 sync 命令。
- Master 接到命令啟動后臺的存盤進程,同時收集所有接收到的用于修改數據集命令。
- 在后臺進程執(zhí)行完畢之后,master 將傳送整個數據文件到 slave,以完成一次完全同步。
- 全量復制:而 slave 服務在接收到數據庫文件數據后,將其存盤并加載到內存中。
- 增量復制:Master 繼續(xù)將新的所有收集到的修改命令依次傳給 slave,完成同步。
- 但是只要是重新連接 master,就會先來一次全量,后續(xù)增量同步(全量復制)將被自動執(zhí)行
2.4 接力賽(薪火相傳)
我們上面已經完成了基本的主從搭建,一主二仆,兩個從機都是連接在一個主機上的,這樣的連接方式對主機造成的壓力比較大,如果一個主機連接很多從機的時候,它的同步可能延時非常高。所以還有另外一種結構,我們同步的時候可以從從機上去同步。比如讓 6380 作為 6379 的從機去同步 7379 的數據,讓 6381 作為 6380的從機 同步6380 的數據,依此類推往下接,這也是一種搭建思路。
主從復制的兩種搭建結構:
- 一主二仆結構:

- 接力賽結構:

搭建方式很簡單,在前文基礎上,我們只需要修改 6381 的 master 即可,在 6381 實例上執(zhí)行如下命令,讓 6381 從 6380 實例上復制數據,如下:
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
OK
此時,我們再看 6379 的 slave,如下:
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=0,lag=1
master_replid:4a38bbfa37586c29139b4ca1e04e8a9c88793651
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
只有一個 slave,就是 6380,我們再看 6380 的信息,如下:
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:70
slave_priority:100
slave_read_only:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=70,lag=0
master_replid:4a38bbfa37586c29139b4ca1e04e8a9c88793651
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
6380 此時的角色是一個從機,它的主機是 6379,但是 6380 自己也有一個從機,那就是 6381.此時我們的主從結構如下圖:

2.5 哨兵模式
我們一共介紹了兩種主從模式了,但是這兩種,不管是哪一種,都會存在這樣一個問題,那就是當主機宕機時,就會發(fā)生群龍無首的情況,如果在主機宕機時,能夠從從機中選出一個來充當主機,那么就不用我們每次去手動重啟主機了,這就涉及到一個新的話題,那就是哨兵模式。
所謂的哨兵模式,其實并不復雜,我們還是在我們前面的基礎上來搭建哨兵模式。假設現在我的master 是 6379,兩個從機分別是 6380 和 6381,兩個從機都是從 6379 上復制數據。先按照上文的步驟,我們配置好一主二仆,然后在 redis 目錄下打開 sentinel.conf 文件,做如下配置:
#配置監(jiān)控的主機
sentinel monitor mymaster 127.0.0.1 6379 1
#主機的訪問密碼
sentinel auth-pass mymaster 123456
其中 mymaster 是給要監(jiān)控的主機取的名字,隨意取,后面是主機地址,最后面的 2 表示有多少個sentinel 認為主機掛掉了,就進行切換(我這里只有一個,因此設置為1)。好了,配置完成后,輸入如下命令啟動哨兵:
redis-sentinel sentinel.conf
然后啟動我們的一主二仆架構,啟動成功后,關閉 master,觀察哨兵窗口輸出的日志,如下:

可以看到,6379 掛掉之后,redis 內部重新舉行了選舉,6380 重新上位。此時,如果 6379重啟,也不再是主機角色了,只能屈身做一個 slave 了。
2.6 注意問題
由于所有的寫操作都是先在 Master 上操作,然后同步更新到 Slave 上,所以從 Master 同步到 Slave機器有一定的延遲,當系統(tǒng)很繁忙的時候,延遲問題會更加嚴重,Slave 機器數量的增加也會使這個問題更加嚴重,因此后續(xù)我們還需要集群來進一步提升 redis 性能。
3.Jedis 操作哨兵模式
準備工作:
-
所有的實例均配置 masterauth (在 redis.conf 配置文件中)
-
所有實例均需要配置綁定地址:bind 192.168.91.128
另外,哨兵配置的時候,監(jiān)控的 master 也不要直接寫 127.0.0.1,按如下方式寫:
sentinel monitor mymaster 192.168.91.128 6380 1
-
做好準備工作,然后啟動三個 redis 實例,同時啟動哨兵
public class Sentinel { public static void main(String[] args) { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(10); config.setMaxWaitMillis(1000); String master = "mymaster"; Set<String> sentinels = new HashSet<>(); sentinels.add("192.168.91.128:26379"); JedisSentinelPool sentinelPool = new JedisSentinelPool(master, sentinels, config, "javaboy"); Jedis jedis = null; while (true) { try { jedis = sentinelPool.getResource(); String k1 = jedis.get("k1"); System.out.println(k1); } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }在Jedis 客戶端取值過程中,如果手動停掉一個Redis節(jié)點,那我們客戶端是會短暫的報錯的。等Redis選舉完成后,客戶端就可以正常的獲取值了。
4.Spring Boot 操作哨兵模式
SpringBoot 操作哨兵模式和 Jedis 的前提條件相同,配置相比起來反而更簡單。
配置 Redis 連接:
spring:
redis:
password: javaboy
timeout: 5000
sentinel:
master: mymaster
nodes: 192.168.91.128:26379
測試代碼:
@SpringBootTest
class SentinelApplicationTests {
@Autowired
StringRedisTemplate redisTemplate;
@Test
void contextLoads() {
while (true) {
try {
String k1 = redisTemplate.opsForValue().get("k1");
System.out.println(k1);
} catch (Exception e) {
} finally {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Redis 集群
集群原理
Redis 集群架構如下圖:

Redis 集群運行原理如下:
- 所有的 Redis 節(jié)點彼此互聯(PING-PONG機制),內部使用二進制協(xié)議優(yōu)化傳輸速度和帶寬
- 節(jié)點的 fail 是通過集群中超過半數的節(jié)點檢測失效時才生效
- 客戶端與 Redis 節(jié)點直連,不需要中間 proxy 層,客戶端不需要連接集群所有節(jié)點,連接集群中任何一個可用節(jié)點即可
- Redis-cluster 把所有的物理節(jié)點映射到 [0-16383]slot 上,cluster (簇)負責維護 node<->slot<->value 。Redis 集群中內置了 16384 個哈希槽,當需要在 Redis 集群中放置一個key-value 時,Redis 先對 key 使用 crc16 算法算出一個結果,然后把結果對 16384 求余數,這樣每個 key 都會對應一個編號在 0-16383 之間的哈希槽,Redis 會根據節(jié)點數量大致均等的將哈希槽映射到不同的節(jié)點
怎么樣投票
投票過程是集群中所有 master 參與,如果半數以上 master 節(jié)點與 master 節(jié)點通信超過 cluster-node-timeout 設置的時間,認為當前 master 節(jié)點掛掉。
怎么樣判定節(jié)點不可用
- 如果集群任意 master 掛掉,且當前 master 沒有 slave.集群進入 fail 狀態(tài),也可以理解成集群的 slot映射 [0-16383] 不完整時進入 fail 狀態(tài)
- 如果集群超過半數以上 master 掛掉,無論是否有 slave,集群進入 fail 狀態(tài),當集群不可用時,所有對集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)錯誤
集群搭建
在之前上海老東家的時候,那時候用的還是低于 redis 3.x 的版本,搭建集群還需要使用 ruby 環(huán)境,redis 5.0后,將 ruby 整合進了redis-cli中,集群搭建進一步簡化,下面就帶大家搭建一個 三主三從 Redis 集群
-
在指定目錄創(chuàng)建 redis-cluster 文件夾,并且將 在此文件夾下解壓安裝 redis
1. cd /home 2. mkdir redis-cluster 3. cd redis-cluster 4. tar -zxvf redis-6.2.1.tar.gz 5. cd redis-6.2.1 6. make 7. make install -
回到 redis-cluster 目錄下,建立集群各個節(jié)點的配置文件夾 這里以7001 - 7006 為例子 ,并將剛剛安裝好的 redis 中的配置文件拷貝至各個節(jié)點文件夾中
1. mkdir 700{1,2,3,4,5,6} 2. cp redis-6.2.1/redis.conf 7001/ cp redis-6.2.1/redis.conf 7002/ cp redis-6.2.1/redis.conf 7003/ cp redis-6.2.1/redis.conf 7004/ cp redis-6.2.1/redis.conf 7005/ cp redis-6.2.1/redis.conf 7006/ -
拷貝完成后修改各個節(jié)點文件夾中 redis.conf 配置,修改內容如下
port xxxx(修改為具體節(jié)點端口,7001就填7001,7002就填7002) #bind 127.0.0.1 (此處修改為具體節(jié)點所在的ip地址) #開啟集群及配置對應節(jié)點配置文件 cluster-enabled yes cluster-config-file nodes-7001.conf (此處7001也修改為對應端口) #關閉訪問保護 后臺運行 protected no daemonize yes #開啟密碼 requirepass 123456 #開啟主機授權密碼(作為從機連接時使用) masterauth 123456 -
啟動各節(jié)點 redis 服務
1.redis-server ../7001/redis.conf -h 192.168.0.105 -p 7001 -a 123456 2.redis-server ../7002/redis.conf -h 192.168.0.105 -p 7002 -a 123456 3.redis-server ../7003/redis.conf -h 192.168.0.105 -p 7003 -a 123456 4.redis-server ../7004/redis.conf -h 192.168.0.105 -p 7004 -a 123456 5.redis-server ../7005/redis.conf -h 192.168.0.105 -p 7005 -a 123456 6.redis-server ../7006/redis.conf -h 192.168.0.105 -p 7006 -a 123456 -
創(chuàng)建集群
#建立集群 并且集群副本為1(6個節(jié)點 三個主機三個從機) [root@localhost redis-6.2.1]# redis-cli --cluster create 192.168.0.105:7001 192.168.0.105:7002 192.168.0.105:7003 192.168.0.105:7004 192.168.0.105:7005 192.168.0.105:7006 --cluster-replicas 1 -a 123456 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Performing hash slots allocation on 6 nodes... Master[0] -> Slots 0 - 5460 Master[1] -> Slots 5461 - 10922 Master[2] -> Slots 10923 - 16383 Adding replica 192.168.0.105:7005 to 192.168.0.105:7001 Adding replica 192.168.0.105:7006 to 192.168.0.105:7002 Adding replica 192.168.0.105:7004 to 192.168.0.105:7003 >>> Trying to optimize slaves allocation for anti-affinity [WARNING] Some slaves are in the same host as their master M: 9397d9050fc5db96ad3561c579307fbbdd534aff 192.168.0.105:7001 slots:[0-5460] (5461 slots) master M: 1d84e7ac4fce694d5cdc354cf447209caded41a7 192.168.0.105:7002 slots:[5461-10922] (5462 slots) master M: 32afb011852dc446464a3844aabe6c111f142ca6 192.168.0.105:7003 slots:[10923-16383] (5461 slots) master S: a928fd65a4ed5ca3d13c8af99934f2b105da155a 192.168.0.105:7004 replicates 1d84e7ac4fce694d5cdc354cf447209caded41a7 S: 46e3ccc1eac204d6a5182be65978a5caa7afabaf 192.168.0.105:7005 replicates 32afb011852dc446464a3844aabe6c111f142ca6 S: a8ee7daff2f82b9beb076ba5207012e54319061c 192.168.0.105:7006 replicates 9397d9050fc5db96ad3561c579307fbbdd534aff #是否采用上述配置方案 Can I set the above configuration? (type 'yes' to accept): yes >>> Nodes configuration updated >>> Assign a different config epoch to each node >>> Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join .. >>> Performing Cluster Check (using node 192.168.0.105:7001) M: 9397d9050fc5db96ad3561c579307fbbdd534aff 192.168.0.105:7001 slots:[0-5460] (5461 slots) master 1 additional replica(s) M: 32afb011852dc446464a3844aabe6c111f142ca6 192.168.0.105:7003 slots:[10923-16383] (5461 slots) master 1 additional replica(s) S: a8ee7daff2f82b9beb076ba5207012e54319061c 192.168.0.105:7006 slots: (0 slots) slave replicates 9397d9050fc5db96ad3561c579307fbbdd534aff S: 46e3ccc1eac204d6a5182be65978a5caa7afabaf 192.168.0.105:7005 slots: (0 slots) slave replicates 32afb011852dc446464a3844aabe6c111f142ca6 S: a928fd65a4ed5ca3d13c8af99934f2b105da155a 192.168.0.105:7004 slots: (0 slots) slave replicates 1d84e7ac4fce694d5cdc354cf447209caded41a7 M: 1d84e7ac4fce694d5cdc354cf447209caded41a7 192.168.0.105:7002 slots:[5461-10922] (5462 slots) master 1 additional replica(s) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. -
集群創(chuàng)建成功后,測試連接并查看集群信息
root@localhost redis-6.2.1]# redis-cli -a 123456 -h 192.168.0.105 -p 7001 -c Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. #集群狀態(tài) OK 192.168.0.105:7001> 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:85 cluster_stats_messages_pong_sent:91 cluster_stats_messages_sent:176 cluster_stats_messages_ping_received:86 cluster_stats_messages_pong_received:85 cluster_stats_messages_meet_received:5 cluster_stats_messages_received:176 #集群節(jié)點關系 及solt分配 可以看出 7001是當前主機 從機為 7006 192.168.0.105:7001> CLUSTER NODES 32afb011852dc446464a3844aabe6c111f142ca6 192.168.0.105:7003@17003 master - 0 1620025676591 3 connected 10923-16383 a8ee7daff2f82b9beb076ba5207012e54319061c 192.168.0.105:7006@17006 slave 9397d9050fc5db96ad3561c579307fbbdd534aff 0 1620025677000 1 connected 46e3ccc1eac204d6a5182be65978a5caa7afabaf 192.168.0.105:7005@17005 slave 32afb011852dc446464a3844aabe6c111f142ca6 0 1620025678602 3 connected 9397d9050fc5db96ad3561c579307fbbdd534aff 192.168.0.105:7001@17001 myself,master - 0 1620025677000 1 connected 0-5460 a928fd65a4ed5ca3d13c8af99934f2b105da155a 192.168.0.105:7004@17004 slave 1d84e7ac4fce694d5cdc354cf447209caded41a7 0 1620025677000 2 connected 1d84e7ac4fce694d5cdc354cf447209caded41a7 192.168.0.105:7002@17002 master - 0 1620025679608 2 connected 5461-10922
動態(tài)擴容增加節(jié)點
-
首先我們準備一個增加的節(jié)點,可以復制一個已經配置好的節(jié)點,替換其中的部分配置信息
1. cd /home/redis-cluster 2. cp -rf 7006 7007 3. vim 7007/redis.conf 4. 替換所有7006為7007 :%s/7006/7007/g #啟動 7007 新增加的節(jié)點 redis-server ../7007/redis.conf -
將新增節(jié)點加入集群
#新增主機節(jié)點 7007 redis-cli --cluster add-node 192.168.0.105:7007 192.168.0.105:7001 -a 123456 -
查看集群節(jié)點信息,節(jié)點增加成功后沒有分配槽的,沒有分配到 slot 將不能存儲數據,此時我們需要手動分配 slot

#手動分配槽
redis-cli --cluster reshard 192.168.0.105:7001 --cluster-from 32afb011852dc446464a3844aabe6c111f142ca6,9397d9050fc5db96ad3561c579307fbbdd534aff,1d84e7ac4fce694d5cdc354cf447209caded41a7 --cluster-to 3c822cb6e4420811971221fea71f40a628a65b5a --cluster-slots 4096 -a 123456

- 連接節(jié)點查看集群信息,及槽分配信息

-
我們發(fā)現我們已經成功的擴容了一個主機節(jié)點并且分配了槽,那按照我們之前的我們沒有從機,我們如何給主機對應的擴容一個從機呢?
#新增從機機節(jié)點 7008 redis-cli --cluster add-node 192.168.0.105:7008 192.168.0.105:7001 --cluster-slave --cluster-master-id 3c822cb6e4420811971221fea71f40a628a65b5a -a 123456
Jedis 操作 RedisCluster
public class RedisCluster {
public static void main(String[] args) {
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.91.128", 7001));
nodes.add(new HostAndPort("192.168.91.128", 7002));
nodes.add(new HostAndPort("192.168.91.128", 7003));
nodes.add(new HostAndPort("192.168.91.128", 7004));
nodes.add(new HostAndPort("192.168.91.128", 7005));
nodes.add(new HostAndPort("192.168.91.128", 7006));
nodes.add(new HostAndPort("192.168.91.128", 7007));
JedisPoolConfig config = new JedisPoolConfig();
//連接池最大空閑數
config.setMaxIdle(300);
//最大連接數
config.setMaxTotal(1000);
//連接最大等待時間,如果是 -1 表示沒有限制
config.setMaxWaitMillis(30000);
//在空閑時檢查有效性
config.setTestOnBorrow(true);
JedisCluster cluster = new JedisCluster(nodes, 15000, 15000, 5,
"javaboy", config);
String set = cluster.set("k1", "v1");
System.out.println(set);
String k1 = cluster.get("k1");
System.out.println(k1);
}
}
Redis Stream
基本介紹
從 Redis5 開始,推出 Stream 功能。在 Stream 中,有一個消息鏈表,所有加入鏈表中的消息都會被串起來。每一條消息都有一個唯一的ID,還有對應的消息內容,所謂的消息內容,就是鍵值對。
一個 Stream 上可以有多個消費者,每一個消費者都有一個游標,這個游標根據消息的消費情況在鏈表上移動。多個消費者之間互相獨立、互不影響。
基本命令
-
xadd 添加消息
#xadd key id string string * 代表服務器自動生成的ID; loadsysonfig、writemessage 任務名 dosomething 內容 192.168.0.105:7001> xadd job * loadsysonfig dosomething writemessage dosomething "1620035197992-0" -
xdel 刪除消息
192.168.0.105:7001> xdel job 1620035197992-0 (integer) 1 -
xlen 消息個數
192.168.0.105:7001> XLEN job (integer) 1 -
xrange 獲取消息列表
#返回所有消息 XRANGE job - + -
del 刪除Stream
del job
消息消費
-
xread 讀取消息
#從頭部開始讀取 xread count 1 streams job 0-0 #從尾部開始讀取 xread count 1 streams job $
關于Stream消息消費 其實還有很多知識,通常我們會建立一個消費組,從消費組中消費消息。也可以把消息隊列設計為阻塞隊列,設置一個阻塞時長。這里給大家介紹的目的主要是讓大家知道有這么個新特性,比如出去面試,提到這個自己知道不會讓面試管覺得自己的知識面很狹窄。對于消息的處理,在不考慮復雜性的前提下,我們通常會采用專業(yè)的消息中間件處理。
Redis 過期策略
Redis 中所有的 key 都可以設置過期時間。Redis是把每一個設置過期時間的 key 放到一個獨立的數據字典中,定時遍歷這個字典來刪除到期的 key。除了定時刪除以外,還會使用一些惰性策略,客戶端訪問這個key 的時候,檢查這個 key 的過期

浙公網安備 33010602011771號