1. 程式人生 > 資料庫 >redis-詳解

redis-詳解

1. redis 安裝

redis是什麼?

redis是資料庫的一種,我們常見的資料庫可以分為關係型資料庫和菲關係型資料庫,redis就是菲關係型資料庫的一種。並且redis是key-value型資料庫。

從上面的解釋引出新的問題:關係型資料庫和非關係型資料庫:

  • 關係型資料庫:使用關係模型來組織資料的資料庫。使用表結構來維護資料結構,可以使用通用sql語句操作資料庫。資料庫事物必須遵循ACID
  • 非關係型資料庫:非關係型,資料結構不固定,不需要遵循acid,分散式的。

為什麼要使用redis?

當我們使用一門技術時,首先要想的是我為什麼要使用它,使用它能給我帶來什麼好處,能解決什麼問題,又會帶來什麼問題?

redis要記住的特點就是:快,單執行緒
因為redis是單執行緒的,因此不存線上程安全的問題,而且reids是基於記憶體的。所以速度快。使用redis可以顯著的緩解資料庫的壓力。
使用redis做快取時,可以解決快取多個服務之間快取共享的問題。

1.1 簡單安裝redis

從網上下載redis-5.0.0.tar.gz壓縮包,解壓壓縮包:

tar zxvf redis-5.0.0.tar.gz

然後移動檔案至目錄 /usr/local/下:

mv redis-5.0.0 /usr/local/

進入redis解壓後文件目錄 cd /usr/local/redis-5.0.5,可以看到如下內容:

[root@localhost redis-5.0.5]# ll
total 268
-rw-rw-r--.  1 root root 106874 May 15  2019 00-RELEASENOTES
-rw-rw-r--.  1 root root     53 May 15  2019 BUGS
-rw-rw-r--.  1 root root   2381 May 15  2019 CONTRIBUTING
-rw-rw-r--.  1 root root   1487 May 15  2019 COPYING
drwxrwxr-x.  6 root root   4096 May 15  2019 deps
-rw-rw-r--.  1 root root     11 May 15  2019 INSTALL
-rw-rw-r--.  1 root root    151 May 15  2019 Makefile
-rw-rw-r--.  1 root root   6888 May 15  2019 MANIFESTO
-rw-rw-r--.  1 root root  20555 May 15  2019 README.md
-rw-rw-r--.  1 root root  61797 May 15  2019 redis.conf
-rwxrwxr-x.  1 root root    275 May 15  2019 runtest
-rwxrwxr-x.  1 root root    280 May 15  2019 runtest-cluster
-rwxrwxr-x.  1 root root    341 May 15  2019 runtest-moduleapi
-rwxrwxr-x.  1 root root    281 May 15  2019 runtest-sentinel
-rw-rw-r--.  1 root root   9710 May 15  2019 sentinel.conf
drwxrwxr-x.  3 root root   4096 May 15  2019 src
drwxrwxr-x. 11 root root   4096 May 15  2019 tests
drwxrwxr-x.  8 root root   4096 May 15  2019 utils

檢視README.md,並按照此檔案來安裝redis。
執行make命令,開始redis的編譯,預設redis安裝位置為/usr/local/bin
如果想要更改此安裝位置,使用編譯命令 make install PREFIX=/home/zhaoshuai/redis

預設安裝目錄/usr/local/bin, 因為linux預設環境變數配置了/usr/local/bin目錄,因此如果使用PREFIX指定目錄的話,需要配置環境變數指定此目錄。
配置方式:
vi /etc/profile
新增如下配置
export REDIS_HOME=/home/zhaoshuai/redis
export PATH=$PATH:$REDIS_HOME/bin

儲存並退出,重新整理變數source /etc/profile

看到如下輸出則表示安裝成功:

Hint: It's a good idea to run 'make test' ;)

    INSTALL install
    INSTALL install
    INSTALL install
    INSTALL install
    INSTALL install

如果安裝失敗,則執行命令 make distclean,撤銷上次執行命令,重新編譯。
編譯成功後並配置環境變數後,可以在任意目錄執行 redis-server來啟動redis例項,啟動後輸出如下表示啟動成功:

[root@localhost src]# redis-server 
59669:C 20 Oct 2020 14:49:43.063 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
59669:C 20 Oct 2020 14:49:43.063 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=59669, just started
59669:C 20 Oct 2020 14:49:43.063 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
59669:M 20 Oct 2020 14:49:43.064 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 59669
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

59669:M 20 Oct 2020 14:49:43.075 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
59669:M 20 Oct 2020 14:49:43.075 # Server initialized
59669:M 20 Oct 2020 14:49:43.075 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
59669:M 20 Oct 2020 14:49:43.077 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
59669:M 20 Oct 2020 14:49:43.077 * Ready to accept connections

此時啟動是非後臺啟動的,也就是當前終端視窗關閉,或按 ctrl+c後,那麼此例項將被關閉。

1.2 將redis作為linux的服務啟動

進入redis主目錄下的utils目錄下

cd /usr/local/redis-5.0.5/utils

執行 ./install_server.sh命令,可以看到如下內容:

[root@localhost utils]# ./install_server.sh 
Welcome to the redis service installer
This script will help you easily set up a running redis server

Please select the redis port for this instance: [6379]    

可以看到,請為此redis例項選擇一個埠號,如果不輸的話,預設是6379
點選回車後,會輸出如下:

Please select the redis config file name [/etc/redis/6379.conf] 

請選擇此redis的配置檔案,預設是/etc/redis/6379.conf,也可以手動指定目錄。
後面還有其他配置,我們一直點回車使用預設配置,可以看到輸出如下:

Please select the redis log file name [/var/log/redis_6379.log] 
Selected default - /var/log/redis_6379.log
Please select the data directory for this instance [/var/lib/redis/6379] 
Selected default - /var/lib/redis/6379
Please select the redis executable path [/usr/local/bin/redis-server] 
Selected config:
Port           : 6379
Config file    : /etc/redis/6379.conf
Log file       : /var/log/redis_6379.log
Data dir       : /var/lib/redis/6379
Executable     : /usr/local/bin/redis-server
Cli Executable : /usr/local/bin/redis-cli
Is this ok? Then press ENTER to go on or Ctrl-C to abort.
Copied /tmp/6379.conf => /etc/init.d/redis_6379
Installing service...
Successfully added to chkconfig!
Successfully added to runlevels 345!
Starting Redis server...
Installation successful!

至此我們已經啟動了一個redis例項。可以通過命令 ps -ef|grep redis來檢視redis的程序資訊。
上面的命令 ./install_server可以執行多次,也就是說一臺物理機可以啟動多個redis例項,不同的redis例項通過埠號進行區分。每個例項的配置檔案等都會隨著埠號而變化。
為了驗證我們再重新啟動一臺,埠號為6380。啟動日誌如下:

[root@localhost utils]# ./install_server.sh 
Welcome to the redis service installer
This script will help you easily set up a running redis server

Please select the redis port for this instance: [6379] 6380
Please select the redis config file name [/etc/redis/6380.conf] 
Selected default - /etc/redis/6380.conf
Please select the redis log file name [/var/log/redis_6380.log] 
Selected default - /var/log/redis_6380.log
Please select the data directory for this instance [/var/lib/redis/6380] 
Selected default - /var/lib/redis/6380
Please select the redis executable path [/usr/local/bin/redis-server] 
Selected config:
Port           : 6380
Config file    : /etc/redis/6380.conf
Log file       : /var/log/redis_6380.log
Data dir       : /var/lib/redis/6380
Executable     : /usr/local/bin/redis-server
Cli Executable : /usr/local/bin/redis-cli
Is this ok? Then press ENTER to go on or Ctrl-C to abort.
Copied /tmp/6380.conf => /etc/init.d/redis_6380
Installing service...
Successfully added to chkconfig!
Successfully added to runlevels 345!
Starting Redis server...
Installation successful!

注意上面我們在輸入埠號時輸入了6380,後面所有的配置檔名都隨著埠號而變化。

我們開啟一個例項的配置檔案檢視內容,vi /etc/redis/6379.conf。會看到很多配置內容,這些內容再後面會進行詳細解釋。
現在瞭解就好,可以再配置檔案中檢視到port,logfile等。
使用 install_server指令碼後,還會將啟動的redis例項註冊為linux的服務。我們試著使用如下命令關閉6380的例項。

[root@localhost ~]# service redis_6380 stop
Stopping ...
Redis stopped

可以看到redis例項已經關閉,然後再使用 ps -ef|grep redis檢視,確認redis例項已關閉。

如何將服務註冊為linux服務?
想要使用service 服務名 start/stop/restart來啟動服務,需要在/etc/init.d/資料夾下,新建指令碼,指令碼名就是服務名。
執行install_server指令碼後,除了建立配置檔案等外,還在/etc/init.d/資料夾下建立了啟動指令碼,指令碼名就是redis_埠號。
開啟此檔案,檢視內容如下,以後想要將服務註冊為linux服務也可以照著寫。

redis 的資料型別

在學習redis的資料型別前,首先學習一個命令 help。使用命令redis-cli -h檢視客戶端的幫助資訊,輸入引數資訊等。
執行 redis-cli命令,預設連線6379埠服務。
首先我們需要了解一個知識,redis有16個數據庫,這些庫的名字為0-15。可以通過 select 0來選擇庫,預設為0庫,不同的庫之間資料是隔離的。
然後我們在學習一個命令 help @group,使用這個命令我們可以自己學習redis的各種命令。

string型別

使用命令 help @string來檢視string型別的命令。根據help來學習string命令

set 新增一條資料

127.0.0.1:6379> set k1 hello
OK

append value追加

127.0.0.1:6379> append k1 redis
(integer) 10

get 根據key查詢value

127.0.0.1:6379> get k1
"helloredis"

del 刪除key

127.0.0.1:6379> del k1
(integer) 1

del命令不只針對string型別,所有型別的資料都可以通過del key的方式刪除,del後面可以跟多個key,可以一次刪除多條資料。

incr key 自增命令,每次自增1

127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> incr k1
(integer) 2
127.0.0.1:6379> incr k1
(integer) 3

incrby key increment 增加指定數字

127.0.0.1:6379> INCRBY k1 5
(integer) 8

decr key 自減,每次自減1

有增就有減

127.0.0.1:6379> DECR k1
(integer) 7

decrby key decrement 減少指定資料

127.0.0.1:6379> DECRBY k1 4
(integer) 3

incrbyfloat key increment 增加浮點數

127.0.0.1:6379> INCRBYFLOAT k1 3.5
"6.5"

注意自增浮點數後,資料型別變成了 ""string型別浮點數,之前都是(integer)型別的。浮點數進行整數加減法運算結果仍為浮點數,
而incr,incrby,decr,decrby等運算結果為integer,所以進行incrbyfloat後,如果數字為浮點數,不能使用上面命令,
不過可以使用incrbyfloat key -num來減調浮點數。

strlen key 計算值的長度

127.0.0.1:6379> set a1 "hello"
OK
127.0.0.1:6379> STRLEN a1
(integer) 5
127.0.0.1:6379> set a2 100
OK
127.0.0.1:6379> STRLEN a2
(integer) 3

setrange 設定指定偏移量的值

127.0.0.1:6379> SETRANGE a1 5 redis
(integer) 10
127.0.0.1:6379> get a1
"helloredis"
127.0.0.1:6379> SETRANGE a1 4 " test"
(integer) 10
127.0.0.1:6379> get a1
"hell tests"
127.0.0.1:6379> 

需要注意當值的長度不夠偏移量時,往後追加,如果設定的值長度+偏移量小於原值長度時,替換指定長度的值。

getrange 獲取指定起始位置的值,終止位置可以為負數(-1),代表從倒數,但是起始位置不能為負數。

127.0.0.1:6379> GETRANGE a1 0 -1
"hell tests"
127.0.0.1:6379> GETRANGE a1 0 5
"hell t"

setnx 設定值當key不存在時,如果key已經存在,那麼設定失敗。

127.0.0.1:6379> SETNX k1 "redis"
(integer) 0
127.0.0.1:6379> SETNX k5 "redis"
(integer) 1

setex 設定值的過期時間

127.0.0.1:6379> SETEX k3 1000 "expire data"
OK
127.0.0.1:6379> TTL k3
(integer) 994

上面設定了一個key為k3,值為expire data的資料,設定的過期時間是1000 秒, 在redis中,過期時間單位是秒,ttl可以檢視指定key還有多久過期。

補充一個命令 expire key seconds 指定key的過期時間
127.0.0.1:6379> expire k1 10
(integer) 1
過十秒後再檢視key
127.0.0.1:6379> get k1
(nil)

mset 設定多個值

127.0.0.1:6379> mset b1 "hello" b2 "redis" b3 "java"
OK

mget 檢視多個值

127.0.0.1:6379> mget b1 b2 b3
1) "hello"
2) "redis"
3) "java"

getset 獲取舊值並設定新值

127.0.0.1:6379> GETSET k2 10
"5"
127.0.0.1:6379> get k2
"10"

msetnx 設定多個值,只有當key不存在時才能設定成功,如果有一個key設定失敗,那麼則全都失敗。

127.0.0.1:6379> MSETNX c1 "hello" c2 "redis" c3 "python"
(integer) 1
127.0.0.1:6379> MSETNX c1 "hello" c2 "redis" b2 "python"
(integer) 0

psetex 設定值,而且key將在指定毫秒數內過期。

127.0.0.1:6379> PSETEX k1 1000 hello
OK
127.0.0.1:6379> get k1
(nil)

上面設定k1在1秒內過期。

** bitmap點陣圖,下面的內容仍然資料string型別的命令,屬於位運算。

redis是二進位制安全的

什麼是二進位制安全?
redis與外界互動的時候永遠都是位元組陣列。面向流一般有位元組流和字元流,那麼當別人訪問redis的時候,拿到的永遠是位元組流。
為什麼?
因為如果redis只存位元組,沒有從位元組中取出東西,按照某一編碼集轉換的話,資料就不會被破壞,所以叫二進位制安全。

127.0.0.1:6379> set k2 中
OK
127.0.0.1:6379> get k2
"\xe4\xb8\xad"

--raw觸發格式化

[root@zhaoshuai ~]# redis-cli --raw
127.0.0.1:6379> get k2
中

關於點陣圖的操作:

setbit 設定二進位制位

預設一個位元組是8個二進位制位,而位操作就是在這八個二進位制位上進行操作。二進位制只有0和1

127.0.0.1:6379> SETBIT k1 1 1
(integer) 0
127.0.0.1:6379> SETBIT k1 7 1
(integer) 0
127.0.0.1:6379> GET k1
"A"

上面操作,表示將k1的值的第二位設定為1,第7位設定為1。那麼此二進位制就表示為:01000001,對應的ascii碼就是A。那麼如何將它變成B呢?
B對應的二進位制碼為:01000010

127.0.0.1:6379> SETBIT k1 6 1
(integer) 0
127.0.0.1:6379> SETBIT k1 7 0
(integer) 1
127.0.0.1:6379> get k1
"B"

bitcount 統計值二進位制位中1的個數,可以指定起止字元

127.0.0.1:6379> BITCOUNT k1
(integer) 2

將k1的值設定為4個字元

127.0.0.1:6379> SETBIT k1 9 1
(integer) 0
127.0.0.1:6379> SETBIT k1 15 1
(integer) 0
127.0.0.1:6379> get k1
"BA"
127.0.0.1:6379> APPEND k1 B
(integer) 4
127.0.0.1:6379> get k1
"BACB"

統計最後兩個字元CB中的1的個數

127.0.0.1:6379> BITCOUNT k1 2 3
(integer) 5

bitpos 尋找第一個二進位制位,start和end引數為可選引數,指定位元組數。

127.0.0.1:6379> BITPOS k1 1 0 1
(integer) 1
127.0.0.1:6379> BITPOS k1 1 1 1
(integer) 9
127.0.0.1:6379> BITPOS k1 0 1 1
(integer) 8

bitop 進行位運算,位運算有與/或運算,將兩個值進行位運算後賦給一個新值。

127.0.0.1:6379> set k2 A
OK
## A的二進位制碼為: 01000001
127.0.0.1:6379> set k3 C
OK
## C的二進位制碼為: 01000011

所以A^C=A;A|C=C

127.0.0.1:6379> BITOP or k4 k2 k3
(integer) 1
127.0.0.1:6379> get k4
"C"
127.0.0.1:6379> bitop and k5 k2 k3
(integer) 1
127.0.0.1:6379> get k5
"A"

getbit 獲取二進位制位的值

127.0.0.1:6379> get k2
"A"
127.0.0.1:6379> GETBIT k2 7
(integer) 1
127.0.0.1:6379> GETBIT k2 6
(integer) 0

bitfield 對字串任意位置進行位運算

使用bitfield時,將字串視為位陣列

待處理 todo

list型別

list 列表,特點:

  • 有序,雙向連結串列儲存
  • 允許重複元素
    使用命令help @list檢視命令幫助

lpush 從左邊往列表中推資料

127.0.0.1:6379> LPUSH k1 1 2 3 4 5 6
(integer) 6

lrange 擷取列表的元素

127.0.0.1:6379> LRANGE k1 0 1
1) "6"
2) "5"

查詢所有元素:

127.0.0.1:6379> LRANGE k1 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
5) "2"
6) "1"

lset 設定指定索引的元素值

127.0.0.1:6379> lset k1 0 8
OK
127.0.0.1:6379> LRANGE k1 0 -1
1) "8"
2) "5"
3) "4"
4) "3"
5) "2"
6) "1"

linsert 插入資料

127.0.0.1:6379> LINSERT k1 before 5 7
(integer) 7
127.0.0.1:6379> LRANGE k1 0 -1
1) "8"
2) "7"
3) "5"
4) "4"
5) "3"
6) "2"
7) "1"

linsert 插入可以選擇before或after某一個元素,也就是在指定元素之前或者之後插入資料,不是根據下標操作。

rpush 從右邊向列表中推入元素

127.0.0.1:6379> RPUSH k2 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> LRANGE k2 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"

在列表的命令以l開頭,有兩種意思,一種是表示列表,如:lrange,lset。另一種則表示left,從左邊操作,如lpush,lpop等。
有左就有右,分別表示從列表頭或從列表尾操作列表。

llen 統計列表元素個數

127.0.0.1:6379> llen k1
(integer) 7

lpop 從頭部彈出一個元素

127.0.0.1:6379> lpop k1
"8"
127.0.0.1:6379> lpop k1
"7"
127.0.0.1:6379> lpop k1
"5"

rpop 從尾部彈出一個元素

127.0.0.1:6379> lrange k2 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
127.0.0.1:6379> RPOP k2 
"6"
127.0.0.1:6379> RPOP k2 
"5"
127.0.0.1:6379> RPOP k2 
"4"

根據lpush,rpush,lpop,rpop可以組成常見資料結構,如棧、佇列
棧:先進後出
127.0.0.1:6379> lpush k3 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> lpop k3
"6"
127.0.0.1:6379> lpop k3
"5"
127.0.0.1:6379> lpop k3
"4"
127.0.0.1:6379> lpop k3
"3"
127.0.0.1:6379> lpop k3
"2"
127.0.0.1:6379> lpop k3
"1"
佇列:先進先出
127.0.0.1:6379> lpush k4 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> rpop k4
"1"
127.0.0.1:6379> rpop k4
"2"
127.0.0.1:6379> rpop k4
"3"
127.0.0.1:6379> rpop k4
"4"
127.0.0.1:6379> rpop k4
"5"
127.0.0.1:6379> rpop k4
"6"

lpushx 當列表存在時,從頭部新增資料

學習一個新命令: keys [pattern] 根據指定的模式匹配key,*匹配所有
127.0.0.1:6379> keys *

  1. "k1"
  2. "k2"
    表示當前有兩個key,k1,k2
127.0.0.1:6379> LRANGE k2 0 -1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> LPUSHX k2 4 5 6
(integer) 6
127.0.0.1:6379> LPUSHX k3 1 2 3
(integer) 0

rpushx 當列表存在時,從尾部插入元素

127.0.0.1:6379> RPUSHX k3 4 5 6
(integer) 0
127.0.0.1:6379> RPUSHX k2 7 8 9
(integer) 9
127.0.0.1:6379> LRANGE k2 0 -1
1) "6"
2) "5"
3) "4"
4) "1"
5) "2"
6) "3"
7) "7"
8) "8"
9) "9"

rpopLpush 彈出列表的最後一個元素,並將它從頭部插入新的列表。

127.0.0.1:6379> RPOPLPUSH k2 k4
"9"
127.0.0.1:6379> LRANGE k4 0 -1
1) "9"
127.0.0.1:6379> RPOPLPUSH k2 k4
"8"
127.0.0.1:6379> LRANGE k4 0 -1
1) "8"
2) "9"
127.0.0.1:6379> RPOPLPUSH k2 k4
"7"
127.0.0.1:6379> LRANGE k4 0 -1
1) "7"
2) "8"
3) "9"
127.0.0.1:6379> LRANGE k2 0 -1
1) "6"
2) "5"
3) "4"
4) "1"
5) "2"
6) "3"

lindex 根據索引查詢元素

127.0.0.1:6379> LINDEX k2 0
"6"
127.0.0.1:6379> LINDEX k2 4
"2"

ltrim 將列表修剪到指定範圍

這個命令和lrange看起來很像,lrange命令,擷取指定索引的字串,例如:

127.0.0.1:6379> LRANGE k2 0 3
1) "6"
2) "5"
3) "4"
4) "1"

返回指定範圍的資料,但是列表k2的長度並沒有改變

127.0.0.1:6379> lrange k2 0 -1
1) "6"
2) "5"
3) "4"
4) "1"
5) "2"
6) "3"

ltrim命令則會保留指定的返回的元素,刪除範圍之外的元素,執行結果輸出為成功或失敗。

127.0.0.1:6379> LTRIM k2 2 4
OK
127.0.0.1:6379> LRANGE k2 0 -1
1) "4"
2) "1"
3) "2"

可以看出ltrim會修改列表的長度。ltrim和lrange兩個命令的輸出也是不一樣的,lrange輸出的是擷取返回的元素值,而ltrim返回擷取成功或失敗。

lrem 刪除元素,指定元素的值及刪除個數

127.0.0.1:6379> lpush k5 1 2 3 2 1 2 2 1 3 2
(integer) 10
127.0.0.1:6379> LRANGE k5 0 -1
 1) "2"
 2) "3"
 3) "1"
 4) "2"
 5) "2"
 6) "1"
 7) "2"
 8) "3"
 9) "2"
10) "1"   

下面我們刪掉3個2,會從左邊開始統計,刪除三個2結束

127.0.0.1:6379> LREM k5 3 2
(integer) 3
127.0.0.1:6379> LRANGE k5 0 -1
1) "3"
2) "1"
3) "1"
4) "2"
5) "3"
6) "2"
7) "1"

再刪除一個1

127.0.0.1:6379> LRANGE k5 0 -1
1) "3"
2) "1"
3) "2"
4) "3"
5) "2"
6) "1"

lrem輸出的是刪除的元素個數,當元素不存在時,輸出0,列表沒變化

127.0.0.1:6379> LREM k5 1 4
(integer) 0
127.0.0.1:6379> LRANGE k5 0 -1
1) "3"
2) "1"
3) "2"
4) "3"
5) "2"
6) "1"

當要刪除的元素個數超過列表中的元素個數時,則刪除所有並返回刪除的元素個數

127.0.0.1:6379> LREM k5 4 3
(integer) 2
127.0.0.1:6379> LRANGE k5 0 -1
1) "1"
2) "2"
3) "2"
4) "1"

blpop,brpop,brpoplpush 阻塞的單播佇列

啟動三個客戶端redis-cli,開啟一個檢視

127.0.0.1:6379> keys *
1) "k4"
2) "k1"
3) "k2"
4) "k5"

檢視沒有k6的鍵,兩個客戶端輸入

127.0.0.1:6379> BLPOP k6 0

127.0.0.1:6379> BLPOP k6 0

會阻塞
然後在另一臺上往k6中推入一個元素

127.0.0.1:6379> LPUSH k6 1
(integer) 1

檢視阻塞的兩個客戶端

127.0.0.1:6379> BLPOP k6 0
1) "k6"
2) "1"
(47.36s)
127.0.0.1:6379> 
127.0.0.1:6379> BLPOP k6 0

一臺客戶端結束阻塞,另一個仍在阻塞

set型別

list是列表,set是集合。
set的特徵:

  • 無序
  • 去重
    使用命令 help @set檢視set命令的幫助文件

sadd 新增集合

127.0.0.1:6379> sadd k1 a b c d e 
(integer) 5

smemebers 檢視所有元素

127.0.0.1:6379> SMEMBERS k1
1) "c"
2) "d"
3) "b"
4) "a"
5) "e"

scard 獲取集合元素總數(集合長度)

127.0.0.1:6379> scard k1
(integer) 5

srandmember key [count] 隨機獲取count個元素

隨機獲取元素,但是元素仍在集合中,並沒有取出來

127.0.0.1:6379> srandmember k1 
"c"
127.0.0.1:6379> srandmember k1 3
1) "a"
2) "d"
3) "e"
127.0.0.1:6379> SMEMBERS k1
1) "d"
2) "b"
3) "a"
4) "c"
5) "e"

sdiff key [key1, key2..] 取多個集合的差集

從引數可以看出,查詢的是key與 key1,key2..的差集

127.0.0.1:6379> SADD k2 b c d e f
(integer) 5
127.0.0.1:6379> SDIFF k1 k2
1) "a"

sdiffstore destination key [key1, key2..] 取差集並存入目標集合destination

127.0.0.1:6379> sadd k3 c d e f g
(integer) 5
127.0.0.1:6379> SDIFFSTORE k4 k1 k3
(integer) 2
127.0.0.1:6379> SMEMBERS k4
1) "b"
2) "a"
127.0.0.1:6379> SDIFFSTORE k5 k3 k1
(integer) 2
127.0.0.1:6379> SMEMBERS k5
1) "f"
2) "g"

sinter key [key1,key2..] 取key與多個集合的交集

127.0.0.1:6379> SINTER k1 k2 k3
1) "c"
2) "d"
3) "e"
127.0.0.1:6379> SINTER k1 k2
1) "b"
2) "c"
3) "d"
4) "e"

sinterstore destination key [key1,key2..] 取多個集合的交集並存入新的集合

127.0.0.1:6379> SINTERSTORE k6 k1 k2 k3
(integer) 3
127.0.0.1:6379> SMEMBERS k6
1) "c"
2) "d"
3) "e"

sunion key [key1,key2..] 取多個集合的並集

127.0.0.1:6379> SUNION k1 k2
1) "b"
2) "a"
3) "f"
4) "c"
5) "d"
6) "e"
127.0.0.1:6379> SUNION k1 k2 k3
1) "b"
2) "a"
3) "f"
4) "c"
5) "g"
6) "d"
7) "e"

sunionstore destination key [key1, key2..] 取多個集合的並集存入新的集合

127.0.0.1:6379> SUNIONSTORE k7 k1 k2 k3
(integer) 7
127.0.0.1:6379> SMEMBERS k7
1) "b"
2) "a"
3) "f"
4) "c"
5) "g"
6) "d"
7) "e"

sismember key member 判斷集合中是否存在成員

127.0.0.1:6379> SMEMBERS k1
1) "b"
2) "a"
3) "c"
4) "d"
5) "e"
127.0.0.1:6379> SISMEMBER k1 a
(integer) 1
127.0.0.1:6379> SISMEMBER k1 g
(integer) 0

smove source destination member 移動source中的成員member到一個新的集合destination

127.0.0.1:6379> SMEMBERS k4
1) "b"
2) "a"
127.0.0.1:6379> SMOVE k1 k4 c
(integer) 1
127.0.0.1:6379> SMEMBERS k4
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> SMEMBERS k1
1) "b"
2) "a"
3) "d"
4) "e"

spop key [count] 移除[count,預設1]個元素

127.0.0.1:6379> SPOP k1 3
1) "b"
2) "a"
3) "d"
127.0.0.1:6379> SMEMBERS k1
1) "e"
127.0.0.1:6379> SMEMBERS k2
1) "b"
2) "c"
3) "f"
4) "d"
5) "e"
127.0.0.1:6379> SPOP k2
"d"
127.0.0.1:6379> SMEMBERS k2
1) "b"
2) "c"
3) "f"
4) "e"

srem key member [member...] 刪除指定元素

127.0.0.1:6379> SREM k2 b c e
(integer) 3
127.0.0.1:6379> SMEMBERS k2
1) "f"

sorted_set型別

sorted_set和set的區別?
之前已經有了一個Set型別,為什麼還要有一個sorted_set型別呢?這兩個型別的具體區別:

  • set是無序的,sorted_set是有序的,但是要注意這個有序是排序,與list不同,list的有序是指輸入順序與輸出順序一致,也就是儲存的順序。
    而sorted_set每個元素都有一個score分值,根據這個分值對元素進行排序,因此通過更改分值就可以更改元素的位置。

使用命令 help @sort_set來檢視幫助資訊

zadd key [nx|xx] [ch] [incr] score member[score member..] 新增命令

[nx|xx]: nx: 當member成員不存在時,插入成員資訊
xx: 當member成員存在時,更新成員分數
將班級學生按照語文成績進行排序,那麼這個集合應該是這樣的

127.0.0.1:6379> zadd k1 100 zhangsan 80 lisi 90 wangwu 50 maliu
(integer) 4
127.0.0.1:6379> ZADD k1 nx 75 xiaoming
(integer) 1
127.0.0.1:6379> ZADD k1 nx 84 lisi
(integer) 0

zrange key start end [withsocres] 獲取值

127.0.0.1:6379> ZRANGE k1 0 -1 withscores
 1) "maliu"
 2) "50"
 3) "xiaoming"
 4) "75"
 5) "lisi"
 6) "80"
 7) "wangwu"
 8) "90"
 9) "zhangsan"
10) "100"
127.0.0.1:6379> ZRANGE k1 0 -1
1) "maliu"
2) "xiaoming"
3) "lisi"
4) "wangwu"
5) "zhangsan"

學習這個命令後再回頭看zadd命令的xx命令

 127.0.0.1:6379> ZADD k1 xx 70 maliu
 (integer) 0
 127.0.0.1:6379> ZRANGE k1 0 -1 withscores
  1) "maliu"
  2) "70"
  3) "xiaoming"
  4) "75"
  5) "lisi"
  6) "80"
  7) "wangwu"
  8) "90"
  9) "zhangsan"
 10) "100"

zrangebyscore 上面是按照索引取,還可以按照分數

127.0.0.1:6379> ZRANGEBYSCORE k1 0 100 withscores
 1) "xiaoming"
 2) "75"
 3) "lisi"
 4) "80"
 5) "wangwu"
 6) "90"
 7) "maliu"
 8) "100"
 9) "zhangsan"
10) "100"
127.0.0.1:6379> ZRANGEBYSCORE k1 75 90
1) "xiaoming"
2) "lisi"
3) "wangwu"

zcard 獲取元素總數

127.0.0.1:6379> ZCARD k1
(integer) 5

zcount key min max 獲取某一個分數範圍內的元素總數

127.0.0.1:6379> ZCOUNT k1 50 80
(integer) 3

zincyby key increment member 某一個成員增加分數

127.0.0.1:6379> ZINCRBY k1 30 maliu
"100"
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
 1) "xiaoming"
 2) "75"
 3) "lisi"
 4) "80"
 5) "wangwu"
 6) "90"
 7) "maliu"
 8) "100"
 9) "zhangsan"
10) "100"

zrevrange 倒序按索引取數

127.0.0.1:6379> ZREVRANGE k1 0 -1 withscores
 1) "zhangsan"
 2) "100"
 3) "maliu"
 4) "100"
 5) "wangwu"
 6) "90"
 7) "lisi"
 8) "80"
 9) "xiaoming"
10) "75"

zrevrangebyscore 倒序按分數取數

127.0.0.1:6379> ZREVRANGEBYSCORE k1 90 40
1) "wangwu"
2) "lisi"
3) "xiaoming"

zscore 獲取成員的分數

127.0.0.1:6379> ZSCORE k1 zhangsan
"100"

zrank 確定成員在排序集中的索引

127.0.0.1:6379> ZRANGE k1 0 -1
1) "xiaoming"
2) "lisi"
3) "wangwu"
4) "xiaohong"
5) "zhaosi"
6) "maliu"
7) "zhangsan"
127.0.0.1:6379> ZRANK k1 xiaoming
(integer) 0
127.0.0.1:6379> ZRANK k1 lisi
(integer) 1
127.0.0.1:6379> ZRANK k1 zhangsan
(integer) 6

zrevrank 確定成員在拍序集中的索引,排序集為倒序排序

127.0.0.1:6379> ZREVRANK k1 zhangsan
(integer) 0
127.0.0.1:6379> ZREVRANK k1 xiaoming
(integer) 6
127.0.0.1:6379> ZREVRANK k1 maliu
(integer) 1

zrem 移除指定成員

127.0.0.1:6379> ZREM k1 xiaohong
(integer) 1
127.0.0.1:6379> ZRANGE k1 0 -1
1) "xiaoming"
2) "lisi"
3) "wangwu"
4) "zhaosi"
5) "maliu"
6) "zhangsan"

zunionStore 兩個排序集取並集並存入新的排序集

127.0.0.1:6379> zadd k2 1 a 2 b 3 c
(integer) 4
127.0.0.1:6379> zadd k3 5 b 3 c 2 d
(integer) 4
127.0.0.1:6379> ZUNIONSTORE k4 2 k2 k3 
(integer) 4
127.0.0.1:6379> ZRANGE k4 0 -1
1) "a"
2) "c"
3) "d"
4) "b"
127.0.0.1:6379> ZRANGE k4 0 -1 withscores
1) "a"
2) "1"
3) "d"
4) "2"
5) "c"
6) "6"
7) "b"
8) "7"

兩個集合取並集時,如果有相同的元素,會重新計算分值,計算規則有 min/max/sum,可以計算分值的權重

zinterstore 取交集

127.0.0.1:6379> ZINTERSTORE k5 2 k2 k3
(integer) 2
127.0.0.1:6379> ZRANGE k5 0 -1
1) "c"
2) "b"

zpopmax 彈出分數最高的成員

127.0.0.1:6379> ZRANGE k1 0 -1
 1) "a"
 2) "b"
 3) "c"
 4) "d"
 5) "xiaoming"
 6) "lisi"
 7) "wangwu"
 8) "zhaosi"
 9) "maliu"
10) "zhangsan"
127.0.0.1:6379> ZPOPMAX k1 2
1) "zhangsan"
2) "100"
3) "maliu"
4) "100"

zpopmin 彈出分數最低的成員

127.0.0.1:6379> ZPOPMIN k1 3
1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
 1) "d"
 2) "4"
 3) "xiaoming"
 4) "75"
 5) "lisi"
 6) "80"
 7) "wangwu"
 8) "90"
 9) "zhaosi"
10) "90"

zlexcount key min max

sorted_set 使用skip_list跳錶做儲存保證增刪改的速度

hash型別

hash用來存放鍵值對,類似於java中的hashmap
使用命令 help @hash檢視hash相關的命令

hset key field value 設定hash型別的值

127.0.0.1:6379> HSET h1 name zhangsan
(integer) 1
127.0.0.1:6379> HSET h1 age 18
(integer) 1

hgetall key 檢視所有的鍵值對

127.0.0.1:6379> HGETALL h1
1) "name"
2) "zhangsan"
3) "age"
4) "18"

hget key field 根據key獲取value

127.0.0.1:6379> hget h1 name
"zhangsan"

hlen key 獲取總的鍵值對數

127.0.0.1:6379> HLEN h1
(integer) 2

hkeys key 獲取所有的key(keySet)

127.0.0.1:6379> HKEYS h1
1) "name"
2) "age"

hvals key 獲取所有的值(valueSet)

127.0.0.1:6379> HVALS h1
1) "zhangsan"
2) "18"

hincrby key field increment 指定key對應的value增加指定數字

 127.0.0.1:6379> HINCRBY h1 age 2
 (integer) 20

hincrbyfloat key field increment 增加float浮點數

127.0.0.1:6379> HINCRBYFLOAT h1 age 2.5
"22.5"

hstrlen 值的長度

127.0.0.1:6379> HSTRLEN h1 name
(integer) 8

hsetnx 當key不存在時設定key,value,否則設定失敗

127.0.0.1:6379> HSETNX h1 name lisi
(integer) 0
127.0.0.1:6379> HSETNX h1 gender man
(integer) 1

hexists key field 查詢指定key是否存在

127.0.0.1:6379> HEXISTS h1 name
(integer) 1
127.0.0.1:6379> HEXISTS h1 hobby
(integer) 0

hmset 設定多個值

127.0.0.1:6379> HMSET h2 yuwen 100 shuxu 90 yingyu 60
OK
127.0.0.1:6379> HGETALL h2
1) "yuwen"
2) "100"
3) "shuxu"
4) "90"
5) "yingyu"
6) "60"

hmget 獲取多個值

127.0.0.1:6379> HMGET h2 yuwen shuxu yingyu 
1) "100"
2) "90"
3) "60"

redis事物

redis是單執行緒的,所以是執行緒安全,redis對事物的支援僅僅支援原子性,也就是要麼都執行,要麼都不執行,不支援回滾事物。
使用help命令學習redis事物。 help @transactions

multi 開啟事物

127.0.0.1:6379> multi
OK

如果使用redis事物,就要使用此命令開啟事物,開啟事物後可以輸入redis存資料的命令

127.0.0.1:6379> set s1 "hello"
QUEUED
127.0.0.1:6379> set s2 "hello"
QUEUED

可以看到開啟事物後,命令全都存入了佇列中,並沒有被執行。

exec 提交事物,執行佇列中的所有redis命令

127.0.0.1:6379> exec
1) OK
2) OK

discard 丟棄當前佇列中的所有命令

127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 "hello"
QUEUED
127.0.0.1:6379> set k2 "java"
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

執行exec命令,報錯沒有可以執行的事物。因為discard將佇列中的命令丟棄了。

redis事物演示:

127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set k1 "hello"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> SETNX k2 "a"
QUEUED
127.0.0.1:6379> SETNX k1 "b"
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
1) "k1"

清空所有的key值,然後插入一個k1,值為hello,然後開啟事物,插入k2,在setnx k1,此時已經k1已經有值了,因此這條命令會失敗,然後exec提交事物
查詢所有的key,發現在k1前的k2也沒有插入成功,證明事物的原子性。

watch 監控一個key

watch命令就像java的樂觀鎖一樣,使用watch後會監控一個key,當這個key的值發生改變時,那麼事物就會提交失敗。
開啟兩臺redis客戶端。client1,client2:
client1:

127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> MULTI 
OK
127.0.0.1:6379> set k2 b
QUEUED
127.0.0.1:6379> set k3 c
QUEUED
127.0.0.1:6379> 

在client2中對k1的值做更改。此時client1還未提交事物。
client2:

127.0.0.1:6379> INCR k1
(integer) 2

然後在client1中提交事物:

127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> keys *
1) "k1"

發現client1的事物提交失敗。並沒有插入資料成功。

unwatch 取消對key的監控

a

redis的訂閱服務

在學習list時,學習了單播阻塞佇列。多個客戶端阻塞等待一個列表中的元素,當有列表中有元素時,只會有一個客戶端取出資料,其他客戶端仍然阻塞。
訂閱就是當往列表中新增元素時,多個阻塞客戶端都能收到資料。
類似聊天群,一個人發了資訊,其他所有人都可以收到資訊。
使用help命令檢視訂閱服務的命令 help @pubsub

subscribe 監聽通道中的訊息。

開啟多個客戶端,監聽c1通道。
client1:

1) "subscribe"
2) "c1"
3) (integer) 1

client2:

1) "subscribe"
2) "c1"
3) (integer) 1

publish 往通道中推送訊息

新啟一個客戶端

127.0.0.1:6379> PUBLISH c1 hello
(integer) 2
127.0.0.1:6379> PUBLISH c1 java
(integer) 2
127.0.0.1:6379> PUBLISH c1 "my channel message"
(integer) 2

然後檢視其他的阻塞客戶端:
client1:

127.0.0.1:6379> SUBSCRIBE c1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
1) "message"
2) "c1"
3) "hello"
1) "message"
2) "c1"
3) "java"
1) "message"
2) "c1"
3) "my channel message"

client2:

127.0.0.1:6379> SUBSCRIBE c1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
1) "message"
2) "c1"
3) "hello"
1) "message"
2) "c1"
3) "java"
1) "message"
2) "c1"
3) "my channel message"

可以看出,兩個阻塞的客戶端都收到了資訊

unsubscribe 取消監聽

psubscribe 監聽所有匹配指定模式通道的資訊

punsubscribe 取消監聽

redis擴充套件庫

進入
這個目錄下都是redis的擴充套件庫,以bloom擴充套件庫為例,找到此拓展庫,進入主頁,就跳轉進github的主頁了。
然後複製clone標籤下的zip檔案的連結。進入redis伺服器主目錄,執行 wget 下載連結就能下載到壓縮包master.zip
使用 unzip master.zip解壓,然後進入目錄,使用make編譯。編譯後的到.so檔案。
然後修改redis啟動的配置檔案,新增loadmodule 剛編譯得到的.so檔案路徑。
然後重啟redis例項。檢視日誌:

...
16568:M 24 Oct 2020 14:25:34.613 * Module 'bf' loaded from /usr/local/redis-5.0.5/RedisBloom-master/redisbloom.so
16568:M 24 Oct 2020 14:25:34.613 * DB loaded from disk: 0.000 seconds
16568:M 24 Oct 2020 14:25:34.613 * Ready to accept connections

可以看到載入了bf模組,就是bloom。
進入redis客戶端,就可以使用這個模組的命令了。
bloom模組解決了一個問題:快取穿透

什麼是快取穿透?

redis作為快取時,用來存一些經常被查詢的資料,那麼當請求過來後,查詢的內容沒有時,就會到資料庫中查詢,然後再將資料庫中的資料放入redis中一份,用來
防止減輕資料庫的壓力。但是在特殊情況--資料庫查到的結果也是空。那麼這種情況下,如果這個請求的併發量比較大的話(正常情況/惡意攻擊)。那麼快取就會失效
所有的請求就會壓到資料庫層,導致資料庫宕機。
** 快取和資料庫都沒有資料 **

快取穿透的解決辦法:

以搜尋某商品為例:

  • 將所有的商品名都存入快取,當請求過來時,首先查詢redis中是否有此商品,找到的話,直接返回,沒找到的話,提示沒有。
  • 當redis中沒有找到資料時,查詢資料庫,如果資料庫搜尋結果也為空,那麼將這個搜尋的關鍵字存入一個null到redis中,並設定過期時間。

快取擊穿?

快取中沒有資料,但是資料庫中有
可能是快取key值過期了,而正好這個key的併發查詢比較大,那麼就會導致所有的請求全都壓到資料庫層,導致資料庫壓力過大

快取擊穿解決辦法:

  • 設定熱點資料永不過期
  • 對查詢資料庫的操作加鎖,dcl判斷redis中是否有資料。

快取雪崩?

快取擊穿是一個key過期,這個key併發量大引起,而快取雪崩是大量的key同時過期,導致資料庫壓力突增

解決:

  • 所有的key過期時間設定為隨機
  • 熱點資料永不過期
  • 將資料均勻分佈多臺伺服器上

redis快取LRU

redis可以用來做快取,也可以用來做資料庫。

為什麼要用redis做快取?

因為redis的特性就是快,用來減少資料庫的壓力,所以redis中應該放的是熱點資料。

快取的特點:

  • 快,快取的資料是在記憶體中的,記憶體的速度要比磁碟快很多,這樣就可以降低io的耗時,提高速度。
  • 資料易丟失,因為快取中的資料是放在記憶體中的,記憶體中的資料如果系統發生異常或伺服器宕機就會丟失。
    所以根據快取的特性,我們資料都是全量存放在資料庫中的,使用快取來存放資料的副本,降低資料庫的壓力,因此快取中的資料並"不重要"(允許資料出現不一致),
    但是最終資料庫中的資料一定要是準確的。

使用redis做快取需要注意的地方--redis只能存放熱點資料?

  1. redis做快取時,快取中的熱點資料並不是一成不變的,今天的訪問量比較高的資料,可能明天就變了,以後都不會再訪問了,那麼就需要清理這些冷資料。
  2. 當redis中存放的資料量過大時,就要淘汰部分資料,用來給新的資料騰位置
    為什麼?
    因為記憶體的大小時固定的,使用記憶體可以解決io的瓶頸,但是自身的大小也是一個瓶頸。在看上面兩種情況,一種是可以明確確定,那兒些資料是過期的,應該扔掉
    但是第二種情況,因為記憶體空間引起,導致沒有空間存放新的資料,這時,並不能確定到底那兒些資料時沒用的,可以清理的。那麼就只能隨機選取資料清理掉,為新的
    資料騰位置。

當redis的記憶體滿了之後怎麼辦?

  1. 開啟redis的配置檔案可以查到下面幾個配置:
# 設定redis例項的最大記憶體大小
maxmemory <bytes> 
# redis到達最大記憶體限制時的刪除策略
maxmemory-policy noeviction 
# 上面策略每次比較的key的個數
maxmemory-samples 5

關於maxmemory-policy配置,檢視配置檔案此配置的註釋,可以看到如下內容:

MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
is reached. You can select among five behaviors:

volatile-lru -> Evict using approximated LRU among the keys with an expire set.
allkeys-lru -> Evict any key using approximated LRU.
volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key among the ones with an expire set.
allkeys-random -> Remove a random key, any key.
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
noeviction -> Don't evict anything, just return an error on write operations.

LRU means Least Recently Used
LFU means Least Frequently Used

Both LRU, LFU and volatile-ttl are implemented using approximated
randomized algorithms.

Note: with any of the above policies, Redis will return an error on write
      operations, when there are no suitable keys for eviction.

      At the date of writing these commands are: set setnx setex append
      incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
      sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
      zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
      getset mset msetnx exec sort

從上面的註釋內容可以瞭解到,此配置的策略有如下選擇:

在看策略之前首先對LRU、LFU做一個解釋: 上面的註釋文件中也對這兩個定義做了解釋
LRU 最近最少使用
LFU 最少使用
同時在上面的策略中還能看到兩個字首:volatile、allkeys
volatile:表示快要過期的key
allkeys:表示所有的key

當redis的記憶體空間佔滿時,就會觸發下面的策略:

  • volatile-lru : 也就是說移除設定有過期時間的key中最近最少使用的
  • allkeys-lru : 移除所有key中最近最少使用的
  • volatile-lfu : 移除設定有過期時間的key中最少使用的
  • allkeys-lfu : 移除所有key中最少使用的
  • volatile-random : 隨機移除設定有過期時間的key
  • allkeys-random : 隨機移除所有的key
  • volatile-ttl : 刪除最接近到期時間的(快到期的)
  • noeviction : 不刪除任何的key,返回客戶端錯誤

檢視上面的策略進行分析:
如果redis作為資料庫的話,那麼只能使用noevication,因為要保證資料不回丟失,當記憶體滿的時候,寧願報錯,通知開發人員,空間不足。也不能丟棄現在已有的資料。
但是如果redis作為快取的話,那麼就可以選擇其他的策略了。在實際應用中怎麼進行選擇?
首先看*-random這兩個策略,這兩個策略都是隨機選擇淘汰,因此可能出現一個熱點資料剛存入就又被刪除了,有點太隨意了。volatile-ttl這種方式,它會查詢
所有key的過期時間,然後在通過比較刪除最接近到期時間的,時間複雜度比較高。排除這些後,剩下的,就是兩種情況 lru/lfu。根據需要選擇一種策略後,再看
volatile或allkeys,如果設定過期時間的key比價多就選擇volatile,如果沒有設定過期時間的key多的話,就選擇allkeys。

關於redis的key過期原理

redis的key過期有兩種方式,主動和被動。

  • 被動:當redis的一個key過期後,如果一直沒有新的請求訪問這個key,那麼這個key可能並不會被刪除,redis一直訪問他,也不管它,那麼他就一直佔著空間
    不釋放,當有使用者訪問它,然後發現這個key已經過期了,那麼就會返回給客戶端沒有,並且清除它,這樣就會有一個時間差,如果一個月一年不訪問,那麼他就會
    一直佔著空間。
  • 主動:主動方式就代表著輪詢。如果redis中有十萬個key,那麼就要遍歷十萬次,每個key都看一眼,就會阻塞響應客戶端,影響效能。因此redis是間接式
    主動遍歷,具體就是redis每秒檢測十次,每次隨機抽20個key檢視是否過期並刪除已經過期的key,如果有多於25%的key過期,那麼就重複上面的過程,直到過期key
    低於25%,這意味著在任何時刻,最多刪除25%的key。

redis持久化

redis可以用來做快取,也可以用來做資料庫,做快取時,資料不一定要求必須可靠,丟了就丟了,後邊還會有一個mysql資料庫存資料,無非就是重新在往快取裡
放一遍,但是做資料庫時候,資料是絕對不能丟的。要保證資料的可靠性。如果作為資料庫使用的話,就會引出一個問題:持久化
為什麼要持久化?
因為redis是記憶體型資料庫,記憶體的特點就是資料易丟失。
只要是與持久化,資料可靠性有關的,無關技術,無論是mysql還是redis等,只要是儲存層技術都會有一個通用的知識點: 快照(副本)、日誌

  • 快照: 將資料庫中的全量資料取出來,寫在一個檔案中,要麼放在硬碟,要麼放在其他的伺服器(異地可靠性儲存)這樣即使伺服器硬體壞了,也可以通過讀取快照
    獲取最近一次儲存的資料。
  • 日誌: 當用戶做寫操作(增刪改)的時候,就會將命令記錄到日誌中,只要日誌足夠完整,即使資料全丟失了,也可以通過執行日誌中的所有命令,來恢復資料。

在redis中,快照這種方式叫做RDB,日誌就叫做AOF

RDB(快照)

在上面已經解釋了什麼是快照,使用快照儲存資料就會牽扯出一個知識點:資料的時點性

什麼是資料的時點性?

比如說:我現在每隔一個小時,資料落地一次(做一次快照儲存),那麼現在我8點的時候,要進行一次快照,那麼應該怎麼實現呢?
方式一: save,阻塞當前可用服務,只進行資料儲存的操作。
方式二: bgsave,使用非同步的方式在後臺進行資料落地,不影響服務的使用。
上面兩種方式進行比較,很明顯我們應該使用的是第二種方式--為了保證服務的可用性。但是第一種方式仍然是有必要的,比如說我明確知道我現在要對某一個伺服器
擴容,或者除塵...那麼就要將當前伺服器停機,就需要手動的進行save操作,停用當前服務並儲存資料。
回到開始的問題,什麼是資料的時點性?我們假設現在有10g的資料要進行快照儲存,那麼儲存的時間假設需要半個小時,8點開始儲存,八點半結束。如何保證我八點半
儲存的資料就是八點整那個時刻資料庫中的資料?
就是說,假設我redis庫中本來有個a=3,那麼八點開始儲存資料,在儲存資料的過程中,也就是八點半之前,我又將a的值改為了a=8,那麼我這次儲存到本地磁碟的
a的值應該是3還是8?這就是資料的時點性

資料的時點性問題如何解決?

為了方便理解,首先需要知道一個父子程序的概念。在linux伺服器中,有一個管道的概念,就是使用了父子程序。
在linux中輸入 echo $$可以列印當前程序的id。也就是我們建立這個shell連線的程序id。

[root@zhaoshuai ~]# echo $$
56192

可以看到當前程序的id是56192。當使用管道 |的時候,其實左邊和右邊都是一個子程序,|左邊的輸出會作為|右邊程序的輸入。
那麼進行測試,輸入 echo $$|more,分析這個命令,左邊會輸出當前程序id,然後作為右邊的資料,右邊程序就會將這個程序id輸出出來。執行結果如下:

[root@zhaoshuai ~]# echo $$|more
56192

我們發現執行結果並沒有變化,仍然是父程序的id。那是否推翻了上面的說法呢?
$$作用相同的還有一個環境變數$BASHPID,也表示當前程序的id。

[root@zhaoshuai ~]# echo $BASHPID
56192

發現 $BASHPID的作用和 $$是一樣的,使用 $BASHPID再次進行上面的測試。

[root@zhaoshuai ~]# echo $BASHPID|more
56327
[root@zhaoshuai ~]# echo $BASHPID|more
56329
[root@zhaoshuai ~]# echo $BASHPID|more
56331

可以發現,每次列印的結果都是不一樣的,說明每次左邊的程序都是不一樣的。為了驗證 |兩邊每次的程序都是新的,我們編輯一個指令碼進行測試。

[root@zhaoshuai ~]# vi testpid.sh

在指令碼中輸入下面內容:

#!/bin/bash
read a
echo left:$a
echo right:$BASHPID

將左邊的輸出作為右邊的輸入,賦給變數a,列印a和當前程序id,然後我們為這個指令碼授權。

[root@zhaoshuai ~]# chmod +x testpid.sh 

執行這個指令碼

[root@zhaoshuai ~]# echo $BASHPID|./testpid.sh 
left:56397
right:56398
[root@zhaoshuai ~]# echo $BASHPID|./testpid.sh 
left:56399
right:56400

我們發現每次的結果都是不一樣的,也就證明了上面的理論: |兩邊會各起兩個自執行緒。
那麼為什麼使用 $$的時候不行呢?
因為 $$的優先順序高於管道,因此優先執行了 echo $$,此時還沒有執行管道,沒有開闢新的程序,因此輸出的是父程序的id,然後才執行 |開闢兩個
新程序,因此輸出還是父程序的id。

通過上面的學習,瞭解了父子程序。那麼父子程序之間的資料是否可以互相訪問?

首先在父程序中建立一個環境變數:

[root@zhaoshuai ~]# export num=1
[root@zhaoshuai ~]# echo $num
1

使用管道在子程序中輸出num:

[root@zhaoshuai ~]# echo $num|more
1

可以看到子程序可以訪問父程序的資料,那麼子程序對資料做修改父程序是否能看到呢?。
學習一個自增的操作 ((num++))

[root@zhaoshuai ~]# ((num++))
[root@zhaoshuai ~]# echo $num
2

然後我們使用管道在子程序中對num進行++,檢視父程序中變數是否被修改?

[root@zhaoshuai ~]# ((num++))|echo $num
2
[root@zhaoshuai ~]# echo $num
2

左邊子程序對變數進行了++操作,右邊輸出仍是2,兩個子程序資料隔離的,然後父程序重新列印變數的值不變,說明子程序對變數的修改,父程序是看不到的。
那麼父程序對變數的修改,子程序是否能看到?
為了驗證上面的問題,我們編寫一個指令碼:

[root@zhaoshuai ~]# vi testdata.sh 

#!/bin/bash
echo old:$num
echo "waiting parent modify num"
sleep 20
echo new:$num
[root@zhaoshuai ~]# chmod +x testdata.sh 

然後使用後臺執行的方式執行testdata.sh,方便在等待時間對父程序變數進行修改。

[root@zhaoshuai ~]# nohup sh testdata.sh &
[2] 56503
[root@zhaoshuai ~]# nohup: ignoring input and appending output to `nohup.out'

[root@zhaoshuai ~]# ((num++))
[root@zhaoshuai ~]# ((num++))

然後我們檢視日誌: tail -f nohup.out

old:2
waiting parent modify num
new:2

可以看到父程序對變數的修改並不影響子程序。因此我們可以得出結論: 父子程序之間的資料是隔離的,資料修改互不干涉

緊隨而來的問題是:redis進行RDB時,建立子程序的速度以及記憶體空間大小。

基於上面的理論,我們是不是可以認為,父程序建立子程序時,也為子程序匯出了一份變數的副本,然後兩個程序各自修改各自的變數(當然實際並不是這樣,我們目前進行這樣的猜想)

建立子程序的速度?

那麼要保證資料的時點性,首先需要考慮的是建立子程序的速度。因為要匯出變數的副本,首先需要建立子程序,如果子程序建立十分鐘,那麼匯出的副本就是8點10粉的資料
了,也就不能保證資料的時點性了。
怎麼解決?
linux系統有一個系統呼叫叫做fork(),fork()玩的是一個指標的引用,可以達到的效果:1.建立速度特別快。2.空間佔用小。
怎麼實現?
計算機的記憶體,叫做實體記憶體,可以將其看成是一個線性陣列,然後每個應用程式執行在記憶體中,都有一個虛擬地址空間,程式預設所有的記憶體都是自己可用的。
因此redis執行在記憶體中時,會有自己的虛擬地址,而且redis內部也有虛擬地址,比如定義一個變數a=8,那麼8是資料,存放在實體記憶體的1位置,然後變數a指向虛擬記憶體5
的位置,虛擬記憶體5又存放的是實體記憶體的位置8,這樣a就可以取出資料8了。

當建立一個子程序時,子程序也是一個程序,也有自己的虛擬空間,如果它要把父程序中的資料複製一份,他應該怎麼做呢?
  1. 將父程序的實體記憶體再複製一份,比如說父程序中a->虛擬記憶體地址5->實體記憶體地址1(實體記憶體1中存放資料8),然後在子程序中,將父程序的實體記憶體複製一份,比如說:
    實體地址2(存放資料8),然後子程序中a->虛擬記憶體地址5->實體記憶體地址2(實體記憶體2中存放資料8)。
  2. 將父程序中的所有虛擬地址複製一遍,也就是說最終子程序和父程序的變數a指向同一個實體地址。

記憶體空間的大小

比較上面兩種建立子程序資料複製的方式,明顯方式1是不可取的,因為那樣會造成實體記憶體佔用翻倍。而fork()呼叫使用的就是第二種方式。
這種方式實體記憶體的佔用並沒有改變,也達到了複製資料的目的。

引出的一個新的問題,兩個程序資料複製沒有問題,那麼資料修改怎麼辦?

我們之前證明了,兩個程序的資料修改是互不影響的。也就是說父程序將變數a的值改為9時,子程序中應該還是8。但是現在兩個程序變數指向同一個實體地址。
如果父程序對變數修改,將a的值改為9,那麼如果改動實體地址的話,肯定子程序的值也就改變。這樣不符合上面的驗證結果。

copy on write

為了解決上面出現的問題,使用了一個知識點叫做 copy on write,寫時複製。我們上面已經詳細講解了,建立子程序時並不複製資料。而copy on write意思是
當父程序或子程序要修改資料時,才發生複製。也就是說原來父程序中變數a->虛擬地址5->實體地址1(資料為8),那麼建立子程序時複製這份指標。
當父程序或子程序需要對資料進行修改時,首先將實體地址複製一份,實體地址2(資料為8),然後將程序的變數實體地址指向進行修改。變數a->虛擬地址5->實體地址2,
然後再在實體地址2中將資料8改為9。這樣就保證了兩個程序的資料修改互不影響。

上面詳細說明了為了保證資料的時效性使用的方法以及遇到的問題,處理方式。因為複製指標,所以建立程序非常快,同時因為不可能父程序將所有資料都修改一遍。
因此記憶體的佔用非常少。

redis在進行RDB時,會使用fork()建立子程序,建立程序非常快,也就保證了子程序中的資料就是8點那一個時間點的資料,然後因為子程序是往磁碟落資料的,因此
子程序中的資料是隻讀的,不會發生修改。父程序對外提供服務,父程序的修改使用了寫時複製,因此不會影響到子程序。

redis的RDB配置

開啟redis的配置檔案,關於RDB的配置如下:

#快照檔案存放路徑
dir /var/lib/redis/6379
#快照檔名
dbfilename dump.rdb
#快照的觸發條件
save 900 1
save 300 10
save 60 10000
#是否開啟壓縮
rdbchecksum yes

後臺執行RDB操作的命令是bgsave,但是在配置檔案中的配置確是save,也就是說配置檔案中的save配置實際是對bgsave生效的。save後有兩個引數,第一個是時間,
第二個是條數。save有多條配置,只要滿足任意一個,就會寫RDB。
上面的配置,就是說當60秒的時候如果寫運算元達到10000條就會執行RDB,如果沒有達到10000,那麼從61秒開始,就會進入300秒的判斷,當寫操作到達10條就會寫RDB,
如果這個仍沒有命中,那麼就會再判斷下一個,當到達900秒的時候,寫操作是否達到1條,如果有一條寫操作,就寫RDB檔案,防止時間過長,導致資料丟失。
如果想要關閉RDB,就寫一個 save "",預設是開啟RDB的。

RDB的弊端

經過上面的瞭解,可以得出RDB落的是某一個時點的全量資料,那麼RDB的弊端就是資料丟失相對多。比如說現在每隔一小時落一次資料,那麼當8點落資料後,9點落RDB前,掛機了,
那麼就丟失了一個小時的資料。

RDB的優點

rdb這種映象,類似於java中的序列化,其實就是記憶體中的位元組陣列用最快的方式搬到磁碟中去,所以恢復資料的時候也相對快。

AOF(日誌)

AOF 就是 append on file,向檔案中追加,追加的就是寫操作。也就是說每個增刪改操作都會寫到檔案裡,這樣的好處就是資料丟失會少。
**注意:當redis同時開啟了aof和rdb,那麼aof會落,rdb也會落,但是當服務重啟時,只會從aof中恢復資料,因為aof恢復的資料相對完整,就不做rdb恢復了。

aof的優點:丟失資料少

aof的缺點:檔案體積無限變大,恢復比較慢,因為日誌中記錄了很多的寫操作,aof恢復就是將這些操作在執行一遍,所以速度比較慢。

關於aof和rdb的優缺點在上面都總結過了。再說一遍:rdb恢復快,資料丟失比較多;aof恢復慢,但是資料丟失比較少。

那麼如果我們就要想一種方法,怎麼樣能夠既保證恢復速度,又保證資料丟失少的方案?

想辦法將兩種方案組合起來:使用RDB的方式落時點檔案,兩個時點之間的檔案使用aof來儲存。通過這種組合方式,無論什麼時間點宕機,我們都可以通過找到上一次rdb的檔案
以及上一次rdb完到宕機的時間之間的aof檔案來恢復資料,使用rdb恢復全量資料,保證恢復速度,而使用aof來對恢復資料進行補充,減少資料丟失。
例如:每隔一個小時進行一次RDB。8點的時候進行一次RDB,然後8點到9點之間的資料都通過aof記錄在日誌中,9點的時候將8點時的RDB檔案及增量日誌存入映象中,重新進行RDB,然後
將日誌清空,重新記錄增量日誌,這樣就能保證日誌檔案足夠小。當服務宕機時,只需要恢復最近的一次RDB檔案,以及寫入增量日誌檔案就可以恢復資料。
但是前面說了,redis中如果開啟了aof,那麼就只會通過aof來恢復資料,不使用rdb檔案,那麼redis是如何恢復資料的?

  • 在redis-4.0版本之前redis會有一種機制叫做重寫。
    什麼是重寫?  
       假如說我們redis中存了十年的資料,在這十年間就是不斷的建立key,刪除key,如果最後一次是建立key,沒有刪除,那麼其實前面的都是可以抵消調的,只需要建立一次key就行了。
    如果最後一次是刪除key,那麼這個key就直接不需要建立,因為最終的資料是沒有這條資料的。
       還有就是如果資料是一個集合型別,那麼這十年間不斷的往集合中新增元素,刪除元素,例如執行了十萬次push 1,那麼其實最終可以使用push 10個1,來達到相同的
    效果,前面執行了十萬次push操作,而最終恢復資料只用了一次push操作,兩個效果相同,很明顯執行一個push的效率比較高。
    **重寫就是抵消和整合命令,刪除抵消的命令,合併重複的命令,歸屬到一個key裡面**  
    重寫的結果是多條指令合併程一條指令,然後將重寫後的指令都放入一個純指令的日誌檔案裡面。
    但是因為日誌檔案大小是不斷增加的,因此如果日誌檔案非常大的話,那麼重寫這個過程也是非常耗時的,因此恢復資料仍然很慢。因此在4.0版本之後進行了改進。
    
  • 在redis-4.0之後
    在4.0之後,重寫會將所有的合併後指令放入一個純指令檔案,然後redis會將這個檔案中的資料通過rdb的方式寫入aof檔案中,然後再將增量的資料以指令的方式append到aof,
    也就是說間接的出結論:aof當中包含rdb和增量日誌。此時aof檔案就是一個混合體,即利用了rdb恢復快,又利用了日誌的全量,資料丟失少。如果8點觸發,那麼就
    把8點記憶體裡的資料寫成rdb,寫到aof檔案中,然後8點往後的所有新的增刪改就開始追加。
    

經過上面的學習,處理了持久化的問題,那麼此時會引出一個新的問題:I/O
redis是記憶體型資料庫,資料都存在記憶體中,那麼持久化的話就必然會染指I/O。redis的特徵就是快,但是當產生I/O後,那麼就會影響redis的速度。
redis往aof檔案寫操作提供了三種I/O級別:always、no、everysec。下面開啟redis的配置檔案學習aof的相關配置。

# 預設關閉aof
appendonly no

# aof檔名
appendfilename "appendonly.aof"

# aof預設的I/O級別
# appendfsync always 
appendfsync everysec
# appendfsync no

# 當aof日誌增加指定百分比時,開啟重寫
auto-aof-rewrite-percentage 100
# 重寫aof檔案的最小大小
auto-aof-rewrite-min-size 64mb

關於三個I/O級別:

我們在寫java程式碼時,如果需要向某一個檔案中寫資料,寫完之後,關閉檔案之前,需要做什麼操作?
flush
為什麼要呼叫flush?
在計算機中,所有對硬碟的I/O操作都必須要呼叫核心,核心會對每一個I/O開出一個快取空間buffer,然後如果想要向磁碟中寫東西,會優先寫到buffer中,buffer滿了
之後,核心會呼叫flush向磁碟中刷寫。而flush就是重新整理緩衝區,將緩衝區的資料刷入磁碟中。所以說,如果向檔案中寫資料,最後一次寫的資料沒有佔滿buffer,
那麼核心不會主動呼叫flush,那麼如果沒有呼叫flush,就會丟失這一塊的資料,所以需要手動呼叫flush,重新整理緩衝區。緩衝區的大小可以調,一般4k左右。
然後我們在看redis的三個I/O級別:

  • no:redis不調flush。也就是說當redis來了四筆寫操作,那麼每一筆都會向buffer中寫,buffer什麼時候滿了,就往磁碟中刷寫。這種方式可能會丟失的最大資料量
    就是buffer的大小。
  • always: 每一筆寫操作都呼叫flush。這個級別資料是最可靠的,最多可能就是有一筆資料過來調flush的時候停電了,那麼最多丟失一筆資料。
  • everysec: 每秒鐘呼叫一次flush。這個級別的資料丟失量是多少呢?最壞的情況,在這一秒內,這個buffer內的資料差一點滿了,但是還沒滿,因此沒有調flush,
    那麼在下一秒到達,剛要呼叫flush時,停電了,那麼最多就是丟失一個buffer的資料。但是如果寫的比較快的話, buffer很快就滿了,就會自動呼叫flush,因此一秒可能
    會呼叫三四次flush。所以上面那種情況觸發概率是非常低的。因此每秒這種是no這種最慢的和always這種最快的中間的一種,相對丟失資料較少的。

在配置檔案中有這麼個配置: aof-use-rdb-preamble yes預設是yes開啟的,這個配置是在aof中寫入rdb檔案。
因為在老的版本中,如果觸發重寫,那麼redis需要遍歷老的aof檔案,該抵消的抵消,該整合的整合,這是一個非常消耗cpu的計算判斷過程。使用這種方式的話,就把cpu
判定的過程給取消掉了,直接對用記憶體對磁碟的aof檔案做卸數,將記憶體的東西持久化,導成rdb這種方式寫入到aof檔案中,相當於加快了重寫的過程。最終aof檔案中同時
包含aof和rdb,aof是增量的。成一個混合體了。
檢視aof檔案,如果開頭出現了"redis"這個字串,則說明這是一個混合檔案,如果沒有則說明這是一個老的aof檔案。

rdb和aof實操

首先 ps -ef |grep redis保證沒有redis例項在執行。
修改redis的配置檔案,將後臺執行改為前臺阻塞執行 daemonize no並註釋掉日誌檔案 # logfile /var/log/redis_6379.log,因為日誌檔案開啟的話,
就會將東西記到日誌中,關了後會打到螢幕上。開啟aof配置 appendonly yes
然後先關閉 aof-use-rdb-preamble no,並刪除 /var/lib/redis/6379/dump.rdb檔案,這是以前的資料,清掉。然後啟動redis

[root@zhaoshuai ~]# service redis_6379 start
Starting Redis server...
62114:C 29 Oct 2020 02:36:19.138 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
62114:C 29 Oct 2020 02:36:19.138 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=62114, just started
62114:C 29 Oct 2020 02:36:19.138 # Configuration loaded
62114:M 29 Oct 2020 02:36:19.139 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 62114
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

62114:M 29 Oct 2020 02:36:19.143 # Server initialized
62114:M 29 Oct 2020 02:36:19.143 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
62114:M 29 Oct 2020 02:36:19.144 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
62114:M 29 Oct 2020 02:36:19.146 * Module 'bf' loaded from /usr/local/redis-5.0.5/RedisBloom-master/redisbloom.so
62114:M 29 Oct 2020 02:36:19.147 * Ready to accept connections

另啟一個視窗,開啟redis客戶端,然後執行 set k1 hello

[root@zhaoshuai ~]# redis-cli 
127.0.0.1:6379> set k1 hello
OK

檢視持久化目錄

[root@zhaoshuai redis]# cd /var/lib/redis/6379
[root@zhaoshuai 6379]# ll
total 4
-rw-r--r--. 1 root root 55 Oct 29 02:37 appendonly.aof

然後開啟這個檔案,看到如下內容:

*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$5
hello

*後面的數字表示後邊幾個元素組成。$後邊的數字表示這個元素是由幾個字元或幾個位元組組成。上面的內容就是aof的內容,而且這個檔案是很純淨的,沒有其他多餘的東西。
再新增一條 set k2 redis,再次開啟,可以看到每條命令都會追加到這個檔案後面
然後我們會看到上面沒有dump.rdb。
執行 save命令會阻塞服務,將資料打成dump.rdb;使用 bgsave後會重啟一個子程序,將服務落成dump.rdb。檢視前臺阻塞視窗日誌:

64351:M 29 Oct 2020 18:17:45.712 * DB saved on disk
64351:M 29 Oct 2020 18:19:05.981 * Background saving started by pid 64373
64373:C 29 Oct 2020 18:19:06.038 * DB saved on disk
64373:C 29 Oct 2020 18:19:06.038 * RDB: 6 MB of memory used by copy-on-write
64351:M 29 Oct 2020 18:19:06.110 * Background saving terminated with success

進入 /var/lib/redis/6379,可以看到有aof和rdb兩個檔案。

[root@zhaoshuai 6379]# ll
total 8
-rw-r--r--. 1 root root  87 Oct 29 18:17 appendonly.aof
-rw-r--r--. 1 root root 117 Oct 29 18:19 dump.rdb

使用vi命令,檢視dump.rdb

REDIS0009ú      redis-ver^E5.0.5ú
redis-bitsÀ@ú^EctimeÂ
j<9b>_ú^Hused-memÂ^H^^^M^@ú^Laof-preambleÀ^@þ^@û^B^@^@^Bk2^Eredis^@^Bk1^Ehelloÿ~<83>ED<85>þ·Q                                                                                         

dump.rdb是一個二進位制檔案,不太好看,可以使用命令: redis-check-rdb dump.rdb來檢查rdb檔案:

[root@zhaoshuai 6379]# redis-check-rdb dump.rdb 
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '5.0.5'
[offset 40] AUX FIELD redis-bits = '64'
[offset 52] AUX FIELD ctime = '1604020746'
[offset 67] AUX FIELD used-mem = '859656'
[offset 83] AUX FIELD aof-preamble = '0'
[offset 85] Selecting DB ID 0
[offset 117] Checksum OK
[offset 117] \o/ RDB looks OK! \o/
[info] 2 keys read
[info] 0 expires
[info] 0 already expired

現在rdb和aof檔案都有了,但是我們還沒有驗證 aof-use-rdb-preamble這個設定,我們在redis客戶端中執行:bgrewriteaof命令,開啟重寫,重寫完成後
檢視aof檔案:

*2
$6
SELECT
$1
0
*3
$3
SET
$2
k2
$5
redis
*3
$3
SET
$2
k1
$5
hello

發現aof檔案與之前沒有變化,開頭並沒有出現redis,所以是老的aof檔案。
然後我們在驗證重寫整合命令:在這之前我們存入了k1-hello,k2-redis,往後追加命令

127.0.0.1:6379> set k1 a
OK
127.0.0.1:6379> set k2 b
OK
127.0.0.1:6379> set k1 c
OK

檢視aof檔案:

*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$5
hello
*3
$3
set
$2
k2
$5
world
*3
$3
set
$2
k1
$1
a
*3
$3
set
$2
k2
$1
b
*3
$3
set
$2
k1
$1
c

可以看到所有的命令都是往後追加,這樣下去檔案就會非常大。我們執行重寫命令再次檢視檔案:

*2
$6
SELECT
$1
0
*3
$3
SET
$2
k1
$1
c
*3
$3
SET
$2
k2
$1
b

可以看到重寫之後檔案簡化了很多。

然後我們關閉redis前端阻塞服務(ctrl+C),刪除aof和rdb檔案,修改配置檔案開啟 aof-use-rdb-preamble yes,再次啟動服務。重複上面操作。

127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> set k2 redis
OK
127.0.0.1:6379> save
OK
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

再次檢視aof檔案:

REDIS0009ú      redis-ver^E5.0.5ú
redis-bitsÀ@ú^EctimeÂ^Mr<9b>_ú^Hused-memÂ^P^^^M^@ú^Laof-preambleÀ^Aþ^@û^B^@^@^Bk2^Eredis^@^Bk1^Ehelloÿúî^Y<9c><8a>î¨<9e>

可以看到開頭是redis開頭的,而且我們不難發現,這寫內容與上面看的rdb檔案的內容一樣。這也驗證了我們上面的說法,然後在往後追加資料,檢視日誌:

REDIS0009ú      redis-ver^E5.0.5ú
redis-bitsÀ@ú^EctimeÂ^Mr<9b>_ú^Hused-memÂ^P^^^M^@ú^Laof-preambleÀ^Aþ^@û^B^@^@^Bk2^Eredis^@^Bk1^Ehelloÿúî^Y<9c><8a>î¨<9e>*2^M
$6^M
SELECT^M
$1^M
0^M
*3^M
$3^M
set^M
$2^M
k3^M
$1^M
a^M
*3^M
$3^M
set^M
$2^M
k4^M
$1^M
b^M

我們可以看到,後續的操作都是追加在rdb檔案後面的。此時我們再次執行: bgrewriteaof

127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

然後檢視rdb檔案,可以看到這是一個新的rdb檔案,再次整合了之前追加的操作為rdb,後面的操作將追加在此rdb檔案後面:

REDIS0009ú      redis-ver^E5.0.5ú
redis-bitsÀ@ú^EctimeÂõr<9b>_ú^Hused-memÂp^^^M^@ú^Laof-preambleÀ^Aþ^@û^D^@^@^Bk4^Ab^@^Bk3^Aa^@^Bk2^Eredis^@^Bk1^Ehelloÿ<9f><8a>ÿ²â@ûß

經過上面的操作我們知道了,無論何時,aof中的檔案永遠都是最近一次rdb全量資料+追加增量資料,而且每次都會重寫這個檔案--aof檔案開頭都是新的rdb檔案,也就保證了
aof檔案永遠不會太大。利用了aof和rdb兩個的優點。

需要記住的兩個命令:
save/bgsave:儲存rdb檔案/後臺儲存rdb檔案
bgrewriteaof: aof重寫檔案

開啟aof-use-rdb-preamble後,aof檔案仍是正常的純命令檔案,之後執行重寫後才會變成混合檔案

之前我們學習rdb時,知道rdb有三個配置可以自動儲存:

save 900 1
save 300 10
save 60 10000

但是上面我們在使用aof的重寫時,都是手動觸發的,aof也有自動觸發機制。回顧關於aof的配置:

# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

我們大概可以翻譯一下這個註釋:意思就是自動重寫aof檔案。當aof檔案的大小增長到指定的百分比時,redis會通過呼叫bgwriteaof來實現重寫log檔案。
然後講了它是怎麼工作的:redis會記住最後一次重寫的檔案大小(如果重啟後沒有重寫發生過,那麼就會記住重啟時aof檔案的大小)
基礎大小會與當前的大小進行比較,如果當前大小比指定的百分比大的話,就會觸發重寫,你也需要指定重寫的最小aof檔案大小,這樣即使在百分比增加的情況下,也可以避免
重寫的aof仍然很小。
指定百分比為0可以禁用自動重寫
說人話:就是說redis會記住你最後一次重寫後文件的大小(如果我重啟後,還沒有觸發過重寫,那麼就會使用當前aof檔案的大小),如果當前aof檔案的大小與
我記住的aof檔案大小相比增長了100%,那麼就會觸發重寫。重寫後文件的大小就會減少。比如說我最後一次重寫後,檔案大小是32M,那麼當檔案增加到64M後,就會觸發重寫,
然後再次重寫後,檔案大小是40M,那麼我下次增加到80M我才會觸發重寫。 auto-aof-rewrite-percentage 100就是你來設定這個百分比的,當達到指定百分比就會
觸發重寫。那麼還有一個問題,就是你要知道,如果我是第一次啟動的話,那麼我沒有aof檔案,這樣的話,我就沒辦法知道我該跟誰比較,來決定觸發重寫。所以你需要再指定
auto-aof-rewrite-min-size這個配置,指定一個最小的重寫大小,然後第一次就跟這個比較,第一次重寫過之後,就會記住那個值,然後下次就是跟記住的大小進行比較。

redis叢集

之前一直學習使用的都是redis單機、單例項。單機會遇到的問題:

  1. 單點故障。也就是說單節點如果服務一旦掛了,那麼服務就不可用了。
  2. 記憶體大小。單個伺服器的記憶體大小是優先的,但是資料是隨著時間不斷增大的,記憶體就會不夠用。
  3. 壓力。客戶端連線數,socketI/O壓力,cpu計算壓力。

單節點的這三個問題怎麼解決?

只要出現單例項,那麼就可以通過AKF拆分原則解決。什麼是AKF?
AKF擴充套件立方,就是通過X,Y,Z三個座標軸的方向來解決問題。

  • X軸:用來解決單點故障,服務可用性的問題。也就是說沿著X軸,為服務多複製幾個一樣的例項(做備用節點),平時一個例項對外提供服務,並將自己的資料複製到備份
    節點,也就是一主多從的方式。主節點提供服務並將資料儲存到備用節點,一旦主節點宕機,備用節點立馬升級為主節點,保證服務的可用性。同時可以配置讀寫分離,降低主節點
    的壓力。

    注意:基於X軸做主從節點這種方式,一般都是全量映象(備用節點中儲存有主節點的所有資料)

  • Y軸:沿X軸做拓展只能解決單點故障的問題,也就是保證服務的可用性。但是並不能解決容量不足的情況。沿Y軸拓展就是將資料進行拆分,將資料按照型別或業務型別進行分類,
    然後將資料分散在不同的節點上。這樣就解決了容量不足的問題。

    X和Y並不是必須發生的,可以只有X,也可以只有Y,還可以X和Y一起使用。但是一般按照Y拆分的話,如果某一個節點掛了,那麼這個業務的資料就不可用了,所以會在X軸做備用節點

  • Z軸:在Y軸中對資料進行了分類,那麼當某一個分類的資料量非常大的時候,就可以在Z軸進行拓展。也就是說將Y軸某一個節點的資料再進行拆分,分散到多個節點上,
    但是必須要配置一個規則,按照這個規則去拆分資料,保證資料能夠分配到指定的節點。

對單節點的問題,通過以上AKF的拆分,然後請求肯定是訪問某一個型別的資料,這樣就會將請求分散到訪問資料的節點,也就間接減小了訪問壓力。

CAP原則

上面使用AKF解決了單節點的問題,但是一般解決一個問題就會引來新的問題。配置叢集會產生的問題:CAP

什麼是CAP?

  • C:Consistency(一致性)。後臺提供服務的多個節點之間資料保持一致
  • A: Availability(可用性)。使用者訪問服務,服務的響應時間在可以接受的範圍內。因為客戶端通過建立連線訪問服務,一般都會設定一個超時時間,如果超過時間的話,連線就會被關閉,那麼客戶端就會返回失敗,那麼如果每次都因為超時返回失敗的話,客戶端就會認為這個服務是不可用的。
  • P: Partition tolerance(分割槽容錯性)。高可用,一個節點宕機,並不影響其他的節點。

CAP原則就是值:上面這三個要素最多隻能同時實現兩個,不能三個都保證。

為什麼?
假設現在需要搭建一個redis服務,那麼首先我們建立一個單例項的節點。為了解決單點故障的問題,我們就根據AKF原則沿X軸擴充套件,搭建主備節點,來提高可用性。
那麼我們現在搭建一個一主兩備的服務,單點故障問題解決了,可用性提高了。但是當出現主備或主從服務時,如果我們現在要往redis中存資料,那麼就要保證資料的一致性。
也就是說我們要保證主節點和備用節點(或從節點)的資料時一樣的。

關於主備和主從節點的區別:
主備:主節點對外提供讀寫服務,當主節點可用時,備節點就只是單純的同步主節點的資料,並不對外提供服務。當主節點宕機時,我們可以啟用備用節點,保證服務可用。
也就是說當主節點可用時,備節點是不可用的。
主從:主節點對外提供讀寫服務,從節點只能對外提供讀的服務,也就是說從節點是隻讀的。

主節點對外提供寫的服務,備節點或從節點,要麼不對外提供服務,要麼提供讀的服務,所以資料只能從主節點同步到備節點或從節點。
當出現這種多個節點資料同步時,那麼就會出現一個問題:資料一致性

什麼是資料一致性?

  • 當服務為主備節點時,那麼只有主節點對外提供讀寫的服務,當插入一條資料時,會儲存到主節點中,並同時儲存到備份節點中。當一條資料儲存到主節點中,還沒來的及
    儲存到備用節點,主節點宕機了,那麼備用節點就會少一條資料,主節點和備用節點的資料就會不一致。
  • 當服務為主從節點時,那麼主節點發生寫操作,從節點發生讀操作,那麼當網路延遲或其他情況,導致主節點中的資料沒有及時的同步到從節點,就會造成從節點中的資料和
    主節點不一致。

經過上面的分析,發現數據的不一致的原因都是因為主節點向從節點或備份節點同步資料時出現問題造成的。那麼這個問題怎麼解決?
當有一條寫操作到達主節點時,主節點會先儲存到本地,然後再將資料儲存到備份節點,那麼此時有兩種方案儲存到備份節點:

  1. 同步的方式: 主節點寫入成功後,並不立刻響應客戶端,而是會在主節點阻塞並同時向兩個備份節點插入資料,只有兩個備份節點都返回ok,主節點才會返回ok。
  2. 非同步的方式: 主節點寫入成功後,立刻返回客戶端ok,並通過非同步的方式將資料寫入從節點或備份節點。

我們分析上面兩種同步資料的方式的優缺點:

  1. 同步的方式:
    • 優點: 所有節點的資料都是一致的,因為主節點會阻塞,只有所有節點的資料都儲存成功,才算儲存成功。這是強一致性
    • 缺點: 發生阻塞,但是客戶端會有一個超時的概念,也就是說,當阻塞時間過長時,客戶端會發生超時,那麼就會認為儲存失敗,這樣的話,客戶端就會認為服務不可用。
      因為我想要儲存一條資料,但是儲存失敗。這樣就會破壞服務的可用性。
  2. 非同步的方式:
    • 優點: 主節點儲存成功就會立即返回,響應速度快。而且不會影響服務的可用性。
    • 缺點: 無法保證資料的一致性。因為如果主節點在準備將資料儲存到備用節點,但是還沒有開始時,主節點宕機了,那麼就會造成備用節點資料丟失。

分析了上面兩種方式,我們發現,如果要保證資料的強一致性,就會破壞服務的可用性,如果要保證服務的可用性,那麼就一定會影響資料的一致性。
怎麼解決上面的問題?
看起來好像是一個死迴圈,企業都想要追求資料的強一致性,但是這樣卻會破壞可用性,為了解決這個問題,只能將資料一致性降級。
資料一致性分為:

  • 強一致性
  • 弱一致性
  • 最終一致性

強一致性會破壞服務可用性,那麼往下降級,使用弱一致性,但是使用資料弱一致性就要忍受資料的丟失,只會丟失一點資料。為了解決弱一致性中出現數據丟失的問題,就出現了最終一致性。
最終一致性的解決方案:
就是客戶端寫操作進來後,首先肯定是進入主節點,然後我主節點往自己本地儲存成功後,並不是先往備用節點儲存,而是在主節點和備用節點中間加上一個類似kafka這種功能的服務,
這個服務首先要保證高可用,就是redis主節點往這個服務裡寫資料不能失敗,還要保證夠快;然後主節點將資料寫入這個中間層後,就返回給客戶端成功。然後備用節點從這個中間層
拿資料寫到本地,這樣就能保證最終各個節點的資料是一致的,這就是最終一致性。

注意:redis並沒有使用這種方式,redis是怎麼做的,後面講解,現在講的CAP原則的一致性。

最終一致性會帶來的問題就是,在達到最終一致性之前,可能主從節點的資料可能會出現短暫的不一致,但是最終是一致的。所以最終一致性也是屬於弱一致性的。

上面我們說了一致性的問題,那麼再想一下,現在無論我們是主備節點還是主從節點,都有一個什麼?,那麼問題就是這個主也是個單節點。
此時想一下,我們上面因為單節點的問題,又是做主備,又是做主從,然後還引出了資料一致性的問題,以及可用性的問題,繞了一大圈,又回到了單節點的問題:主節點是單節點。
主節點是單節點的問題: 如果是主備的話,那麼主節點掛了的話,服務就不可用了,如果是主從節點,那麼主節點掛了的話,只能讀,不能寫了。
所以我們就需要對主節點做高可用。也就是將一個備節點切換成主節點。
高可用是隻對主節點做的,如果是備節點掛了也就掛了,並不會對客戶端有影響,因為只有主節點對外提供服務。
那麼高可用強調的是我這個主節點永遠不會出現問題。(當然這是不可能的),所以高可用更傾向的是當主節點出現問題時,我能夠立馬出現一個新的主節點出來,對外表現
是並沒有出現問題。
那麼出現新節點這個事情,應該怎麼做呢?
肯定不能人來做,我不可能讓一個人來時時刻刻看他有沒有宕機,所以應該有一個程式來處理,實現故障轉移,對外表現一種高可用的狀態,也就是說通過程式監控主節點
當主節點出現問題時,能夠及時的將備用節點切換成主節點,將出現故障的原主節點撤下來。
那麼程式如何來監控這個服務有沒有出現問題?首先要想,這個程式肯定不能是一個,因為如果是一個的話,就會出現單點故障的問題,所以這個程式也應該是一個叢集。那麼這個
監控程式是叢集,那麼就會引出一個新問題:
假設這個叢集有三個例項,那麼就是說三個程式監控一個redis節點,那麼這三個監控如何確定這個redis有沒有問題呢?
三個監控去監控這一臺redis主節點,首先,每個監控程式肯定要能與redis之間進行通訊,不能通訊還怎麼監控。然後如果說我有一臺監控程式因為網路問題,無法與redis主節點通訊了,
但是另外兩臺都是可以正常與redis節點通訊的,那麼這時候我怎麼能肯定這個主節點到底有沒有問題呢?
問題也就是說: 現在三個監控,有一個監控說redis掛了,因為他出現了網路問題,無法與主節點通訊了,他認為redis主節點掛了,但是其實人家是正常的,另外兩個都能正常訪問的
那這時候該怎麼辦呢?不能你說他掛了他就掛了,所以這三個監控程式之間也應該能夠通訊。那麼這三臺之間通訊的話,就會有資料一致性問題了,我現在三臺一塊商量這個redis到底有沒有掛,
那如果有一個節點出現網路問題或其他問題,無法通訊了,那我如果要保證強一致性,另外兩臺就會阻塞等待第三臺也給出一個意見,但是第三個已經掛了,那麼此時這個監控也就
不可用了。所以這三臺監控之間不能是強一致性,因為強一致性會影響可用性。那麼既然三臺不能強一致性,那麼我三臺該怎麼商量他到底有沒有問題,也就是說該不該替換掉這個
主節點?既然不能三臺都給出的話,那麼只能是一部分節點給出這個決定,只要這一部分給出的結果是要替換,那麼就把當前的主節點給替換掉。那這個一部分應該是幾臺呢?

很容易想到的就是半數選舉。現在我們就推導這個半數是怎麼來的:

假如說我們現在這個監控程式有五個節點,那麼這個一部分能不能是一臺呢?肯定不能。其實上面已經分析過了,一個節點給出決定的話,有可能是因為這個監控程式自身的問題,
其實主節點是正常的。自己的問題自己不知道還認為是別人的問題,這肯定是不行的。而且如果一臺能夠決定的話,會出現競爭,你說他掛了,但是我認為他沒掛,到底該聽誰的。
那麼兩臺呢?也就是說,我有兩臺監控,然後我倆一商量都認為redis的主節點掛了,那這時候該不該換掉他呢?其實和一個節點的時候一樣,有可能你這兩個節點的網路網段變了,
人家另外三臺還是可以正常連線的,所以此時,也不應該替換掉。為什麼,因為你這兩個節點結成一個勢力了,但是人家另外三臺節點也結成另一個勢力了,你的勢力沒有人家的大。
人家三個人,你打不過人家,所以誰拳頭大誰做主,人家三個說他沒掛,那就是沒掛,三個說他掛了,那他就是掛了。即使是這三個的網段變了,主節點真的沒掛,但是人家三個
拳頭大, 說他掛他就得掛。這樣我們就推匯出半數選舉了。

我們應該在搭建叢集的時候都聽過一句話: 奇數臺最好。

無論是redis還是分散式裡的註冊中心,都是搭建奇數臺,為什麼奇數臺最好?
首先我們分析,當數量為3臺時,那麼此時允許出現故障的數量是1臺,因為三臺時只有兩臺才能結成勢力作出決定。那麼四臺的時候呢?允許出現故障的數量也是1臺。如果有兩臺出現故障,
那麼出現故障的勢力也是2,這樣就無法決定到底該聽哪兒個的了,無法給出最終決策,所以允許出現故障的數量也是1臺。那麼同樣都是1臺。3臺和4臺的成本可是不一樣的,
四臺更貴,而且四臺比三臺更容易出故障,所以根據經驗,奇數臺最好。

腦裂問題(出現腦裂問題的前提是沒有考慮過半機制)

什麼是腦裂?我們人只有一個大腦,在分散式服務中,多個例項也只能有一個主節點,這個主節點就是大腦。那麼腦裂就是現在一個大腦裂開了,變成了兩個甚至三個大腦。
對映到服務中就是出現了多個主節點。
腦裂問題怎麼出現的?
前面我們分析了,為了實現高可用,要對主從節點搭建監控服務,監控主節點的心跳狀態。比如說我們現在有五臺服務,假設現在網路出現問題了,然後五個監控程式分成了兩個網段。
有三臺是一組,這三臺之間可以互相通訊,另外兩臺為一組,這兩臺之間也可以互相通訊。但是隻有一組能夠跟主節點通訊,那麼無法跟主節點通訊的這一組,就會自成一派,然後倆人合計以分析
主節點掛了,然後就將一個從節點提成主節點了,那麼這時就會造成一個服務中出現兩個主節點,這兩個主節點間無法通訊,客戶端每次訪問時,可能訪問這個主節點,也可能訪問
另一個主節點,而且這兩個主節點的資訊是無法保證一致的,老得主節點中儲存的是老得資料,新的主節點中儲存新的資料。但是因為對外表現高可用,客戶端是不知道你服務有兩個主節點的。
那麼對外表現是就是,我客戶端呼叫服務,然後可能每次呼叫結果都不一樣。

分割槽容忍性

腦裂問題是由於網路分割槽引起的。其實這種服務對網路分割槽有一個分割槽容忍性的概念。也就是說能不能容忍腦裂時出現的資料不一致問題。
例如,在微服務中,都會有一個註冊中心。註冊中心就是用來存放提供服務的例項資訊,例如有個訂單業務,有十個例項,但是因為出現了網路分割槽的問題,此時這邊註冊了八臺,
那邊註冊了兩臺,但是請求過來後,並不關心你總共多少臺,只需要給我一個能用的例項我去呼叫就行了,不管是八臺的還是兩臺的,都可以提供一個服務給客戶端呼叫,而且能夠呼叫
成功,這時就是可以容忍網路分割槽的。

這一塊自己講的時候總感覺被繞進去,其實就是要搞清楚一個點:
過半選舉:如果是過半選舉的話,肯定要保證,各個監控之間是能夠正常通訊的,多個監控共同監控一個主節點,可能某一個或幾個節點無法與主節點通訊了,才會進行選舉新的主節點。
如果監控之間無法通訊了,那就會出現腦裂了。

上面我們一起分析了CAP三大原則,那麼回到最初的問題,為什麼這三個原則不能同時滿足,最多隻能同時滿足兩個?

CA:如果既要滿足強一致性,又要滿足可用性,那隻能選擇單機了。單機這兩點都滿足,但是高可用達不到,因為高可用要求就是叢集。
CP:如果要滿足高可用,就要搭建叢集,搭建叢集,如果要滿足資料強一致性,那麼就會破壞可用性(一致性裡分析過)。
AP:如果要同時滿足可用性,和高可用性。前面也說過了,要滿足可用性,就會破壞強一致性。滿足強一致性救護破壞可用性,只要滿足P,那麼C和A就只能選擇一個。

redis叢集搭建

redis的叢集搭建,採用的是主從複製叢集,主從複製採用非同步複製,因為非同步的可用性更高。回顧一致性中,保證可用性的話,就會破壞一致性。因此redis是弱一致性的。
容易丟失資料。為什麼不採用最終一致性?因為redis的特點就是快,為了快,就要減少技術整合。

redis叢集的搭建

  1. 首先停掉所有的redis例項, ps -ef |grep redis
  2. 通過install_server建立三個redis例項(在一臺伺服器上面通過開啟多個埠開啟多例項)6379,6380,6381三個服務。並關閉服務。
  3. 那麼現在就有了三個redis例項,我們搭建一個以6379為主,6380和6381為從節點的主從服務。那麼修改6380和6381的配置檔案,修改引數 5.0版本前是slaveof
    5.0版本之後是replicaof更改為 replicaof 127.0.0.1 6379
  4. 刪除這三個例項的日誌檔案,並啟動主節點以及兩個從節點。 rm -rf /var/log/redis*
[root@zhaoshuai log]# service redis_6379 start
Starting Redis server...
[root@zhaoshuai log]# service redis_6380 start
Starting Redis server...
[root@zhaoshuai log]# service redis_6381 start
Starting Redis server...
  1. 使用tail監控6379日誌:
.....
74730:M 01 Nov 2020 23:58:30.422 * Module 'bf' loaded from /usr/local/redis-5.0.5/RedisBloom-master/redisbloom.so
74730:M 01 Nov 2020 23:58:30.423 * DB loaded from disk: 0.000 seconds
74730:M 01 Nov 2020 23:58:30.423 * Ready to accept connections
74730:M 01 Nov 2020 23:58:36.817 * Replica 127.0.0.1:6380 asks for synchronization
74730:M 01 Nov 2020 23:58:36.817 * Full resync requested by replica 127.0.0.1:6380
74730:M 01 Nov 2020 23:58:36.817 * Starting BGSAVE for SYNC with target: disk
74730:M 01 Nov 2020 23:58:36.818 * Background saving started by pid 74747
74747:C 01 Nov 2020 23:58:36.834 * DB saved on disk
74747:C 01 Nov 2020 23:58:36.835 * RDB: 6 MB of memory used by copy-on-write
74730:M 01 Nov 2020 23:58:36.912 * Background saving terminated with success
74730:M 01 Nov 2020 23:58:36.912 * Synchronization with replica 127.0.0.1:6380 succeeded
74730:M 01 Nov 2020 23:58:40.359 * Replica 127.0.0.1:6381 asks for synchronization
74730:M 01 Nov 2020 23:58:40.359 * Full resync requested by replica 127.0.0.1:6381
74730:M 01 Nov 2020 23:58:40.359 * Starting BGSAVE for SYNC with target: disk
74730:M 01 Nov 2020 23:58:40.360 * Background saving started by pid 74761
74761:C 01 Nov 2020 23:58:40.425 * DB saved on disk
74761:C 01 Nov 2020 23:58:40.425 * RDB: 6 MB of memory used by copy-on-write
74730:M 01 Nov 2020 23:58:40.428 * Background saving terminated with success
74730:M 01 Nov 2020 23:58:40.428 * Synchronization with replica 127.0.0.1:6381 succeeded

可以看到上面的日誌,主節點同步從節點6380和6381。並落rdb檔案到這兩個例項。
6. 開啟一個從節點日誌:

...
74743:S 01 Nov 2020 23:58:36.814 * DB loaded from disk: 0.002 seconds
74743:S 01 Nov 2020 23:58:36.814 * Ready to accept connections
74743:S 01 Nov 2020 23:58:36.814 * Connecting to MASTER 127.0.0.1:6379
74743:S 01 Nov 2020 23:58:36.817 * MASTER <-> REPLICA sync started
74743:S 01 Nov 2020 23:58:36.817 * Non blocking connect for SYNC fired the event.
74743:S 01 Nov 2020 23:58:36.817 * Master replied to PING, replication can continue...
74743:S 01 Nov 2020 23:58:36.817 * Partial resynchronization not possible (no cached master)
74743:S 01 Nov 2020 23:58:36.819 * Full resync from master: a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3:0
74743:S 01 Nov 2020 23:58:36.912 * MASTER <-> REPLICA sync: receiving 192 bytes from master
74743:S 01 Nov 2020 23:58:36.912 * MASTER <-> REPLICA sync: Flushing old data
74743:S 01 Nov 2020 23:58:36.912 * MASTER <-> REPLICA sync: Loading DB in memory
74743:S 01 Nov 2020 23:58:36.912 * MASTER <-> REPLICA sync: Finished with success

可以注意到 connecting to MASTER 127.0.0.1:6379連線主節點以及Flushing old data清空就的資料,然後loading DB in memory載入主節點的資料。

至此一個redis主從複製叢集就搭建成功了。在主節點中存入資料,然後到從節點中可以檢視資料。主節點的資料通過rdb的方式傳遞給從節點,從節點載入rdb檔案獲取主節點的資料。
而且嘗試在從節點中寫資料會發現,從節點是隻讀的。不能寫資料(可以在配置檔案中調)
此時是一主兩叢的叢集,那麼主節點可能會掛,從節點也可能會掛。我們現在考慮主節點健康,從節點掛的時候,它的資料同步方式是怎樣的?
是主節點將所有的資料打一個rdb檔案,從節點重新拉取資料?還是說從節點只同步主節點增量資料?
我們將從節點6381停掉。然後在主節點中寫入資料k3,此時從節點6381是停掉的,
所以肯定不可能存入資料k3,然後我們再啟動6381節點,檢視k3。發現6381是能拿到k3的值的。檢視6381的日誌:

....
74883:S 02 Nov 2020 00:27:24.481 * Ready to accept connections
74883:S 02 Nov 2020 00:27:24.481 * Connecting to MASTER 127.0.0.1:6379
74883:S 02 Nov 2020 00:27:24.482 * MASTER <-> REPLICA sync started
74883:S 02 Nov 2020 00:27:24.482 * Non blocking connect for SYNC fired the event.
74883:S 02 Nov 2020 00:27:24.482 * Master replied to PING, replication can continue...
74883:S 02 Nov 2020 00:27:24.483 * Trying a partial resynchronization (request a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3:2304).
74883:S 02 Nov 2020 00:27:24.483 * Successful partial resynchronization with master.
74883:S 02 Nov 2020 00:27:24.483 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.

從節點重啟後會重新嘗試去主節點同步資料。而且重啟後沒有落rdb檔案的事,也就是說,6381曾經追隨過6379,現在重啟後,只會同步增量的資料。
再此將6381停掉,然後修改配置檔案,開啟aof appendonly yes,然後發現,本來是不落rdb檔案的,也就是說本來重啟後我應該只同步增量資料的。但是現在我每次重啟都會
落rdb檔案

75017:S 02 Nov 2020 01:11:38.103 * Ready to accept connections
75017:S 02 Nov 2020 01:11:38.103 * Connecting to MASTER 127.0.0.1:6379
75017:S 02 Nov 2020 01:11:38.105 * MASTER <-> REPLICA sync started
75017:S 02 Nov 2020 01:11:38.105 * Non blocking connect for SYNC fired the event.
75017:S 02 Nov 2020 01:11:38.106 * Master replied to PING, replication can continue...
75017:S 02 Nov 2020 01:11:38.106 * Partial resynchronization not possible (no cached master)
75017:S 02 Nov 2020 01:11:38.115 * Full resync from master: a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3:6149
75017:S 02 Nov 2020 01:11:38.254 * MASTER <-> REPLICA sync: receiving 216 bytes from master
75017:S 02 Nov 2020 01:11:38.254 * MASTER <-> REPLICA sync: Flushing old data
75017:S 02 Nov 2020 01:11:38.254 * MASTER <-> REPLICA sync: Loading DB in memory
75017:S 02 Nov 2020 01:11:38.254 * MASTER <-> REPLICA sync: Finished with success
75017:S 02 Nov 2020 01:11:38.255 * Background append only file rewriting started by pid 75022
75017:S 02 Nov 2020 01:11:38.486 * AOF rewrite child asks to stop sending diffs.
75022:C 02 Nov 2020 01:11:38.486 * Parent agreed to stop sending diffs. Finalizing AOF...
75022:C 02 Nov 2020 01:11:38.486 * Concatenating 0.00 MB of AOF diff received from parent.
75022:C 02 Nov 2020 01:11:38.486 * SYNC append only file rewrite performed
75022:C 02 Nov 2020 01:11:38.486 * AOF rewrite: 6 MB of memory used by copy-on-write
75017:S 02 Nov 2020 01:11:38.535 * Background AOF rewrite terminated with success
75017:S 02 Nov 2020 01:11:38.535 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)
75017:S 02 Nov 2020 01:11:38.535 * Background AOF rewrite finished successfully

又是刪除舊的資料,拉master的所有資料。為什麼開啟aof後就落rdb檔案了?
前面說過,當開啟aof後,rdb就不生效了,會直接載入aof檔案。rdb可以儲存曾經追隨誰的資訊,但是aof雖然是一個混合檔案,但是aof檔案並不包含曾經追隨誰的資訊。
所以開啟aof後,每次重啟都會重新落rdb檔案。
檢視rdb檔案:

REDIS0009ú      redis-ver^E5.0.5ú
redis-bitsÀ@ú^EctimeÂJÍ<9f>_ú^Hused-memÂàp^]^@ú^Nrepl-stream-dbÀ^@ú^Grepl-id(a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3ú^Krepl-offsetÁ^E^Xú^Laof-preambleÀ^@þ^@û^C^@^@^Bk3
test_slave^@^Bk1^Ehello^@^Bk2^Eworldÿ^F^LeØ6_â®

注意上面這段資訊: repl-id(a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3
檢視aof,aof中的rdb檔案中是沒有這個資訊的,所以開啟aof時,每次都重啟都載入aof檔案,然後aof檔案並不記載曾經追隨誰的資訊,因此每次重啟都要重新rdb。
上面是從節點掛機的情況,從節點掛機並不影響主節點對外提供服務,而且在主節點的日誌中,可以看到從節點連線資訊,也就是說從節點掛了的話,在主節點也可以看到。
主節點可以知道主上連了多少從。那麼當主節點宕機時,怎麼處理?
我們將主節點6379服務停掉, service redis_6379 stop,然後檢視從節點的日記:

...
74743:S 02 Nov 2020 02:15:46.525 # Error condition on socket for SYNC: Connection refused
74743:S 02 Nov 2020 02:15:47.550 * Connecting to MASTER 127.0.0.1:6379
74743:S 02 Nov 2020 02:15:47.551 * MASTER <-> REPLICA sync started
74743:S 02 Nov 2020 02:15:47.551 # Error condition on socket for SYNC: Connection refused
74743:S 02 Nov 2020 02:15:48.575 * Connecting to MASTER 127.0.0.1:6379
74743:S 02 Nov 2020 02:15:48.575 * MASTER <-> REPLICA sync started
74743:S 02 Nov 2020 02:15:48.575 # Error condition on socket for SYNC: Connection refused

可以看到從節點在一直不停的嘗試與主節點建立連線,如果主節點此時重啟的話,那麼就又可以建立連線了。但是我們現在把主節點停了,也就是說主節點不會啟動了。比如說主節點硬體
故障,啟動不了了。那麼此時剩兩個從節點,從節點仍然可以被外部連線,可以提供讀的服務,也就是可以讀到以前寫的資料,但是現在無法提供寫的服務了。
那麼當出現主節點出現故障時,我們是不是需要啟動一個主節點。當沒有監控程式自動去切換時,我們只能手動去將一個從節點升為主節點。
redis-cli -p 6380連線上6380服務,然後將6380升為主節點 replicaof no one

127.0.0.1:6380> REPLICAOF no one
OK

然後檢視日誌就可以看到,6380變成了主節點--MASTER MODE enabled

74743:M 02 Nov 2020 02:21:26.020 # Setting secondary replication ID to a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3, valid up to offset: 11372. New replication ID is 0f23bd1c62e6305778fe26e237e05cfbab645c50
74743:M 02 Nov 2020 02:21:26.020 * Discarding previously cached master state.
74743:M 02 Nov 2020 02:21:26.020 * MASTER MODE enabled (user request from 'id=4 addr=127.0.0.1:43396 fd=7 name= age=8 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=36 qbuf-free=32732 obl=0 oll=0 omem=0 events=r cmd=replicaof')

然後讓6381追隨6380節點,因為雖然6380已經變成了主節點,但是6381仍然追隨的是6379節點,他並不知道主節點已經變了,6381仍在等待6379提供服務。

[root@zhaoshuai ~]# redis-cli -p 6381
127.0.0.1:6381> REPLICAOF 127.0.0.1 6380
OK

檢視6381節點日誌:

75200:S 02 Nov 2020 02:50:50.527 * REPLICAOF 127.0.0.1:6380 enabled (user request from 'id=4 addr=127.0.0.1:46092 fd=7 name= age=11 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=44 qbuf-free=32724 obl=0 oll=0 omem=0 events=r cmd=replicaof')
75200:S 02 Nov 2020 02:50:51.087 * Connecting to MASTER 127.0.0.1:6380
75200:S 02 Nov 2020 02:50:51.088 * MASTER <-> REPLICA sync started
75200:S 02 Nov 2020 02:50:51.088 * Non blocking connect for SYNC fired the event.
75200:S 02 Nov 2020 02:50:51.088 * Master replied to PING, replication can continue...
75200:S 02 Nov 2020 02:50:51.088 * Trying a partial resynchronization (request a6d3f64c1b125dc3178e7bbf4abd3abedbbdfaa3:11372).
75200:S 02 Nov 2020 02:50:51.088 * Successful partial resynchronization with master.
75200:S 02 Nov 2020 02:50:51.088 # Master replication ID changed to 0f23bd1c62e6305778fe26e237e05cfbab645c50
75200:S 02 Nov 2020 02:50:51.088 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.

這樣當主節點掛機時,通過手動方式就將一個從節點升為了主機點。此時如果6379節點再次重新啟動的話,那麼只能讓它作為一個從節點追隨新的主節點。因為此時已經有主節點了。
在redis的配置檔案中有與主從配置有關的:

# 控制從節點是否是隻讀節點
replica-read-only yes
# 這個配置是當你的一個redis服務啟動時,他會追隨一個主節點,主節點資料量非常大的話,同步到從節點是需要時間的,那麼在傳輸資料的時間內,從節點的資料是否對外支援
# 查詢,yes的話就是支援查詢老得資料,no就是不支援
replica-server-stale-data yes
# 複製策略:磁碟或套接字
# 磁碟IO: redis主節點資料落rdb,然後通過磁碟IO的方式同步到從節點。
# 網路IO: redis主節點落rdb檔案後,直接將rdb檔案傳給副本集的套接字,不接觸磁碟。
# 兩種方式根據網路頻寬選擇,yes時表示開啟套接字傳輸
repl-diskless-sync no
# 配置增量資料訊息佇列大小
# redis配置主從節點後,從節點會追隨主節點,並在第一次啟動時,載入主節點的rdb檔案,後期如果從節點掛的話,會讀取主節點的增量資料,這些增量資料就存放在訊息佇列中。
# 如果增加資料比訊息佇列大的話,資料就會丟失,從節點就需要重新載入主節點rdb。
repl-backlog-size 1mb

# 指定redis連線的最小副本集數量
min-replicas-to-write 3
# 每個副本集連線最大延遲時間
min-replicas-max-lag 10
# 當redis當前連線副本集小於指定數量並且延時大於最大延時數時,redis會拒絕寫請求。

上面是通過手動切換主節點來實現故障轉移,但是實現高可用的目標是自動故障轉移。redis提供了哨兵機制--Sentinel來實現高可用

redis哨兵

前面我們說CAP時,聊過通過監控來保證保證高可用,redis就是通過Sentinel哨兵來監控redis例項。

如何啟用哨兵?

首先我們先重啟之前搭建的三臺redis例項,恢復6379為主,6380、6381為從節點的主從複製副本集。檢視日誌,確保正常。
在編譯redis時,在 /usr/local/bin目錄下,除了有 redis-cli``redis-server外,還有一個指令碼redis-sentinel

[root@zhaoshuai bin]# pwd
/usr/local/bin
[root@zhaoshuai bin]# ll
total 64500
-rwxr-xr-x. 1 root root 10423037 Oct 21 09:02 mysql
-rwxr-xr-x. 1 root root  9202939 Oct 20 14:46 redis-benchmark
-rwxr-xr-x. 1 root root 12277485 Oct 20 14:46 redis-check-aof
-rwxr-xr-x. 1 root root 12277485 Oct 20 14:46 redis-check-rdb
-rwxr-xr-x. 1 root root  9580048 Oct 20 14:46 redis-cli
lrwxrwxrwx. 1 root root       12 Oct 20 14:46 redis-sentinel -> redis-server
-rwxr-xr-x. 1 root root 12277485 Oct 20 14:46 redis-server

可以看到redis-sentinel是一個軟連線,連線的時redis-server,也就是說也可以通過redis-server來啟動一個哨兵。
一個哨兵監控一個redis例項,因此redis主從三個節點需要三個哨兵來監控。

使用redis-sentinel來啟動哨兵。

首先我們先執行一下 redis-sentinel指令碼

[root@zhaoshuai bin]# ./redis-sentinel 
77530:X 02 Nov 2020 18:56:14.552 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
77530:X 02 Nov 2020 18:56:14.552 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=77530, just started
77530:X 02 Nov 2020 18:56:14.552 # Warning: no config file specified, using the default config. In order to specify a config file use ./redis-sentinel /path/to/sentinel.conf
77530:X 02 Nov 2020 18:56:14.553 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 77530
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

77530:X 02 Nov 2020 18:56:14.561 # Sentinel started without a config file. Exiting...
[root@zhaoshuai bin]# 

通過日誌可以看到,啟動失敗,失敗原因時沒有配置檔案,然後再網上看,發現預設的載入配置檔案的路徑是 /path/to/sentinel.conf
那麼我們就建立這個檔案(這個檔案的路徑可以自定義), 每一個監控都有一個配置檔案,我們就在redis的配置檔案路徑下建立哨兵的配置檔案。

[root@zhaoshuai /]# cd /etc/redis/
[root@zhaoshuai redis]# ll
total 192
-rw-r--r--. 1 root root 61916 Nov  1 23:40 6379.conf
-rw-r--r--. 1 root root 61898 Nov  1 23:56 6380.conf
-rw-r--r--. 1 root root 61898 Nov  2 01:55 6381.conf
[root@zhaoshuai redis]# touch 6379-sentinel.conf
[root@zhaoshuai redis]# vi 6379-sentinel.conf 
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2

簡單的配置檔案,既然哨兵是一個特殊的redis-server,那麼他也有埠號,因此要配置埠號, sentinel monitor mymaster 127.0.0.1 6379 2配置
sentinel表示這是一個哨兵配置,monitor監控,表示他要監控的節點是什麼,mymaster表示給這個節點起的一個名字,隨便起,127.0.0.1 6379 表示監控的節點資訊
2表示權重,表示投票範圍,也就是說,當主節點宕機時,有多少票說話才能管用。
配置完成後啟動哨兵

[root@zhaoshuai redis]# redis-sentinel ./6379-sentinel.conf 
77680:X 02 Nov 2020 19:34:26.122 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
77680:X 02 Nov 2020 19:34:26.122 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=77680, just started
77680:X 02 Nov 2020 19:34:26.122 # Configuration loaded
77680:X 02 Nov 2020 19:34:26.123 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 5.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 77680
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

77680:X 02 Nov 2020 19:34:26.126 # Sentinel ID is 2e7cbc211f52b61772e594a3e2d1bb1cf1a46322
77680:X 02 Nov 2020 19:34:26.126 # +monitor master mymaster 127.0.0.1 6379 quorum 2
77680:X 02 Nov 2020 19:34:26.127 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
77680:X 02 Nov 2020 19:34:26.128 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379

啟動成功了,而且我們只監控了主節點,列印的日誌中自動加入了從節點的資訊,因為從節點連線主節點,主節點中包含了從節點的資訊。
監控肯定也是叢集,不能是一臺,否則的話無法投票,使用 ctrl+C退出當前哨兵。因為現在是前臺阻塞的。
刪除原配置檔案,因為開啟看一下可以發現啟動後添加了很多其他配置,我們刪掉重新配置 :

port 26380
daemonize yes
logfile /var/log/sentinel-26380.log
sentinel monitor mymaster 127.0.0.1 6379 2

然後

[root@zhaoshuai redis]# mv 6379-sentinel.conf 26379.conf
[root@zhaoshuai redis]# ll
total 196
-rw-r--r--. 1 root root   394 Nov  2 19:34 26379.conf
-rw-r--r--. 1 root root 61916 Nov  1 23:40 6379.conf
-rw-r--r--. 1 root root 61898 Nov  1 23:56 6380.conf
-rw-r--r--. 1 root root 61898 Nov  2 01:55 6381.conf
[root@zhaoshuai redis]# cp 26379.conf 26380.conf
[root@zhaoshuai redis]# cp 26379.conf 26381.conf

修改26380、26381配置檔案中的埠號及日誌檔名為相應埠號。

[root@zhaoshuai redis]# ll
total 204
-rw-r--r--. 1 root root   444 Nov  2 19:48 26379.conf
-rw-r--r--. 1 root root   444 Nov  2 19:50 26380.conf
-rw-r--r--. 1 root root   444 Nov  2 19:50 26381.conf
-rw-r--r--. 1 root root 61916 Nov  1 23:40 6379.conf
-rw-r--r--. 1 root root 61898 Nov  1 23:56 6380.conf
-rw-r--r--. 1 root root 61898 Nov  2 01:55 6381.conf
[root@zhaoshuai redis]# vi 26380.conf 
port 26380
daemonize yes
logfile /var/log/sentinel-26380.log
sentinel monitor mymaster 127.0.0.1 6379 2

啟動三個哨兵

[root@zhaoshuai redis]# redis-sentinel 26379.conf 
[root@zhaoshuai redis]# redis-sentinel 26380.conf 
[root@zhaoshuai redis]# redis-sentinel 26381.conf 
[root@zhaoshuai redis]# ps -ef |grep redis-sentinel
root      77729      1  0 19:52 ?        00:00:00 redis-sentinel *:26379 [sentinel]
root      77734      1  0 19:52 ?        00:00:00 redis-sentinel *:26380 [sentinel]
root      77739      1  0 19:52 ?        00:00:00 redis-sentinel *:26381 [sentinel]
root      77744  77305  0 19:53 pts/1    00:00:00 grep redis-sentinel

此時再檢視26379的日誌:

77890:X 02 Nov 2020 20:08:15.532 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
77890:X 02 Nov 2020 20:08:15.532 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=77890, just started
77890:X 02 Nov 2020 20:08:15.532 # Configuration loaded
77891:X 02 Nov 2020 20:08:15.535 * Increased maximum number of open files to 10032 (it was originally set to 1024).
77891:X 02 Nov 2020 20:08:15.535 * Running mode=sentinel, port=26379.
77891:X 02 Nov 2020 20:08:15.546 # Sentinel ID is da43e59879c95b6a5c2912856d67e9dafb25f2cb
77891:X 02 Nov 2020 20:08:15.546 # +monitor master mymaster 127.0.0.1 6379 quorum 2
77891:X 02 Nov 2020 20:08:15.546 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
77891:X 02 Nov 2020 20:08:15.547 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
77891:X 02 Nov 2020 20:08:21.408 * +sentinel sentinel 13d587768d58fa7d31a7be3bcec7634ef4e23cf8 127.0.0.1 26380 @ mymaster 127.0.0.1 6379
77891:X 02 Nov 2020 20:08:32.080 * +sentinel sentinel 342c6cdfe07f666fa860c59e56ba14f1fba07348 127.0.0.1 26381 @ mymaster 127.0.0.1 6379

另外兩個監控的日誌可以自己去看,發現啟動多個哨兵例項時,會在日誌後面加上新增新的哨兵。
此時我們就啟動了三個節點,監控6379這個主節點。那麼此時如果6379節點掛機呢?

[root@zhaoshuai redis]# service redis_6379 stop
Stopping ...
Redis stopped
[root@zhaoshuai redis]# ps -ef |grep redis-server
root      77655      1  0 19:33 ?        00:00:01 /usr/local/bin/redis-server 127.0.0.1:6380      
root      77669      1  0 19:33 ?        00:00:01 /usr/local/bin/redis-server 127.0.0.1:6381      
root      77777  77305  0 19:58 pts/1    00:00:00 grep redis-server

可以看到6379節點已經停了,此時檢視6380的日誌:

...
77871:S 02 Nov 2020 20:15:24.295 * Connecting to MASTER 127.0.0.1:6379
77871:S 02 Nov 2020 20:15:24.295 * MASTER <-> REPLICA sync started
77871:S 02 Nov 2020 20:15:24.295 # Error condition on socket for SYNC: Connection refused
77871:S 02 Nov 2020 20:15:25.314 * Connecting to MASTER 127.0.0.1:6379
77871:S 02 Nov 2020 20:15:25.314 * MASTER <-> REPLICA sync started
77871:S 02 Nov 2020 20:15:25.314 # Error condition on socket for SYNC: Connection refused
77871:M 02 Nov 2020 20:15:25.977 # Setting secondary replication ID to 308c17c2979464346c64b7d458292d0ef212b270, valid up to offset: 77458. New replication ID is c4d851bed85f97e717b50cc901068284d7e1c3fe
77871:M 02 Nov 2020 20:15:25.977 * Discarding previously cached master state.
77871:M 02 Nov 2020 20:15:25.977 * MASTER MODE enabled (user request from 'id=5 addr=127.0.0.1:44882 fd=8 name=sentinel-da43e598-cmd age=430 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=140 qbuf-free=32628 obl=36 oll=0 omem=0 events=r cmd=exec')
77871:M 02 Nov 2020 20:15:25.978 # CONFIG REWRITE executed with success.
77871:M 02 Nov 2020 20:15:26.473 * Replica 127.0.0.1:6381 asks for synchronization
77871:M 02 Nov 2020 20:15:26.473 * Partial resynchronization request from 127.0.0.1:6381 accepted. Sending 289 bytes of backlog starting from offset 77458.

可以看到它會嘗試連線6379,然後連幾次後哨兵就講6380給切換成了主節點,6381追隨6380去了。
此時再重啟6379節點:

[root@zhaoshuai redis]# service redis_6379 start
Starting Redis server...

然後檢視6379的日誌:

77982:S 02 Nov 2020 20:20:20.330 * Before turning into a replica, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
77982:S 02 Nov 2020 20:20:20.330 * REPLICAOF 127.0.0.1:6380 enabled (user request from 'id=3 addr=127.0.0.1:37258 fd=8 name=sentinel-342c6cdf-cmd age=10 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=148 qbuf-free=32620 obl=36 oll=0 omem=0 events=r cmd=exec')
77982:S 02 Nov 2020 20:20:20.331 # CONFIG REWRITE executed with success.
77982:S 02 Nov 2020 20:20:20.389 * Connecting to MASTER 127.0.0.1:6380
77982:S 02 Nov 2020 20:20:20.390 * MASTER <-> REPLICA sync started
77982:S 02 Nov 2020 20:20:20.390 * Non blocking connect for SYNC fired the event.
77982:S 02 Nov 2020 20:20:20.390 * Master replied to PING, replication can continue...
77982:S 02 Nov 2020 20:20:20.390 * Trying a partial resynchronization (request 6d075b3d0b0353bda449a4ae89551893eae917a0:1).
77982:S 02 Nov 2020 20:20:20.391 * Full resync from master: c4d851bed85f97e717b50cc901068284d7e1c3fe:135741
77982:S 02 Nov 2020 20:20:20.391 * Discarding previously cached master state.
77982:S 02 Nov 2020 20:20:20.523 * MASTER <-> REPLICA sync: receiving 218 bytes from master
77982:S 02 Nov 2020 20:20:20.523 * MASTER <-> REPLICA sync: Flushing old data
77982:S 02 Nov 2020 20:20:20.523 * MASTER <-> REPLICA sync: Loading DB in memory
77982:S 02 Nov 2020 20:20:20.523 * MASTER <-> REPLICA sync: Finished with success

可以看到重啟後,6379也是追隨6380,6380為主。
至此使用哨兵機制完整的實現了。然後我們在回過頭開啟26379.conf檔案:

port 26379
daemonize yes
logfile "/var/log/sentinel-26379.log"
sentinel myid da43e59879c95b6a5c2912856d67e9dafb25f2cb
# Generated by CONFIG REWRITE
dir "/etc/redis"
protected-mode no
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6380 2
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
sentinel known-replica mymaster 127.0.0.1 6379
sentinel known-replica mymaster 127.0.0.1 6381
sentinel known-replica mymaster 192.168.226.138 6379
sentinel known-sentinel mymaster 127.0.0.1 26380 13d587768d58fa7d31a7be3bcec7634ef4e23cf8
sentinel known-sentinel mymaster 127.0.0.1 26381 342c6cdfe07f666fa860c59e56ba14f1fba07348
sentinel current-epoch 1

注意,我們最開始配置,寫的是監控主節點6379,但是經過一次重新選舉後,6380成為了主節點master,因此哨兵自動修改了配置檔案中主節點的資訊從6379變成了6380。
至此完成了redis的哨兵配置。

redis的哨兵是如何發現其他哨兵的?

我們在配置哨兵的時候,只配置了主節點的資訊,但是當主節點宕機時需要其他的哨兵發起投票選出新的master,那麼一個哨兵是如何知道其他的哨兵的?
redis自帶的功能就是釋出訂閱,當一個主節點啟動的時候,哨兵就會在主節點身上進行釋出訂閱。
使用 redis-cli -p 6380連線redis的master。
然後使用psubscribe檢視哨兵之間通訊的訊息:

[root@zhaoshuai ~]# redis-cli -p 6380
127.0.0.1:6380> PSUBSCRIBE *
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "*"
3) (integer) 1
1) "pmessage"
2) "*"
3) "__sentinel__:hello"
4) "127.0.0.1,26380,13d587768d58fa7d31a7be3bcec7634ef4e23cf8,1,mymaster,127.0.0.1,6380,1"
1) "pmessage"
2) "*"
3) "__sentinel__:hello"
4) "127.0.0.1,26381,342c6cdfe07f666fa860c59e56ba14f1fba07348,1,mymaster,127.0.0.1,6380,1"
1) "pmessage"
2) "*"
3) "__sentinel__:hello"
4) "127.0.0.1,26379,da43e59879c95b6a5c2912856d67e9dafb25f2cb,1,mymaster,127.0.0.1,6380,1"
1) "pmessage"
2) "*"
3) "__sentinel__:hello"
4) "127.0.0.1,26380,13d587768d58fa7d31a7be3bcec7634ef4e23cf8,1,mymaster,127.0.0.1,6380,1"
....

可以看到有一個 __sentinel__:hello的通道,然後26379,26380,26381都在這裡面說話,所以只要有一個哨兵連線上主節點,那麼其他的節點就能發現。
哨兵的配置檔案在redis的原始碼中有sentinel.conf。
當哨兵通過master節點發現其他節點後,會在本地配置檔案記錄其他哨兵節點,然後哨兵之間除了通過master通訊,也會有自己的釋出訂閱。

[root@zhaoshuai redis-5.0.5]# redis-cli -p 26379
127.0.0.1:26379> PSUBSCRIBE *
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "*"
3) (integer) 1
1) "pmessage"
2) "*"
3) "+sentinel-address-switch"
4) "master mymaster 127.0.0.1 6380 ip 192.168.226.138 port 26381 for 342c6cdfe07f666fa860c59e56ba14f1fba07348"
1) "pmessage"
2) "*"
...

哨兵之間通訊的通道為 +sentinel-address-switch,檢視此通道的資訊:

127.0.0.1:26379> SUBSCRIBE +sentinel-address-switch
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "+sentinel-address-switch"
3) (integer) 1
1) "message"
2) "+sentinel-address-switch"
3) "master mymaster 127.0.0.1 6380 ip 192.168.226.138 port 26380 for 13d587768d58fa7d31a7be3bcec7634ef4e23cf8"
1) "message"
2) "+sentinel-address-switch"
3) "master mymaster 127.0.0.1 6380 ip 127.0.0.1 port 26380 for 13d587768d58fa7d31a7be3bcec7634ef4e23cf8"
1) "message"
2) "+sentinel-address-switch"
3) "master mymaster 127.0.0.1 6380 ip 192.168.226.138 port 26380 for 13d587768d58fa7d31a7be3bcec7634ef4e23cf8"
1) "message"
2) "+sentinel-address-switch"
3) "master mymaster 127.0.0.1 6380 ip 127.0.0.1 port 26380 for 13d587768d58fa7d31a7be3bcec7634ef4e23cf8"
1) "message"
2) "+sentinel-address-switch"
3) "master mymaster 127.0.0.1 6380 ip 192.168.226.138 port 26381 for 342c6cdfe07f666fa860c59e56ba14f1fba07348"
1) "message"
2) "+sentinel-address-switch"
3) "master mymaster 127.0.0.1 6380 ip 127.0.0.1 port 26381 for 342c6cdfe07f666fa860c59e56ba14f1fba07348"
1) "message"

可以看到這個channel中訂閱另外連個節點發布的資訊。

redis叢集分片

上面解決了叢集的主從複製以及高可用哨兵處理。也就是說根據AKF拆分原則,我們只實現了X軸的拓展。主從節點中的資料都是一樣的,都是全量資料,那麼容量的問題還沒解決。
回顧akf拆分原則,解決容量問題,可以向Y軸拓展,也就是根據業務進行資料分類,當某一型別資料量過大時,則向Z軸拓展,指定規則來對不同的資料指定存放節點。
根據上面這種拆分原則的話,redis解決容量過大有如下方案:

  • 型別分類: 也就是向Y軸拓展,這樣的話,就要求客戶端對資料進行分類,比如說按照訂單,支付等資料進行分類,然後將不同分類的資料存放到不同的redis服務。
    也就是說,客戶端需要對業務進行拆分,根據不同的業務選擇不同的redis。

  • 根據規則分類: 也就是Z軸拓展,指定某一個規則來確定資料存放的位置。當某一業務的資料太大的時候,已經無法按照業務進行更詳細的拆分了,只能向Z軸拓展,也就是說
    指定一個演算法,然後將資料按照這個演算法均勻的分佈到多個不同的節點。這個演算法的實現有以下三種方案:

    • modula(hash+取模): 也就是說對要存入的key先取hash值,然後在通過對redis節點的數量取模,確定要將資料存放到哪兒一個節點。但是這種方式的弊端就是無法進行分散式拓展。
      因為如果本來是兩臺節點,後期資料量變大了,變成三個,那麼假設有一個key取hash後的值是11,取模後原來是存放在1節點上,然後現在拓展成3臺,在對11取模就變成了2,
      也就是說資料取不出來了,因此這種方案的話,節點的數量就不能改變了。無法拓展。
    • random(lpush): random的意思是隨機,也就是說我隨機的往redis中存,隨機就會有一個問題,客戶端壓根不知道存到哪兒臺機器了,取得時候也不知道該去哪兒
      取,因此隨機這種方案一般是存list,使用lpush,在每臺伺服器都會有一個一樣的key,然後值的型別是list,通過lpush往裡面存東西,另一個客戶端不停的從這些list
      中取資料消費,不需要知道是從哪兒臺機器取出來的,只要有就取,然後消費他。為了解決有可能消費失敗的問題,還可以加一個緩衝。
    • kemata(一致性hash演算法):為每一個redis節點起一個id或者使用ip地址,每次計算時,key和node都要參與運算。一般會將它抽象成一個環形。
      我們上面講了hash取模,這裡是一致性hash。兩個都是hash,那麼hash有什麼特點?對映,也就是說無論你給我什麼字串,我最終都會給你對映成一個
      等長的數字。上面我們說了,一般使用環形來表示這個演算法,就想象它是一個環,這個環上有很多的點,每一個點都是一個數字,這些點都是虛擬的。最終根據ip地址或
      node名/id等肯定能根據node資訊經過hash運算在這個環上找到一個點,這個點就是物理的點。這個物理節點就表示一個redis服務節點,那麼將key和node資訊一起經過hash計算
      後,如果這個值距離這個物理節點更近,那麼就講這個值存入這個物理節點。
      這樣的方案的缺點是:新增節點會有一小部分資料不能命中。
      想象以下,本來兩個物理節點,一個佔圓的一半,那麼node1占上半圓,node2佔下半圓,然後現在新增一個節點,這樣的話就是三個節點了,那麼hash計算後發現離第三個
      節點較近,就去node3取資料,但是這個資料本來是存在node2上的,就會取不出來。
      解決方案有兩種:
    • 第一種:快取擊穿。因為你本來能找到資料的,現在加了個節點找不到了,那隻能將請求壓到mysql,然後將mysql的資料重新在新的節點中存一份,這樣以後就可以從redis中
      查到了。時間複雜度仍是O(1)。
    • 第二種:會增大時間複雜度。每次查的時候,會查命中的節點和節點兩邊的節點,這三個節點的資料挨個查一遍。那麼如果一下增加兩個節點的話,還是查不到。

    上面兩種方式各有優缺點,只能人去選擇怎麼取捨。上面的方案還會帶來一個新的問題,就是新增節點後,資料會存入新的節點,但是在原來節點中資料仍然存在,佔著空間。
    所以這些資料必須清理,既然資料要清理,因此他只能用來作為快取,因為資料庫是不允許資料丟失的。資料清理策略有LRU,LFU等。

上面的方案都是在客戶端計算的。方案只能對快取使用。

客戶端的問題解決了,但是作為客戶端,肯定要與服務端進行連線,因為資料分片,資料有可能在node1中,也有可能在node2中,這時,每一個客戶端既要與node1連線,又要與node2連線。
每一個連線都是十分損耗效能的。這個問題怎麼解決?
加入代理。代理伺服器不幹活,不存東西,不參與運算,只是建立連線。
如果代理伺服器的壓力比較大,還可以做叢集,keepalived+LVS
用的比較多的代理服務:twemproxy

Twemproxy代理

雜項補充:

對上面的內容中,沒有牽扯到的內容進行一個補充。

redis為什麼快?

redis的速度非常快,單機的redis就可以支撐每秒十幾萬的併發,是mysql效能的幾十倍。為什麼這麼快?

  • redis是基於記憶體的,記憶體的i/o速度快。
  • redis基於C語言開發,對資料結構做了優化。基於幾種基礎的資料結構,redis做了大量的優化,效能極高。
  • redis是單執行緒的,沒有上下文切換的開銷
  • 基於非阻塞IO的多路複用機制。

什麼是熱key,怎麼解決?

熱key問題就是,突然有幾十萬併發同時請求某個key,就會造成流量過於集中,達到物理網絡卡上限,導致redis伺服器宕機引發雪崩。
解決辦法:

  1. 提前把熱key打散到不同的伺服器,降低壓力
  2. 加入二級快取,提前把熱key資料載入到本地記憶體中,當redis宕機時,走本地記憶體。

redis為什麼變慢了?

  • 使用過於複雜的命令
  • 儲存大key
  • 資料集中過期
  • 例項記憶體達到上限
  • fork耗時嚴重
  • 繫結cpu
  • aof分配不合理
  • 使用swap