Redis一篇從入門到實戰
Redis 入門
1、NoSQL概述
什麼是NoSQL,NoSQL = Not Only SQL(不僅僅是 SQL)
NoSQL 的特點:
-
方便擴充套件(資料之間沒有關係,很好擴充套件)
-
大資料量高效能(Redis一秒寫8萬次,讀取 11 萬次,NoSQL的快取記錄級,是一種細粒度的快取,效能會比較高)
-
資料型別是多樣型的(不需要事先設計資料庫,隨取隨用)
傳統 RDBMS 和 NoSQL的比較:
傳統的 RDBS(關係型資料庫) | NoSQL(非關係型資料庫) |
---|---|
結構化組織 | 不僅僅是資料 |
SQL | 沒有固定的查詢語言 |
資料和關係都存在單獨的表中 | 鍵值對儲存,列儲存,文件儲存,圖形資料庫 |
操作語言,資料庫定義語言 | 最終一致性 |
嚴格的一致性 | CAP 定理和 BASE (異地多活) |
基礎的事務 | 高效能,高可用,高可擴充套件 |
.... | .... |
瞭解:3V+3高
- 大資料時代的3V:主要是描述問題的:海量Volume、多樣Variety、實時Velocity
- 大資料時代的3高:主要是對程式的要求:高併發、高可擴、高效能
- 實際專案:NoSQL+RDBMS 配合使用
NoSQL 的四大分類
1、鍵值對(key-value):Redis
2、文件型資料庫(bson 和 json 一樣):
- MongoDB:
- MongoDB 是一個基於分散式檔案儲存的資料庫,使用 C++編寫,主要用來處理大量的文件。
- MongoDB 是一個介於關係型資料庫和非關係型資料中間的產品!MongoDB 是非關係型資料中功能最豐富,最像關係型資料庫的。
- ConthDB
3、列儲存資料庫:
- HBase
- 分散式檔案系統
4、圖關係資料庫:
- 他不是存圖形,放的是關係,比如:朋友圈社交網路,廣告推薦
- Neo4j、InfoGrid
2、Redis 概述
Redis (Remote Dictionary Server):即遠端字典服務
是一個開源的使用 ANSI C語言編寫、支援網路、可基於記憶體亦可持久刷的日誌型、key-value 資料庫,並提供多種語言的 API。免費和開源!是當下最熱門的NoSQL 技術之一,也被人們稱之為機構化資料庫。
Redis 的作用:
- 記憶體儲存、持久化(RDB 和 AOF)
- 效率高,可以用於快取記憶體
- 釋出訂閱系統
- 地圖資訊分析
- 計數器、計時器(比如瀏覽量)
Redis 的特徵:
- 多樣化的資料型別
- 持久化
- 叢集
- 事務
官網:
- Redis 官網: https://redis.io/
- Redis 中文網: http://www.redis.cn/
3、Windows安裝
1、下載安裝包:https://github.com/tporadowski/redis/releases
2、下載完畢得到壓縮包解壓:Redis-x64-5.0.9.zip
3、開啟Redis:雙擊redis-server.exe
執行服務即可!
[24304] 29 Aug 11:32:40.572 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
[24304] 29 Aug 11:32:40.572 # Redis version=5.0.9, bits=64, commit=9414ab9b, modified=0, pid=24304, just started
[24304] 29 Aug 11:32:40.575 # Warning: no config file specified, using the default config. In order to specify a config file use d:\environment\redis\redis-x64-5.0.9\redis-server.exe /path/to/redis.conf
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.9 (9414ab9b/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 24304
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[24304] 29 Aug 11:32:40.581 # Server initialized
[24304] 29 Aug 11:32:40.581 * Ready to accept connections
4、使用Redis客戶端連線:點選redis-cli.exe
,輸入ping,出現如圖PONG結果即連線成功
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>
4、Linux安裝
1、下載安裝包並將壓縮包上傳到伺服器:redis 6.0.6.tar.gz
2、解壓Redis安裝包:自己的程式一般放在/opt
目錄下
# 移動到/opt目錄下
mv redis-6.0.6.tar.gz /opt
# 解壓Redis壓縮包
tar -zxvf /opt/redis-6.0.6.tar.gz
###### 或者執行下面命令 #######
tar -zxvf redis-6.0.6.tar.gz -C /opt/
3、進入Redis目錄:檢視解壓後的檔案,可以看到Redis的配置檔案redis.conf
[root@liusx redis-6.0.6]# ls
00-RELEASENOTES COPYING Makefile redis.conf runtest-moduleapi src utils
BUGS deps MANIFESTO runtest runtest-sentinel tests
CONTRIBUTING INSTALL README.md runtest-cluster sentinel.conf TLS.md
4、基本的環境安裝
# 增加環境支援
[root@liusx redis-6.0.6]# yum install gcc-c++
# 安裝
[root@liusx redis-6.0.6]# make
[root@liusx redis-6.0.6]# make install
5、Redis預設安裝路徑為:/usr/local/bin
[root@liusx bin]# ll
-rwxr-xr-x 1 root root 4739840 Aug 29 12:15 redis-benchmark
-rwxr-xr-x 1 root root 9653568 Aug 29 12:15 redis-check-aof
-rwxr-xr-x 1 root root 9653568 Aug 29 12:15 redis-check-rdb
-rwxr-xr-x 1 root root 5059032 Aug 29 12:15 redis-cli
lrwxrwxrwx 1 root root 12 Aug 29 12:15 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 9653568 Aug 29 12:15 redis-server
6、進入到該目錄然後將Redis的配置檔案拷貝到此位置,首先新建一個放配置檔案的目錄,然後拷貝至該目錄下
[root@liusx bin]# cd /usr/local/bin
[root@liusx bin]# mkdir rconfig
[root@liusx bin]# cp /opt/redis-6.0.6/redis.conf rconfig
[root@liusx bin]# cd rconfig/
[root@liusx rconfig]# ls
redis.conf
7、修改Redis配置檔案後臺啟動:
[root@liusx rconfig]# vim /usr/local/bin/rconfig/redis.conf
修改配置項:
daemonize
為yes
。- 註釋掉
bind 127.0.0.1
- 修改
protected-mode no
- 關閉protected-mode保護模式,此時外部網路可以直接訪問
- 開啟protected-mode保護模式,需配置bind ip或者設定訪問密碼
No. | 配置項 | 描述 |
---|---|---|
1 | port 6379 | 配置Redis執行埠 |
2 | daemonize yes | 配置Redis是否為後臺執行 |
3 | pidfile /usr/data/redis/run/redis_6379.pid | 設定程序儲存路徑(父目錄必須存在) |
4 | logfile "/usr/data/redis/logs/redis.log" | 設定日誌儲存目錄(父目錄必須存在) |
5 | databases16 | 該Redis支援的資料庫個數(0~15) |
6 | dir /usr/data/redis/dbcache | 儲存快取資料檔案目錄 |
8、啟動Redis服務:在/usr/local/bin
目錄下,輸入
[root@liusx bin]# redis-server rconfig/redis.conf
17909:C 29 Aug 2020 13:37:00.822 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
17909:C 29 Aug 2020 13:37:00.822 # Redis version=6.0.6, bits=64, commit=00000000, modified=0, pid=17909, just started
17909:C 29 Aug 2020 13:37:00.822 # Configuration loaded
9、使用Redis客戶端連線測試
[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set name test
OK
127.0.0.1:6379> get name
"test"
127.0.0.1:6379> keys *
1) "name"
10、檢視 Redis 程序:再開一個視窗
[root@liusx ~]# ps -ef|grep redis
root 8179 1 0 08:43 ? 00:00:00 redis-server 127.0.0.1:6379
root 8210 3437 0 08:44 pts/1 00:00:00 redis-cli -p 6379
root 8306 8281 0 08:48 pts/2 00:00:00 grep --color=auto redis
11、關閉和退出Redis服務:
127.0.0.1:6379> shutdown # 關閉Redis
not connected> exit # 退出
[root@liusx bin]# ps -ef|grep redis
root 8330 3437 0 08:49 pts/1 00:00:00 grep --color=auto redis
[root@liusx bin]#
後面我們會使用單機多Redis啟動叢集測試!
備註:
啟動Redis服務程序: | /usr/local/bin/redis-server /usr/local/bin/rconf/redis.conf |
---|---|
查詢Redis程序是否啟動,進行埠檢視: | netstat -nptl |
連線本機Redis資料庫: | /usr/local/bin/redis-cli |
連線遠端Redis資料庫: | /usr/local/bin/redis-cli-h 127.0.0.1 -p 6379 |
幹掉所有的Redis服務 | killall redis-server |
Redis效能測試工具: | /usr/local/bin/redis-benchmark -n 10000-d 50 -c 2000 |
5、Redis 效能測試
redis-benchmark 是官網自帶的壓力測試工具。
序號 | 選項 | 描述 | 預設值 |
---|---|---|---|
1 | -h | 指定伺服器主機名 | 127.0.0.1 |
2 | -p | 指定伺服器埠 | 6379 |
3 | -s | 指定伺服器 socket | |
4 | -c | 指定併發連線數 | 50 |
5 | -n | 指定請求數 | 10000 |
6 | -d | 以位元組的形式指定 SET/GET 值的資料大小 | 2 |
7 | -k | 1=keep alive 0=reconnect | 1 |
8 | -r | SET/GET/INCR 使用隨機 key, SADD 使用隨機值 | |
9 | -P | 通過管道傳輸 請求 | 1 |
10 | -q | 強制退出 redis。僅顯示 query/sec 值 | |
11 | --csv | 以 CSV 格式輸出 | |
12 | -l | 生成迴圈,永久執行測試 | |
13 | -t | 僅執行以逗號分隔的測試命令列表。 | |
14 | -I | Idle 模式。僅開啟 N 個 idle 連線並等待。 |
使用方式:
# 測試 100 個併發連線,100000 個請求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
分析舉例:
====== SET ======
100000 requests completed in 1.97 seconds # 對 10萬個請求進行寫入測試
100 parallel clients # 100 個併發客戶端
3 bytes payload # 每次寫入 3 個位元組
keep alive: 1 # 只有一臺伺服器來處理這些請求,即單機效能
19.56% <= 1 milliseconds
98.70% <= 2 milliseconds
99.82% <= 3 milliseconds
99.85% <= 9 milliseconds
99.94% <= 10 milliseconds
99.95% <= 126 milliseconds
99.96% <= 127 milliseconds
100.00% <= 127 milliseconds # 所有請求在127 毫秒處理完成
50761.42 requests per second # 每秒處理 50761.42 個請求
6、基礎知識
Redis 預設有 16 個數據庫,這個可以在配置檔案中檢視。
[root@liusx bin]# vim rconfig/redis.conf
預設使用的是第 0 個。
使用 select
命令切換資料庫,使用 dbsize
檢視資料庫大小:
[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> select 3 # 切換資料庫
OK
127.0.0.1:6379[3]> dbsize # 檢視資料庫大小
(integer) 0
127.0.0.1:6379[3]> set name liusx
OK
127.0.0.1:6379[3]> dbsize
(integer) 1
127.0.0.1:6379[3]>
檢視資料庫中所有的key
:keys *
清除當前資料庫:flushdb
清除所有資料庫:flushall
127.0.0.1:6379[3]> keys *
1) "name"
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty list or set)
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
7、Redis單執行緒快(面試)
Redis 是很快的,官方表示,Redis 是基於記憶體操作,CPU 不是 Redis 的效能瓶頸,Redis 的效能瓶頸是機器的記憶體和網路頻寬。既然可以單執行緒來實現,就使用單執行緒。
Redis 是 C 語言寫的,官方提供的資料為 100000+ 的 QPS ,完全不比同樣是使用 key-value 的Memecache 差。
Redis 為什麼單執行緒還這麼快呢?
1、誤區1:高效能的伺服器一定是多線性的?
2、誤區2:多執行緒一定比單執行緒的效率高?
要了解在執行:效率上 CPU > 記憶體 > 硬碟
核心:Redis 是將所有的資料全部放在記憶體中的,所有說使用單執行緒去操作執行效率就是最高的,多執行緒在執行過程中需要進行 CPU 的上下文切換,這個是耗時操作。對於記憶體系統來說,如果沒有上下文切換效率就是最高的,多次讀寫都是在一個 CPU 上的,在記憶體情況下,這個就是最佳方案。
redis 採用網路IO多路複用技術來保證在多連線的時候, 系統的高吞吐量。 參考文章
Redis 8種資料型別
1、Redis 五大資料型別
官網可檢視命令: http://www.redis.cn/commands.html
0、Redis-keys
- 查詢所有的 key:keys *
- 判斷key 是否存在:exists name
- 設定key的過期時間,單位是秒:expire name 10
- 檢視當前key的剩餘過期時間:ttl name
- 檢視當前key的型別:type age
127.0.0.1:6379> keys * # 查詢所有的key
(empty list or set)
127.0.0.1:6379> set name xxx
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> set age 1
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> exists name # 判斷key 是否存在
(integer) 1
127.0.0.1:6379> exists name1
(integer) 0
127.0.0.1:6379> move name 1
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name yyy
OK
127.0.0.1:6379> expire name 10 # 設定key的過期時間,單位是秒
(integer) 1
127.0.0.1:6379> ttl name # 檢視當前key的剩餘過期時間
(integer) 7
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> type age # 檢視當前key的型別
string
127.0.0.1:6379>
Redis 有以下 5 種基本的資料型別
1、String(字串)
127.0.0.1:6379> set key1 v1 # 設定值
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> append key1 "hello" # 追加值,如果不存在,相當於 set key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1 # 獲取字串長度
(integer) 7
127.0.0.1:6379>
自增、自減
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 自增 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> decr views # 自減 1
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> incrby views 10 # 設定步長、自增 10
(integer) 9
127.0.0.1:6379> decrby views 5 # 設定步長、自減 5
(integer) 4
字串範圍
127.0.0.1:6379> set key1 "hello,world!"
OK
127.0.0.1:6379> get key1
"hello,world!"
127.0.0.1:6379> getrange key1 0 3 # 擷取字串[0, 3]
"hell"
127.0.0.1:6379> getrange key1 0 -1 # 獲取全部的字串,和 get key一樣
"hello,world!"
127.0.0.1:6379>
替換字串範圍
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> setrange key2 1 xx # 替換字元,從下標1開始替換xx 2個字元
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"
127.0.0.1:6379>
setnx(set if not exist)
:不存在再設定(在分散式鎖中會經常使用)
setex(set with expire)
:設定過期時間
ttl key
:檢視當前剩餘有效時間
- 如果該內容已經消失則返回 -2,
- 如果沒有消失,則返回剩餘的時間
persist key
:讓該key取消有效時間(在有效期內),這個時候如果使用 ttl 命令檢視則返回的結果是“ -1 ”;
127.0.0.1:6379> setex key3 30 "hello" # 設定 30 秒後過期
OK
127.0.0.1:6379> ttl key3 # 剩餘過期時間
(integer) 25
127.0.0.1:6379> setnx mykey "redis" # mykey 不存在時設定成功
(integer) 1
127.0.0.1:6379> keys *
1) "key2"
2) "key1"
3) "views"
4) "mykey"
127.0.0.1:6379> setnx mykey "mongoDB" # mykey 存在時設定失敗
(integer) 0
127.0.0.1:6379> get mykey # mykey 值不變
"redis"
127.0.0.1:6379>
mset
和 mget
:設定多個key。
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同時設定多個值
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3 # 同時獲取多個值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx 是一個原子性的操作,要麼一起成功,要麼都失敗
(integer) 0
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379>
物件
# 設定一個 user:1 物件 值為 json 字元來儲存一個物件
set user:1 {name:zhangsan, age:3}
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
127.0.0.1:6379>
getset
:先 get 再 set
127.0.0.1:6379> getset db redis # 如果不存在值,則返回 nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb # 如果存在值,獲取原來的值,並設定新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
127.0.0.1:6379>
String 的使用場景:value 除了是字串以外還可以是數字
- 計數器
- 統計多單位的數量
- 粉絲數
- 物件快取儲存
名稱 | 命令 | 範例: | 程式結果: |
---|---|---|---|
設定操作資料: | set key value | set msg lee-hello | OK |
查詢資料: | get key | get msg | "lee-hello" |
不覆蓋設定: | setnx key value | setnx msg lee-hello | (integer)0--false (integer)1--true |
設定資料有效期: | setex key Time value | **setex code 10 adc ** setpx code 10 abc |
OK |
設定多個key: | mset key1 value1 key2 value2 … | mset k1xiaol k2 xiaoz | OK |
不覆蓋設定多個key: | msetnx key1 value1 key2 value2 … | msetnx k1 xiaol k2 xiaoz | (integer)0--false (integer)1--true |
追加內容: | append key 追加內容 | append msg -appendMsg | (integer)19--字元長度 |
取得資料長度: | strlen key | strlen | (integer)19--字元長度 |
刪除指定資料: | del key key key | del name1 name2 | (integer)2 |
查詢所有key | keys * | keys * 或 keys info-* | 1)msg 2)code |
清除當前庫全資料 | flushdb | flushdb | OK |
清除所有庫全資料 | flushall | flushall | OK |
切換索引資料庫 | select 0-16 | select 0 或 select 1 | OK |
數字操作:
設定一個基本型別 | set mldn-pid 100 | OK |
自增1處理,隨後會返回當前增長後的資料內容 | incr mldn-pid | (integer)101 |
預設增長為1,現在要求增長為200: | incrby mldn-pid 200 | (integer)301 |
自減1處理 | decr mldn-pid | (integer)300 |
自減指定的資料: | decrby mldn-pid 100 | (integer)200 |
設定一個Hash型別 | hset mldn pid 100 | (integer)1 |
進行Hash資料型別的數字操作(Hash制有hincrby一種操作) | hincrby mldn pid -1 | (integer)99 |
2、Hash(雜湊)
也是 key - value 形式的,但是value 是一個map。
127.0.0.1:6379> hset myhash field xxx # set 一個 key-value
(integer) 1
127.0.0.1:6379> hget myhash field # 獲取一個欄位值
"xxx"
127.0.0.1:6379> hmset myhash field1 hello field2 world # set 多個 key-value
OK
127.0.0.1:6379> hmget myhash field field1 field2 # 獲取多個欄位值
1) "xxx"
2) "hello"
3) "world"
127.0.0.1:6379> hgetall myhash # 獲取全部的資料
1) "field"
2) "xxx"
3) "field1"
4) "hello"
5) "field2"
6) "world"
刪除key
:
127.0.0.1:6379> hdel myhash field1 # 刪除指定的key,對應的value也就沒有了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field"
2) "xxx"
3) "field2"
4) "world"
127.0.0.1:6379>
獲取value,map相關資訊
127.0.0.1:6379> hlen myhash # 獲取長度
(integer) 2
127.0.0.1:6379> hexists myhash field1 # 判斷指定key是否存在
(integer) 0
127.0.0.1:6379> hexists myhash field2
(integer) 1
127.0.0.1:6379> hkeys myhash # 獲取所有的key
1) "field"
2) "field2"
127.0.0.1:6379> hvals myhash # 獲取所有的value
1) "xxx"
2) "world"
127.0.0.1:6379>
hash數字自增操作
127.0.0.1:6379> hset myhash field3 5
(integer) 1
127.0.0.1:6379> hincrby myhash field3 1 # 指定增量
(integer) 6
127.0.0.1:6379> hincrby myhash field3 -1
(integer) 5
127.0.0.1:6379> hsetnx myhash field4 hello # 如果不存在則可以設定
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 world # 如果存在則不能設定
(integer) 0
127.0.0.1:6379>
Hash 適合儲存經常變動的物件資訊,String 更適合於儲存字串。
名稱 | 命令 | 範例: | 程式結果: |
---|---|---|---|
存放hash資料: | hset 物件key 屬性key 內容 | hset cat name bc | OK |
取得hash資料: | hget 物件key key | hget cat name | "bc" |
不覆蓋設定: | hsetnx 物件key key value | hsetnx cat name bc | (integer)0--false****(integer)1--true |
批量設定: | hmset 物件key key1 value1 key2 value2 … | hmset cat name bc age 18 | OK |
獲取物件的多個key的值: | hmget 物件key key1 key2 | hmget cat name age | 1)"bc"****2)"18" |
判斷某個資料是否存在: | hexists 物件key key | hexists cat name | (integer)1 |
取得全部內容數量: | hlen 物件key | hlen cat | (integer)2 |
刪除指定key資訊: | hdel 物件key key1 kye2 | hdel cat name age | (integer)2 |
取得所有key: | hkeys 物件key | hkeys cat | 1)"name"****2)"age" |
取得hash中所有內容: | hvals 物件key | hvals cat | 1)"bc"****2)"18" |
獲得全部的key與vaue: | hgetall 物件key | hgetall cat | 1)"name"2)"bc"3)"age"****4)"18" |
3、List(列表)
基本的資料型別,列表。
在 Redis 中可以把 list 用作棧、佇列、阻塞佇列。
list 命令多數以 l
開頭。
127.0.0.1:6379> lpush list one # 將一個值或者多個值,插入到列表的頭部(左)
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 檢視全部元素
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 # 通過區間獲取值
1) "three"
2) "two"
127.0.0.1:6379> rpush list right # 將一個值或者多個值,插入到列表的尾部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379>
彈出 pop
127.0.0.1:6379> lrange list 0 -1
1) "!"
2) "world"
3) "world"
4) "hello"
127.0.0.1:6379> lpop list # 移除list的第一個元素
"!"
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "world"
3) "hello"
127.0.0.1:6379> rpop list # 移除list的第一個元素
"hello"
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "world"
127.0.0.1:6379>
索引 Lindex
127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "world"
3) "world"
127.0.0.1:6379> lindex list 1 # 通過下標獲取list中的某一個值
"world"
127.0.0.1:6379> lindex list 0
"hjk"
127.0.0.1:6379>
Llen 長度:
127.0.0.1:6379> llen list
(integer) 3
127.0.0.1:6379>
移除指定的值:
127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "world"
3) "world"
127.0.0.1:6379> lrem list 1 world # 移除list集合中指定個數的value,精確匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "world"
127.0.0.1:6379> lpush list hjk
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hjk"
2) "hjk"
3) "world"
127.0.0.1:6379> lrem list 2 hjk
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "world"
127.0.0.1:6379>
trim 截斷:
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> ltrim mylist 1 2 # 通過下標擷取指定長度,這個list已經被破壞了,截斷之後只剩下截斷後的元素
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello3"
127.0.0.1:6379>
rpoplpush :移除列表的最後一個元素,將他移動到新的列表中。
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
127.0.0.1:6379> rpoplpush mylist myotherlist # 移除列表的最後一個元素,將他移動到新的列表中。
"hello3"
127.0.0.1:6379> lrange mylist 0 -1 # 檢視原來的列表
1) "hello1"
2) "hello2"
127.0.0.1:6379> lrange myotherlist 0 -1 # 檢視目標列表中,確實存在該值
1) "hello3"
127.0.0.1:6379>
lset:將列表中指定下標的值替換為另一個值,更新操作:
127.0.0.1:6379> exists list # 判斷這個列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果不存在的話,更新會報錯
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item # 如果存在,更新當前下標的值
OK
127.0.0.1:6379> lset list 1 other # 如果不存在的話,更新會報錯
(error) ERR index out of range
127.0.0.1:6379>
linsert:將某個具體的value插入到列表中某個元素的前面或者後面:
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
127.0.0.1:6379> linsert mylist before "hello2" hello
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello"
3) "hello2"
127.0.0.1:6379> linsert mylist after "hello2" hello
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello"
3) "hello2"
4) "hello"
127.0.0.1:6379>
小結:
- list 實際上是一個連結串列,前後都可以插入
- 如果key不存在,建立新的連結串列
- 如果移除了所有的值,空連結串列,也代表不存在
- 在兩邊插入或者改動值,效率最高。
名稱 | 命令 | 案例 | 結果 |
---|---|---|---|
向佇列左邊存放資料(頭部): | lpush 集合key 內容 ... | lpush mldn-list a b | (integer)2 |
向佇列右邊存在資料(尾部): | rpush 集合key 內容 ... | rpush mldn-list 1 2 | (integer)4 |
取得指定索引位置的內容: | lrange 集合key start stop | lrange mldn-list 0 -1****(start =0,stop=-1為全部資料) | 1)”b“2)”a“3)"1“****4)"2" |
在指定元素前追加內容: | linsert 集合key before 內容1 增加內容 | linsert mldn-list before 1 0 | (integer)5 |
在指定元素後追加內容: | linsert 集合key after 內容1 增加內容 | linsert mldn-list after 1 0 | (integer)6 |
修改指定索引的內容: | lset 集合key 索引 內容 | lset mldn-list 0 bb | OK |
刪除資料(從左邊刪起): | lrem 集合key 重複個數 刪除內容 | lrem mldn-list 2 0 | (integer)2 |
保留指定key的值範圍內的資料: | ltrim 集合Key 開始索引 結束索引 | trim mldn-list 1 3 | OK |
從指定集合的頭部刪除元素,並返回刪除元素: | lpop 集合key | lpop mldn-list | b |
從指定集合的尾部刪除元素,並返回刪除元素: | rpop 集合key | rpop mldn-list | 2 |
將移除的元素新增到指定的集合中,返回參照rpop: | rpoplpush 移除集合Key 接收集合Key | rpoplpush mldn-list new-list | 2 |
取得元素指定索引的內容: | lindex 集合key 索引 | lindex mldn-list 0 | b |
返回集合中的元素個數: | llen 集合Key | llen mldn-list | (integer)4 |
4、Set (集合)
127.0.0.1:6379> sadd myset "hello" # set 集合中新增元素
(integer) 1
127.0.0.1:6379> sadd myset "world" # set 集合中新增元素
(integer) 1
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "world"
2) "hello"
127.0.0.1:6379> sismember myset hello # 判斷某一個值是不是在set中
(integer) 1
127.0.0.1:6379> sismember myset hello1 # 判斷某一個值是不是在set中
(integer) 0
127.0.0.1:6379>
127.0.0.1:6379> scard myset # 獲取集合中的個數
(integer) 2
127.0.0.1:6379> sadd myset "hello2" # set 集合中新增元素
(integer) 1
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "world"
2) "hello2"
3) "hello"
127.0.0.1:6379> srem myset hello # 移除元素
(integer) 1
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "world"
2) "hello2"
127.0.0.1:6379>
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "kkk"
2) "world"
3) "hjk"
4) "hello2"
127.0.0.1:6379> srandmember myset # 隨機抽取一個元素
"hjk"
127.0.0.1:6379> srandmember myset # 隨機抽取一個元素
"hello2"
127.0.0.1:6379> srandmember myset 2 # 隨機抽取指定個數的元素
1) "world"
2) "hello2"
127.0.0.1:6379> srandmember myset 2 # 隨機抽取指定個數的元素
1) "hello2"
2) "hjk"
127.0.0.1:6379>
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "kkk"
2) "world"
3) "hjk"
4) "hello2"
127.0.0.1:6379> spop myset # 隨機刪除元素
"hjk"
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "kkk"
2) "world"
3) "hello2"
127.0.0.1:6379> spop myset # 隨機刪除元素
"hello2"
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "kkk"
2) "world"
127.0.0.1:6379>
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "kkk"
2) "world"
127.0.0.1:6379> sadd myset2 set2
(integer) 1
127.0.0.1:6379> smove myset myset2 "kkk" # 將一個特定的值,移動到另一個set集合中
(integer) 1
127.0.0.1:6379> smembers myset # 檢視指定Set的所有值
1) "world"
127.0.0.1:6379> smembers myset2 # 檢視指定Set的所有值
1) "kkk"
2) "set2"
127.0.0.1:6379>
127.0.0.1:6379> smembers key1 # 檢視key1的所有值
1) "b"
2) "a"
3) "c"
127.0.0.1:6379> smembers key2 # 檢視key2的所有值
1) "e"
2) "d"
3) "c"
127.0.0.1:6379> sdiff key1 key2 # 差集
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2 # 交集
1) "c"
127.0.0.1:6379> sunion key1 key2 # 並集
1) "e"
2) "a"
3) "c"
4) "d"
5) "b"
名稱 | 命令 | 案例 | 結果 |
---|---|---|---|
向集合新增元素 | sadd 集合key 內容 ... | sadd mldn-set 1 2 3 4 1 2 | (integer)6 |
查詢set集合(無序排列,無重複) | smembers 集合key | smembers mldn-set | 1)22)3.... |
刪除集合元素 | srem 集合key 內容 | srem mldn-set | (integer)1 |
從集合中隨機彈出一個元素,返回刪除元素 | spop 集合key | spop mldn-set | 1 |
從集合中隨機彈出一個元素,但是不刪除元素 | srandmember 集合key | srandmember mldn-set | 1 |
返回兩個集合的差集 | sdiff 集合key1 集合key2 | sdiff mldn-set mldn-set2 | |
返回兩個集合的交集 | sinter 集合key1 集合key2 | sinter mldn-set mldn-set2 | |
返回兩個集合的並集 | sunion 集合key1 集合key2 | sunion mldn-set mldn-set2 | |
將差集儲存到新的集合之中 | sdiffstore 儲存集合 集合key1 集合key2 | sdiffstore new-set mldn-set mldn-set2 | |
將交集儲存到新的集合之中 | sinterstore 儲存集合 集合Key1 集合Key2 | sinterstore new-set mldn-set mldn-set2 | |
將並集儲存到新的集合之中 | sunionstore 儲存集合 集合key1 集合key2 | sunionstore new-set mldn-set mldn-set2 | |
從第一個key對應的set中移除並新增到外一個集合之中 | smove 集合key1 集合key2 第一個集合的內容 | smove mldn-set mldn-set2 6 | (integer)1 |
返回名稱為key的集合個數 | scard 集合key | scard mldn-set | (integer)4 |
測試member是否是名稱為key的set的元素 | sismember 集合key 內容 | sismember mldn-set 9 | (integer)0 |
5、Zset (有序集合)
追加和獲取有序集合資料:
127.0.0.1:6379> zadd myset 1 one # 新增一個值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 新增多個值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1 # 獲取集合資料
1) "one"
2) "two"
3) "three"
127.0.0.1:6379>
實現排序:
127.0.0.1:6379> zadd salary 2500 xiaohong # 新增一個值
(integer) 1
127.0.0.1:6379> zadd salary 5000 xiaoming # 新增一個值
(integer) 1
127.0.0.1:6379> zadd salary 500 xaiozhang # 新增一個值
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 # 獲取集合資料
1) "xaiozhang"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrangebyscore salary -inf +inf # 從小到大顯示全部的使用者
1) "xaiozhang"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrevrange salary 0 -1 # 從大到小進行排序
1) "xiaoming"
2) "xiaohong"
3) "xaiozhang"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores # 附帶成績的顯示所有使用者
1) "xaiozhang"
2) "500"
3) "xiaohong"
4) "2500"
5) "xiaoming"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores # 顯示工資小於 2500 的使用者
1) "xaiozhang"
2) "500"
3) "xiaohong"
4) "2500"
127.0.0.1:6379> zrange salary 0 -1 # 獲取集合資料
1) "xaiozhang"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrem salary xiaohong # 移除特定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xaiozhang"
2) "xiaoming"
127.0.0.1:6379> zcard salary # 獲取有序集合的個數
(integer) 2
127.0.0.1:6379>
127.0.0.1:6379> zadd myset 1 hello # 新增一個值
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 x # 新增兩個值
(integer) 2
127.0.0.1:6379> zcount myset 1 3 # 獲取指定區間的人員數量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
名稱 | 命令 | 案例 |
---|---|---|
追加有序集合資料: | zadd 集合Key 分數 內容 | zadd user-mldn 1 pid-1 zadd user-mldn 1 pid-2 zadd user-mldn 1 pid-3....到pid-7 |
取出有序集合內容: | zrange 集合Key 0 -1 withscores | 顯示所有的資料的元素:grange user-mldn 0 -1 顯示每個元素和其對應的分數:grange user-mldn 0 -1 withscores 也可以輸出指定範圍的資料:grange user-mldn 0 2 withscores |
刪除集合資料: | zrem 集合Key 內容 | zrem user-mldn pid-1 |
資料增長(針對於順序增加): | zincrby 集合Key 分數內容 | zincrby user-mldn 5 pid-1 |
返回集合中指定元素的索引數值: | zrank 集合Key 內容 | zrank user-mldn pid-1 |
反轉資料: | zrevrank 集合Key 內容 | zrevrank user-mldn pid-1 |
反轉後取得資料: | zrevrange 集合 開始分數 結束分數 withscores | zrevrange user-mldn 0 -1 withscores |
根據索引取得指定範圍的資料: | zrangebyscore 集合 開始分數 猜束分數 withscores | 閉區間處理:zrangebyscore user-mldn 3 6 withscores (包含了3和6) 開區間處理:zrangebyscore user-mldn (3 (6 withscores 設定一個範圍:zrangebyscore user-mldn 1 10 withscores limi 0 2 |
取得集合中指定分數範圍的數量: | zcount 集合Key 開始分數 結束分數 | zcount user-mldn 2 8 |
取得指定集合中的元素個數: | zcard 集合Key | zcard user-mldn |
根據下標排序,刪除指定範圍中的資料 | zremrangebyrank 集合Key 索引開始 結束索引 | zremrangebyrank user-mldn 0 5 |
2、Redis 三種特殊資料型別
1、geospatial
Redis 在 3.2 推出 Geo 型別,該功能可以推算出地理位置資訊,兩地之間的距離。
文件: https://www.redis.net.cn/order/3687.html
藉助網站模擬一些資料: http://www.jsons.cn/lngcode/
geoadd 新增地理位置
規則:兩極無法直接新增,一般會下載城市資料,直接通過 Java 程式一次性匯入。
有效的經度從 -180 度到 180 度。有效的緯度從 -85.05112878 度到 85.05112878 度。當座標位置超出指定範圍時,該命令將會返回一個錯誤。
127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin
(error) ERR invalid longitude,latitude pair 39.900000,116.400000
新增一些模擬資料:
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shengzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
127.0.0.1:6379>
geopos 獲得當前定位座標值
127.0.0.1:6379> geopos china:city beijing # 獲得指定城市的經緯度
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
2) "31.22999903975783553"
127.0.0.1:6379>
geodist 獲取兩個位置之間的距離
單位:
- m 表示單位為米。
- km 表示單位為千米。
- mi 表示單位為英里。
- ft 表示單位為英尺。
如果使用者沒有顯式地指定單位引數, 那麼 GEODIST
預設使用米作為單位。
127.0.0.1:6379> geodist china:city beijing shanghai km # 檢視北京和上海直接的直線距離
"1067.3788"
127.0.0.1:6379> geodist china:city beijing chongqing km # 檢視北京和重慶直接的直線距離
"1464.0708"
127.0.0.1:6379>
georedius 以給定的經緯度為中心,找出某一半徑內的元素
獲得所有附近的人的地址,定位!通過半徑來查詢!
獲得指定數量的人,200。所有資料應該都錄入:china:city ,才會讓結果更加請求!
# 以110, 30 這個點為中心,尋找方圓 1000km 的城市
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "chongqing"
2) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord # 顯示他人的定位資訊
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379>
127.0.0.1:6379> georadius china:city 110 30 500 km withdist # 顯示到中心點的距離
1) 1) "chongqing"
2) "341.9374"
2) 1) "xian"
2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 1 # 指定數量
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 2
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379>
GEORADIUSBYMEMBER 找出位於指定元素周圍的其他元素
# 找出位於指定元素周圍的其他元素!
127.0.0.1:6379> georadiusbymember china:city shanghai 1000 km
1) "hangzhou"
2) "shanghai"
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"
geo 底層實現原理其實就是 zset ,可以使用 zset 命令操作 geo
127.0.0.1:6379> zrange china:city 0 -1 # 檢視地圖中全部的元素
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 刪除一個指定元素
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
2、hyperloglog
基數:數學上集合的元素個數,是不能重複的。
UV(Unique visitor):是指通過網際網路訪問、瀏覽這個網頁的自然人。訪問的一個電腦客戶端為一個訪客,一天內同一個訪客僅被計算一次。
Redis 2.8.9 版本更新了 hyperloglog 資料結構,是基於基數統計的演算法。
hyperloglog 的優點是佔用記憶體小,並且是固定的。儲存 2^64 個不同元素的基數,只需要 12 KB 的空間。但是也可能有 0.81% 的錯誤率。
這個資料結構常用於統計網站的 UV。傳統的方式是使用 set 儲存使用者的ID,然後統計 set 中元素的數量作為判斷標準。但是這種方式儲存了大量的使用者 ID,ID 一般比較長,佔空間,還很麻煩。我們的目的是計數,不是儲存資料,所以這樣做有弊端。但是如果使用 hyperloglog 就比較合適了。
127.0.0.1:6379> pfadd mykey a b c d e f g h i j # 建立第一組元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey # 統計 mykey 基數
(integer) 10
127.0.0.1:6379> PFADD mykey2 i j z x c v b n m # 建立第二組元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2 # 統計 mykey2 基數
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 # 合併兩組 mykey mykey2 => mykey3
OK
127.0.0.1:6379> PFCOUNT mykey3
(integer) 15
127.0.0.1:6379>
3、bitmap 點陣圖
bitmap就是通過最小的單位bit來進行0或者1的設定,表示某個元素對應的值或者狀態。一個bit的值,或者是0,或者是1;也就是說一個bit能儲存的最多資訊是2。
bitmap 常用於統計使用者資訊比如活躍粉絲和不活躍粉絲、登入和未登入、是否打卡等。
這裡使用一週打卡的案例說明其用法:
127.0.0.1:6379> setbit sign 0 1 # 週一打卡了
(integer) 0
127.0.0.1:6379> setbit sign 1 0 # 週二未打卡
(integer) 0
127.0.0.1:6379> setbit sign 2 0 # 週三未打卡
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
127.0.0.1:6379>
檢視某一天是否打卡:
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 0
127.0.0.1:6379>
統計:統計打卡的天數
127.0.0.1:6379> BITCOUNT sign
(integer) 4
127.0.0.1:6379>
Redis 事務
Redis 事務的本質:一組命令的集合!一個事務中的所有命令都會被序列化,在事務執行過程中,會按照順序執行。
一次性、順序性、排他性的執行一組命令。
Redis 事務沒有隔離級別的概念。
所有的命令在事務中,並沒有直接被執行,只有發起執行命令的時候才會執行(exec)。
Redis 單條命令是保證原子性的,但是事務不保證原子性。
Redis 事務的命令:
- 開啟事務:multi
- 命令入隊:正常執行命令
- 執行事務:exec
- 撤銷事務:discard
1、正常執行事務
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec # 執行事務
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379>
2、放棄事務
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set m1 n1
QUEUED
127.0.0.1:6379> set m2 n2
QUEUED
127.0.0.1:6379> DISCARD # 放棄事務
OK
127.0.0.1:6379> get m1 # 事務佇列中命令都不會被執行
(nil)
編譯型異常:命令有錯,事務中所有的命令都不會被執行
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> setget k3 v3 # 錯誤的命令
(error) ERR unknown command `setget`, with args beginning with: `k3`, `v3`,
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec # 執行事務報錯
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4 # 所有的命令都不會被執行
(nil)
127.0.0.1:6379>
執行時異常:如果事務中某條命令執行結果報錯,其他命令是可以正常執行的,錯誤命令丟擲異常
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> incr k1 # 會執行失敗
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec # 執行事務報錯
1) (error) ERR value is not an integer or out of range # 第一條命令執行失敗,其餘的正常執行
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
3、監視 Watch (面試常問)
悲觀鎖:很悲觀,認為什麼時候都會出問題,無論什麼都會加鎖。影響效率,實際情況一般會使用樂觀鎖。
樂觀鎖:很樂觀,認為什麼時候都不會出現問題,所以不上鎖。更新資料的時候回判斷一下,在此期間是否修改過監視的資料,也就是獲取 version。
首先要了解redis事務中watch的作用,watch命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之後的事務就不會執行。監控一直持續到exec命令(事務中的命令是在exec之後才執行的,所以在multi命令後可以修改watch監控的鍵值)。假設我們通過watch命令在事務執行之前監控了多個Keys,倘若在watch之後有任何Key的值發生了變化,exec命令執行的事務都將被放棄,同時返回Null multi-bulk應答以通知呼叫者事務執行失敗。
所以,需要注意的是watch監控鍵之後,再去操作這些鍵,否則watch可能會起不到效果。
Redis 監視測試
正常測試:
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 監視 money 物件
OK
127.0.0.1:6379> multi # 事務正常結束,執行期間,money 沒有變動,這個時候就能執行成功了
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>
測試多執行緒修改值,使用 watch 可以當做 Redis 的樂觀鎖操作。
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 10
OK
127.0.0.1:6379> watch money # 監視 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> DECRBY out 10
QUEUED
127.0.0.1:6379> exec # 執行之前,在另外一個執行緒 B 中修改 money 的值,下面就是執行失敗。
(nil)
127.0.0.1:6379>
B 執行緒:
[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> set money 30
OK
如果修改失敗,獲取最新的值就好。
127.0.0.1:6379> UNWATCH # 事務執行失敗,先解鎖
OK
127.0.0.1:6379> WATCH money # 獲取最新的值,再次監視。相當於 MySQL 中的 select version
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 1
QUEUED
127.0.0.1:6379> INCRBY out 1
QUEUED
127.0.0.1:6379> exec # 執行的時候會對比監視的值,如果發生變化會執行失敗。
1) (integer) 29
2) (integer) 11
127.0.0.1:6379>
Redis的Java客戶端
Redis的Java客戶端很多,官方推薦的有三種:Jedis、Redisson和lettuce。
Jedis:
- 輕量,簡潔,便於整合和改造
- 支援連線池
- 支援pipelining、事務、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支援讀寫分離,需要自己實現
- 文件差(真的很差,幾乎沒有……)
Redisson:
- 基於Netty實現,採用非阻塞IO,效能高
- 支援非同步請求
- 支援連線池
- 支援pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支援事務,官方建議以LUA Scripting代替事務
- 支援在Redis Cluster架構下使用pipelining
- 支援讀寫分離,支援讀負載均衡,在主從複製和Redis Cluster架構下都可以使用
- 內建Tomcat Session Manager,為Tomcat 6/7/8提供了會話共享功能
- 可以與Spring Session整合,實現基於Redis的會話共享
- 文件較豐富,有中文文件
Lettuce:
- 基於Netty框架的事件驅動的通訊層
- 其方法呼叫是非同步的
- Lettuce的API是執行緒安全的,
- 所以可以操作單個Lettuce連線來完成各種操作
參考:
1、Jedis
Jedis託管在github上,地址:https://github.com/xetorthio/jedis
使用 Java 操作 Redis 。Jedis 是 Redis 官方推薦的Java 連結開發工具,是 Java 操作 Redis 的中介軟體。
1、匯入依賴
<dependencies>
<!-- jeids -->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!-- fastjson -->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
</dependencies>
2、測試:啟動本地 Windows 版本的 Redis
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379, 10);
System.out.println(jedis.ping()); // PONG
}
}
事務
/**
* 測試事務
*/
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "xxx");
// 開啟事務
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("user1", result);
multi.set("user2", result);
int i = 1 / 0; // 模擬異常
multi.exec(); // 執行事務
} catch (Exception e) {
multi.discard(); // 放棄事務
e.printStackTrace();
} finally {
// 正常執行時{"name":"xxx","hello":"world"}
System.out.println(jedis.get("user1")); // null
System.out.println(jedis.get("user1"));
jedis.close(); // 關閉連結
}
}
}
2、lettuce
lettuce 官網地址:https://lettuce.io/
lettuce GitHub專案地址:https://github.com/lettuce-io/lettuce-core
3、redisson
redisson 官網地址:https://redisson.org/
redisson GitHub專案地址:https://github.com/redisson/redisson/wiki/目錄
SpinrgBoot整合Redis
1、基本使用
Spring Data 也是和 Spring Boot 齊名的專案。
說明:在 Spring Boot 2.x 之後,原來的 Jedis 被替換為了 lettuce。
Jedis:採用的直連,多執行緒操作的話是不安全的,如果想要避免不安全的話,使用 Jedis Pool ,更像 BIO 模式。
lettuce :採用 netty ,例項可以在多個執行緒中共享,不存線上程不安全的情況,可以減少執行緒連結,更像 NIO 模式。
# Spring Boot 所有的配置類,都有一個自動配置類 RedisTemplate
# 自動配置類都會繫結一個 properties 配置檔案。 RedisProperties
閱讀原始碼:
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
) // 我們可以自己定義一個 RedisTemplate 來替換這個預設的。
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 預設的 RedisTemplate 沒有過多的設定, Redis 物件都是需要序列化的。
// 兩個泛型都是 Object, Object 的型別,我們需要強制裝換為 <String, Obejct>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 由於 String 型別是 Redis 中最常用的,所以單獨提出來一個 bean .
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
測試一下:
1、匯入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置連線
# 配置 Redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
3、測試
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void contextLoads() {
// 除了基本的操作,我們常用的方法都可以直接通過 redisTemplate 操作,比如事務和 CRUD
redisTemplate.opsForValue().set("name", "xiaoming");
System.out.println(redisTemplate.opsForValue().get("name")); // xiaoming
}
}
看一下原始碼:RedisTemplate.class
// 序列化配置
@Nullable
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (this.defaultSerializer == null) {
// 預設使用了 JDK 的序列化,會使得字串轉義
this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
}
// ...
}
我們使用 Json 序列化,所以需要自定義配置類
2、序列化
編寫一個實體類 User,測試序列化。
@Component
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
private String name;
private int age;
}
測試序列化:
@Test
public void test() throws JsonProcessingException {
User user = new User("xiaoming", 3);
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
丟擲異常:
Caused by: java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [cn.itzhouq.pojo.User]
at org.springframework.core.serializer.DefaultSerializer.serialize(DefaultSerializer.java:43)
at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:63)
... 35 more
DefaultSerializer requires a Serializable
預設的序列化需要實體類實現序列化介面。所以修改 User:
public class User implements Serializable {
private String name;
private int age;
}
結果:
User(name=xiaoming, age=3)
結果顯示正常,但是控制檯還是轉義的。
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04user"
127.0.0.1:6379>
使用 jackson 的序列化:
@Test
public void test() throws JsonProcessingException {
// 一般開發中都會使用 json 來傳遞物件
User user = new User("xiaoming", 3);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user")); // {"name":"xiaoming","age":3}
}
無論 User 是否實現了 Serializable 介面,控制檯結果顯示正常,但是客戶端中檢視還是被轉義了。
如果不想使用 JDK 的序列化,可以自己編寫 RedisTemplate。
3、自定義RedisTemplate
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* 編寫的自己的 RedisTemplate
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 為了開發方便,一般使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 採用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash 的 key 也採用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 序列化方式採用 Jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 value 序列化方式採用 Jackson
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
注入和測試:
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@Test
public void test() throws JsonProcessingException {
// 一般開發中都會使用 json 來傳遞物件
User user = new User("xiaoming", 3);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user")); // {"name":"xiaoming","age":3}
}
客戶端中檢視:
127.0.0.1:6379> keys *
1) "user"
127.0.0.1:6379>
這個時候的物件就沒有被轉義。
4、Redis實現Session共享
1、Cookie與Session
-
Cookie是什麼? Cookie 是一小段文字資訊,伴隨著使用者請求和頁面在 Web 伺服器和瀏覽器之間傳遞。Cookie 包含每次使用者訪問站點時 Web 應用程式都可以讀取的資訊,我們可以看到在伺服器寫的cookie,會通過響應頭Set-Cookie的方式寫入到瀏覽器
-
HTTP協議是無狀態的,並非TCP一樣進行三次握手,對於一個瀏覽器發出的多次請求,WEB伺服器無法區分是不是來源於同一個瀏覽器。所以伺服器為了區分這個過程會通過一個sessionid來區分請求,而這個sessionid是怎麼傳送給服務端的呢。cookie相對使用者是不可見的,用來儲存這個sessionid是最好不過了
2、Redis實現分散式叢集配置過程:
- 引入依賴:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
@EnableRedisHttpSession:開啟redis session快取
maxInactiveIntervalInSeconds:指定快取的時間
key為:spring:session:sessions:expires:+‘sessionId’的過期時間
- 開啟redis-session共享
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 50)
public class RedisConfig {
}
- 測試
@RestController
public class SessionController {
@RequestMapping(value = "/setSession", method = RequestMethod.GET)
public Map<String, Object> setSession (HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
request.getSession().setAttribute("request Url", request.getRequestURL());
map.put("request Url", request.getRequestURL());
return map;
}
@RequestMapping(value = "/getSession", method = RequestMethod.GET)
public Object getSession (HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
map.put("sessionIdUrl",request.getSession().getAttribute("request Url"));
map.put("sessionId", request.getSession().getId());
return map;
}
}
Redis 配置檔案詳解
Redis.conf 詳解
# 找到啟動時指定的配置檔案:
[root@liusx ~]# cd /usr/local/bin
[root@liusx bin]# ll
drwxr-xr-x 2 root root 4096 Aug 29 13:35 rconfig
-rwxr-xr-x 1 root root 4739840 Aug 29 12:15 redis-benchmark
-rwxr-xr-x 1 root root 9653568 Aug 29 12:15 redis-check-aof
-rwxr-xr-x 1 root root 9653568 Aug 29 12:15 redis-check-rdb
-rwxr-xr-x 1 root root 5059032 Aug 29 12:15 redis-cli
lrwxrwxrwx 1 root root 12 Aug 29 12:15 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 9653568 Aug 29 12:15 redis-server
[root@liusx bin]# cd rconfig/
[root@liusx rconfig]# vim redis.conf
1、單位
# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
配置檔案中 unit 單位對大小寫不敏感。
2、配置檔案包含
################################## INCLUDES ###################################
# Include one or more other config files here. This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings. Include files can include
# other files, so use this wisely.
#
# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf
配置檔案可以將多個配置檔案合起來使用。好比Spring、Improt, include
3、NETWORK 網路
bind 127.0.0.1 # 繫結的 IP
protected-mode no # 保護模式
port 6379 # 埠設定
4、GENERAL 通用
daemonize yes # 以守護程序的方式執行,預設是 no ,我們需要自己開啟為 yes
pidfile /var/run/redis_6379.pid # 如果是後臺啟動,我們需要指定一個pid 檔案
# 日誌級別
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice
logfile "" # 日誌檔案的位置
databases 16 # 資料庫的數量,預設是 16
always-show-logo yes # 是否總是顯示 LOGO
5、快照 SNAPSHOTTING
持久化,在規定的時間內,執行了多少次操作則會持久化到檔案
Redis 是記憶體資料庫,如果沒有持久化,那麼資料斷電即失。
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
# 如果 900s 內,至少有 1 個 key 進行了修改,進行持久化操作
save 900 1
# 如果 300s 內,至少有 10 個 key 進行了修改,進行持久化操作
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes # 如果持久化出錯,是否還要繼續工作
rdbcompression yes # 是否壓縮 rdb 檔案,需要消耗一些 cpu 資源
rdbchecksum yes # 儲存 rdb 檔案的時候,進行錯誤的檢查校驗
dir ./ # rdb 檔案儲存的目錄
6、SECURITY 安全
可以設定 Redis 的密碼,預設是沒有密碼的。
[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass # 獲取 redis 密碼
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 設定 redis 密碼
OK
127.0.0.1:6379> ping
(error) NOAUTH Authentication required. # 發現所有的命令都沒有許可權了
127.0.0.1:6379> auth 123456 # 使用密碼登入
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379>
7、CLIENTS 限制
################################### CLIENTS ####################################
# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000 # 設定能連結上 redis 的最大客戶端數量
# maxmemory <bytes> # redis 設定最大的記憶體容量
maxmemory-policy noeviction # 記憶體達到上限之後的處理策略
- noeviction:當記憶體使用達到閾值的時候,所有引起申請記憶體的命令會報錯。
- allkeys-lru:在所有鍵中採用lru演算法刪除鍵,直到騰出足夠記憶體為止。
- volatile-lru:在設定了過期時間的鍵中採用lru演算法刪除鍵,直到騰出足夠記憶體為止。
- allkeys-random:在所有鍵中採用隨機刪除鍵,直到騰出足夠記憶體為止。
- volatile-random:在設定了過期時間的鍵中隨機刪除鍵,直到騰出足夠記憶體為止。
- volatile-ttl:在設定了過期時間的鍵空間中,具有更早過期時間的key優先移除。
8、AOF 模式配置
APPEND ONLY 模式 AOF 配置
appendonly no # 預設是不開啟 AOF 模式的,預設使用 rdb 方式持久化,大部分情況下,rdb 完全夠用
appendfilename "appendonly.aof" # 持久化的檔案的名字
# appendfsync always # 每次修改都會 sync 消耗效能
appendfsync everysec # 每秒執行一次 sync 可能會丟失這 1s 的資料。
# appendfsync no # 不執行 sync 這個時候作業系統自己同步資料,速度最快。
Redis 持久化(面試)
面試和工作,持久化都是重點。
Redis 是記憶體資料庫,如果不將記憶體中的資料庫狀態儲存到磁碟,那麼一旦伺服器程序退出,伺服器中的資料庫狀態就會消失,所以 Redis 提供了持久化功能。
- RDB(Redis DataBase):(屬於全量資料備份,備份的是資料)
- AOF(Append Only File):(增量持久化備份,備份的是指令)
1、RDB(Redis DataBase)
什麼是 RDB(屬於全量資料備份,備份的是資料)
在指定的時間間隔內,將記憶體中的資料集快照寫入磁碟,也就是 Snapshot 快照,它恢復時是將快照檔案直接讀取到記憶體裡的。
Redis 會單獨建立(fork)一個子程序進行持久化,會先將資料寫入一個臨時檔案中,待持久化過程結束了,再用這個臨時檔案替換上次持久化好的檔案。整個過程中,主程序不進行任何 IO 操作,這就確保的極高的效能。如果需要大規模的資料的恢復,且對資料恢復的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加高效。RDB 唯一的缺點是最後一次持久化的資料可能會丟失。
生產環境下,需要對這個檔案記性
預設持久化方式是 RDB,一般不需要修改。
rdb 儲存的檔案是 dump.rdb :
# The filename where to dump the DB
dbfilename dump.rdb
測試1:
首先修改配置檔案儲存快照的策略
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# save ""
# save 900 1
# save 300 10
# save 60 10000
save 60 5 # 只要 60s 內修改了 5 次 key 就會觸發 rdb 操作。
儲存配置檔案:
127.0.0.1:6379> save
OK
127.0.0.1:6379>
刪除原始的 dump.rdb 檔案:
[root@liusx bin]# ls
dump.rdb jemalloc.sh rconfig luajit mcrypt redis-benchmark redis-check-rdb redis-sentinel
jemalloc-config jeprof libmcrypt-config luajit-2.0.4 mdecrypt redis-check-aof redis-cli redis-server
[root@liusx bin]# rm -rf dump.rdb
[root@liusx bin]# ls
jemalloc-config jeprof libmcrypt-config luajit-2.0.4 mdecrypt redis-check-aof redis-cli redis-server
jemalloc.sh rconfig luajit mcrypt redis-benchmark redis-check-rdb redis-sentinel
[root@liusx bin]#
60s 內修改 5 次 key :
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379> set k4 v4
OK
127.0.0.1:6379> set k5 v5
OK
dump.rdb 檔案再次出現。
[root@liusx bin]# ls
dump.rdb jemalloc.sh rconfig luajit mcrypt redis-benchmark redis-check-rdb redis-sentinel
jemalloc-config jeprof libmcrypt-config luajit-2.0.4 mdecrypt redis-check-aof redis-cli redis-server
[root@liusx bin]#
恢復資料:
關閉 Redis 服務和客戶端,再次進入時資料被自動恢復:
127.0.0.1:6379> shutdown # 關閉 Redis 服務
not connected> exit
[root@liusx bin]# ps -ef|grep redis # redis 已經關閉了
root 25989 23576 0 14:27 pts/1 00:00:00 grep --color=auto redis
[root@liusx bin]# redis-server rconfig/redis.conf # 再次開啟服務
25994:C 02 May 2020 14:28:01.003 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
25994:C 02 May 2020 14:28:01.003 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=25994, just started
25994:C 02 May 2020 14:28:01.003 # Configuration loaded
[root@liusx bin]# redis-cli -p 6379 # 客戶端連線
127.0.0.1:6379> get k2 # 可以直接獲取資料,說明k2 被持久化了。
"v2"
127.0.0.1:6379>
測試2:
刪除 dump.rdb 檔案
root@liusx bin]# rm -rf dump.rdb
[root@liusx bin]# ls
jemalloc-config jeprof libmcrypt-config luajit-2.0.4 mdecrypt
redis-check-aof redis-cli redis-server jemalloc.sh rconfig luajit
在客戶端清除所有資料:
127.0.0.1:6379> flushall
OK
再次檢驗 dump.rdb 檔案:
[root@liusx bin]# ls
dump.rdb jemalloc.sh rconfig luajit mcrypt redis-benchmark
redis-check-rdb redis-sentinel jemalloc-config jeprof libmcrypt-config luajit-2.0.4
mdecrypt redis-check-aof redis-cli redis-server
dump.rdb 檔案再次出現。
觸發機制
1、save 的規則滿足的情況下,會自動觸發 rdb 規則
2、執行 flushall 命令,也會觸發 rdb 規則
3、退出 redis 也會產生 rdb 檔案
備份就自動生成一個 dump.rdb 檔案。
如何恢復 rdb 檔案
1、只需要將 rdb 檔案放在 Redis 啟動目錄就可以,Redis 啟動的時候會自動檢查 dump.rdb ,恢復其中的資料;
2、檢視存放 rdb 檔案的位置,在客戶端中使用如下命令。
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin" # 如果在這個目錄下存在 dump.rdb 檔案,啟動就會自動恢復其中的資料
127.0.0.1:6379>
RDB 的優缺點
優點:
1、適合大規模的資料恢復
2、對資料的完整性要求不高
缺點:
1、需要一定的時間間隔進行操作,如果 Redis 意外宕機,最後一次修改的資料就沒有了
2、fork 程序的時候,會佔用一定的空間。
2、AOF(Append Only File)
AOF( append only file )持久化以獨立日誌的方式記錄每次寫命令,並在 Redis 重啟時在重新執行 AOF 檔案中的命令以達到恢復資料的目的。AOF 的主要作用是解決資料持久化的實時性。
以日誌形式來記錄每個操作,將 Redis 執行的過程的所有指令記錄下來(讀操作不記錄),只追加檔案但不可以改寫檔案,redis 啟動之初會讀取該檔案重新構建資料,換言之,redis 重啟的話就根據日誌檔案的內容將寫指令從前到後執行一遍以完成資料的恢復工作。
AOF 儲存的是 appendonly.aof 檔案。
開啟 AOF 模式
將配置檔案中預設為 no 的 appendonly 修改為 yes ,重啟服務。
appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
重啟後可以看到 AOF 檔案(appendonly.aof
):
[root@liusx bin]# ls
appendonly.aof redis-benchmark redis-cli redis-server backup.db
rconfig redis-check-aof redis-sentinel dump.rdb redis-check-rdb
但是檔案是空的。使用客戶端新增一些資料再次檢視:
[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
127.0.0.1:6379>
[root@liusx bin]# cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$2
k1
$2
v1
*3
$3
set
$2
k2
$2
v2
*3
$3
set
$2
k3
$2
v3
自動修復 AOF檔案
如果手動修改AOF 檔案,可能導致 Redis 服務不能啟動。比如這裡我手動在 AOF 檔案的最後一行隨便新增一些命令:
set
$2
k3
$2
v3
gjjjjjjjjj
刪除 dump.rdb 檔案,重啟服務:
[root@liusx bin]# rm -rf dump.rdb
[root@liusx bin]# ls
appendonly.aof redis-benchmark redis-cli redis-server backup.db
rconfig redis-check-aof redis-sentinel dump.rdb redis-check-rdb
[root@liusx bin]# redis-server rconfig/redis.conf
13746:C 02 May 2020 16:22:43.345 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
13746:C 02 May 2020 16:22:43.346 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=13746, just started
13746:C 02 May 2020 16:22:43.346 # Configuration loaded
[root@liusx bin]# redis-cli -p 6379
Could not connect to Redis at 127.0.0.1:6379: Connection refused # 連線失敗
not connected>
如果這個 AOF 檔案有錯位,客戶端就不能連結了,需要修復 AOF 檔案。Redis 提供了工具 redis-check-aof --fix
[root@liusx bin]# redis-check-aof --fix appendonly.aof
0x 6e: Expected prefix '*', got: 'g'
AOF analyzed: size=122, ok_up_to=110, diff=12
This will shrink the AOF from 122 bytes, with 12 bytes, to 110 bytes
Continue? [y/N]: y
Successfully truncated AOF
[root@liusx bin]#
重啟服務,再次嘗試連結成功。
重寫規則說明
aof 預設就是檔案的無限追加,檔案會越來越大!
rdb-del-sync-files no
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
# 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 檔案大於 64m,太大了! fork一個新的程序來將我們的檔案進行重寫!
AOF 的優缺點
appendonly yes # 預設是 no,是使用rdb方式持久化的,在大部分所有的情況下, rdb完全夠用!
appendfilename "appendonly.aof" # 持久化的檔案的名字
# appendfsync always # 每次修改都會 sync ,消耗效能
appendfsync everysec # 每秒執行一次 sync ,可能會丟失這 1s 的資料
# appendfsync no # 不執行 sync,這個時候作業系統自己同步資料,速度最快
優點:
1、每一次修改都同步,檔案的完整性更加好
2、每秒同步一次,可能會丟失一秒的資料
3、從不同步,效率最高的
缺點:
1、相對於資料檔案來說, AOF 遠遠大於 RDB ,修復的速度也比 RDB 慢
2、AOF 的執行效率也比 RDB 慢,所以 Redis 預設的配置就是 RDB 持久化。
RDB 和 AOF 對比
RDB | AOF | |
---|---|---|
啟動優先順序 | 低 | 高 |
體積 | 小 | 大 |
恢復速度 | 快 | 慢 |
資料安全性 | 丟資料 | 根據策略決定 |
3、擴充套件
1、RDB 持久化方式能夠在指定的時間間隔內對你的資料進行快照儲存
2、AOF 持久化方式記錄每次對伺服器寫的操作,當伺服器重啟的時候會重新執行這些命令來恢復原始的資料,AOF 命令以Redis 協議追加儲存每次寫的操作到檔案尾部,Redis 還能對 AOF 檔案記性後臺重寫,使得AOF 檔案的體積不至於過大
3、只做快取,如果你只希望你的資料在伺服器執行的時候存在,你也可以不使用任何持久化
4、同時開啟兩種持久化方式
- 在這種情況下,當 Redis 重啟的時候會優先載入AOF 檔案來恢復原始的資料,因為在通常情況下,AOF 檔案儲存的資料集要比 RDB 檔案儲存的資料集要完整。
- RDB 的資料不實時,同步使用兩者時伺服器重啟也只會找 AOF 檔案。那要不要只使用 AOF 呢?作者建議不要,因為 RDB 更適合用於備份資料庫(AOF 在不斷變化不好備份),快速重啟,而且不會有 AOF 可能潛在的 BUG,留著作為一個萬一的手段。
5、效能建議
- 因為 RDB 檔案只用作後備用途,建議只在 Slave 上持久化 RDB 檔案,而且只要 15 分鐘備份一次就夠了,只保留save 900 1 這條規則。
- 如果 Enable AOF ,好處是在最惡劣情況下也只會丟失不超過兩秒的資料,啟動指令碼較簡單隻 load 自己的 AOF 檔案就可以了,代價是一是帶來了持續的IO,而是 AOF rewrite 的最後將rewrite 過程中產生的新資料寫到新檔案造成的阻塞幾乎是不可避免的。只要硬碟許可,應該儘量減少 AOF rewrite 的頻率,AOF 重寫的基礎大小預設值是 64M 太小了,可以設定到 5G 以上,預設值超過原大小 100% 大小重寫可以改到適當的數值。
- 如果不 Enable AOF ,僅靠 Master-Slave Repllcation 實現高可用也可以,能省掉一大筆 IO ,也減少了 rewrite 時帶來的系統波動。代價是如果 Master/Slave 同時宕掉,會丟失十幾分鐘的資料,啟動指令碼也要比較兩個 Master/Slave 中的 RDB 檔案,載入較新的那個,微博就是這種架構。
Redis 釋出訂閱
Redis 釋出訂閱(pub/sub)是一種訊息通訊模式:傳送者發(pub)送訊息,訂閱者(sub)接收訊息。
Redis 客戶端可以訂閱任意數量的頻道。
訂閱 / 釋出訊息圖:第一個:訊息傳送者, 第二個:頻道 第三個:訊息訂閱者!
下圖展示了頻道 channel1,已經訂閱這個頻道的三個客戶端。
當有新訊息通過 publish 命令傳送給頻道 channel1 時,這個訊息就會被髮送給訂閱它的三個客戶端。
命令
這些命令被廣泛應用於構建即時通訊應用、比如網路聊天室和實時廣播、實時提醒等。
序號 | 命令及描述 |
---|---|
1 | PSUBSCRIBE pattern pattern ... 訂閱一個或多個符合給定模式的頻道。 |
2 | PUBSUB subcommand [argument [argument ...]] 檢視訂閱與釋出系統狀態。 |
3 | PUBLISH channel message 將資訊傳送到指定的頻道。 |
4 | PUNSUBSCRIBE [pattern [pattern ...]退訂所有給定模式的頻道。 |
5 | SUBSCRIBE channel channel ...訂閱給定的一個或多個頻道的資訊。 |
6 | UNSUBSCRIBE [channel [channel ...]] 指退訂給定的頻道。 |
測試
以下例項演示了釋出訂閱是如何工作的。在我們例項中我們建立了訂閱頻道名為 redisChat:
redis 127.0.0.1:6379> SUBSCRIBE redisChat # 訂閱一個頻道:redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
# 等待讀取推送的資訊
現在,我們先重新開啟個 redis 客戶端,然後在同一個頻道 redisChat 釋出兩次訊息,訂閱者就能接收到訊息。
redis 127.0.0.1:6379> PUBLISH redisChat "Redis is a great caching technique"
(integer) 1
redis 127.0.0.1:6379> PUBLISH redisChat "Learn redis by runoob.com"
(integer) 1
# 訂閱者的客戶端會顯示如下訊息
1) "message"
2) "redisChat"
3) "Redis is a great caching technique"
1) "message"
2) "redisChat"
3) "Learn redis by runoob.com"
原理
Redis 是使用 C語言 實現的,通過分析 Redis 原始碼裡的 public.c 檔案,瞭解釋出和訂閱機制的底層實現,藉此加深對 Redis 的理解。Redis 通過 public 、subscribe 和 psubscribe 等命令實現釋出和訂閱功能。
微信:
通過 subscribe 命令訂閱某頻道後,redis=server 裡面維護了一個字典,字典的鍵就是一個個頻道!而字典的值則是一個連結串列,連結串列儲存了所有訂閱這個 channel 的客戶端。subscribe 命令的關鍵,就是講客戶端新增到給定 channel 的訂閱鏈中。
通過 publish 命令向訂閱者傳送訊息,redis-server 會使用給定的頻道作為鍵,在它所維護的channel 字典中查詢記錄了訂閱這個頻道的所有客戶端的連結串列,遍歷這個連結串列,將訊息釋出給所有的訂閱者。
使用場景:
- 實時訊息系統
- 實時聊天
- 訂閱、關注系統都可以
稍微複雜的場景更多的使用訊息中介軟體 MQ。
主從複製
1、概念
主從複製,是指將一臺 Redis 伺服器的資料,複製到其他的 Redis 伺服器。前者稱之為主節點(master/leader),後者稱之為從節點(slave/flower);資料的複製都是單向的,只能從主節點到從節點。Master 以寫為主,Slave 以讀為主。
預設情況下,每臺 Redis 伺服器都是主節點。且一個主節點可以有多個從節點或者沒有從節點,但是一個從節點只能有一個主節點。
2、主從複製的作用
1、資料冗餘:主從複製實現了資料的熱備份,是持久化的之外的一種資料冗餘方式。
2、故障恢復:當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復。實際也是一種服務的冗餘。
3、負載均衡:在主從複製的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務(即寫 Redis 資料時應用連線主節點,讀 Redis 的時候應用連線從節點),分擔伺服器負載;尤其是在寫少讀多的場景下,通過多個節點分擔讀負載,可以大大提高 Redis 伺服器的併發量。
4、高可用(叢集)的基石:除了上述作用以外,主從複製還是哨兵模式和叢集能夠實施的基礎,因此說主從複製是 Redis 高可用的基礎。
一般來說,要將Redis 運用於工程專案中,只使用一臺 Redis 是萬萬不能的(可能會宕機),原因如下:
1、從結構上,單個 Redis 伺服器會發生單點故障,並且一臺伺服器需要處理所有的請求負載,壓力很大;
2、從容量上,單個 Redis 伺服器記憶體容量有限,就算一臺 Redis 伺服器記憶體容量為 265G, 也不能將所有的記憶體用作 Redis 儲存記憶體,一般來說,單臺 Redis最大使用記憶體不應該超過 20G。
電商網站上的商品,一般都是一次上傳,無數次瀏覽的,說專業點就是“多讀少寫”。
對於這種場景,我們可以使用如下這種架構:
主從複製,讀寫分離!80% 的情況下,都是在進行讀操作。這種架構可以減少伺服器壓力,經常使用實際生產環境中,最少是“一主二從”的配置。真實環境中不可能使用單機 Redis。
3、環境配置
只配置從庫,不用配置主庫。
[root@liusx bin]# redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> info replication # 檢視當前庫的資訊
# Replication
role:master # 角色
connected_slaves:0 # 當前沒有從庫
master_replid:2467dd9bd1c252ce80df280c925187b3417055ad
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>
複製 3 個配置檔案,然後修改對應的資訊
1、port 埠
2、pid 名稱
3、log 檔名稱
4、dump.rdb 名稱
port 6380
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dbfilename dump6380.rdb
port 6381
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump6381.rdb
修改完畢後,啟動我們的 3 個 redis 伺服器,可以通過程序資訊查詢。
[root@liusx ~]# ps -ef|grep redis
root 426 1 0 16:53 ? 00:00:00 redis-server *:6379
root 446 1 0 16:54 ? 00:00:00 redis-server *:6380
root 457 1 0 16:54 ? 00:00:00 redis-server *:6381
root 464 304 0 16:54 pts/3 00:00:00 grep --color=auto redis
4、一主二從
預設情況下,每臺 Redis 伺服器都是主節點,我們一般情況下,只用配置從機就好了。
主機:6379, 從機:6380 和 6381
配置的方式有兩種:
- 一種是直接使用命令配置,這種方式當 Redis 重啟後配置會失效。
- 另一種方式是使用配置檔案。這裡使用命令演示一下。
下面將80 和 81 兩個配置為在從機。
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host port 找誰當自己的老大!
OK
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:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:907bcdf00c69d361ede43f4f6181004e2148efb7
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
127.0.0.1:6380>
配置好了之後,看主機:
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2 # 主節點下有兩個從節點
slave0:ip=127.0.0.1,port=6380,state=online,offset=420,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=420,lag=1
master_replid:907bcdf00c69d361ede43f4f6181004e2148efb7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:420
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:420
127.0.0.1:6379>
真實的主從配置應該是在配置檔案中配置,這樣才是永久的。這裡使用命令是暫時的。
配置檔案 redis.conf
################################# REPLICATION #################################
# Master-Replica replication. Use replicaof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
# +------------------+ +---------------+
# | 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> # 這裡配置
slaveof localhost 6379 # 這裡配置
# 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 <master-password>
配置方式也是一樣的。
5、幾個問題
1、主機可以寫,從機不能寫只能讀。主機中的所有資訊和資料都會儲存在從機中。如果從機嘗試進行寫操作就會報錯。
127.0.0.1:6381> get k1 # k1的值是在主機中寫入的,從機中可以讀取到。
"v1"
127.0.0.1:6381> set k2 v2 # 從機嘗試寫操作,報錯了
(error) READONLY You can't write against a read only replica.
127.0.0.1:6381>
2、如果主機斷開了,從機依然連結到主機,可以進行讀操作,但是還是沒有寫操作。這個時候,主機如果恢復了,從機依然可以直接從主機同步資訊。
3、使用命令列配置的主從機,如果從機重啟了,就會變回主機。如果再通過命令變回從機的話,立馬就可以從主機中獲取值。這是複製原理決定的。
6、複製原理
Slave 啟動成功連線到 Master 後會傳送一個 sync 同步命令。
Master 接收到命令後,啟動後臺的存檔程序,同時收集所有接收到的用於修改資料集的命令,在後臺程序執行完畢後,master 將傳送整個資料檔案到 slave ,並完成一次完全同步。
全量複製:Slave 服務在接收到資料庫檔案後,將其存檔並載入到記憶體中。
增量複製: Master 繼續將新的所有收集到的修改命令一次傳給 slave,完成同步。
但是隻要重新連線 master ,一次完全同步(全量複製)將被自動執行。我們的資料一定可以在從機中看到。
這種模式的原理圖:
第一種模式
第二種模式
這種模式的話,將 6381 的主節點配置為 6380 。主節點 6379 只有一個從機。
如果現在 6379 節點宕機了, 6380 和 6381 節點都是從節點,只能進行讀操作,都不會自動變為主節點。需要手動將其中一個變為主節點,使用如下命令:
SLAVEOF no one
哨兵模式
(自動選舉老大的模式)
1、概述
主從切換技術的方式是:當主機伺服器宕機之後,需要手動將一臺伺服器切換為主伺服器,這需要人工干預,費時費力,還會造成一段時間內的服務不可用。這不是一種推薦的方式,更多的時候我們優先考慮的的是哨兵模式。Redis 從 2.8 開始正式提供了 Sentinel(哨兵)架構來解決這個問題。
哨兵模式能夠後臺監控主機是否故障,如果故障了根據投票數自動將從庫轉換為主庫。
哨兵模式是一種特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一個獨立的程序,作為程序,它獨立執行。其原理是哨兵通過傳送命令,等待 Redis 伺服器響應,從而監控執行的多個 Redis 例項。
這裡的哨兵有兩個作用
- 通過傳送命令,讓 Redis 伺服器返回監控其執行狀態,包括主伺服器和從伺服器
- 當哨兵檢測到 master 宕機,會自動將 slave 切換為 master,然後通過釋出訂閱模式通知其他的從放伺服器,修改配置檔案,讓他們切換主機。
然而一個哨兵程序對 Redis 伺服器進行監控,可能會出現問題,為此,我們可以使用多個哨兵進行監控。各個哨兵之間還會進行監控,這樣就形成了多哨兵模式。
假設主伺服器宕機了,哨兵1先檢測到這個結果,系統並不會馬上進行 failover 過程,僅僅是哨兵 1 主觀認為主伺服器不可用,這個現象稱之為主觀下線。當後面的哨兵也檢測到主伺服器不可用,並且數量達到一定值時,那麼哨兵之間就會進行一次投票,投票的結果由一個哨兵發起,進行 failover 【故障轉移】。切換成功後,就會通過釋出訂閱模式,讓各個哨兵把自己監控的從伺服器實現切換主機,這個過程稱之為客觀下線。
2、哨兵模式(1主2從)
1、配置哨兵模式配置檔案,新建檔案 /usr/local/bin/rconfig/sentinel.conf
。
# sentinel monitor 被監控的名字(隨便寫) host 1
sentinel monitor myredis 127.0.0.1 1
後面的數字1代表主機宕機後,slave投票決定誰成為新的主機,票數最多成為主機。
2、啟動哨兵
[root@liusx bin]# ls
6379.log 6381.log dump6380.rdb dump.rdb redis-benchmark redis-check-rdb redis-sentinel
6380.log dump6379.rdb dump6381.rdb rconfig redis-check-aof redis-cli redis-server
[root@liusx bin]# redis-sentinel rconfig/sentinel.conf
2421:X 15 May 2020 20:24:06.847 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2421:X 15 May 2020 20:24:06.847 # Redis version=5.0.5, bits=64, commit=00000000, modified=0, pid=2421, just started
2421:X 15 May 2020 20:24:06.847 # Configuration loaded
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.5 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2421
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2421:X 15 May 2020 20:24:06.848 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2421:X 15 May 2020 20:24:06.851 # Sentinel ID is 100430af0018d23bd1ae2fe57e71e0d45f64d9a5
2421:X 15 May 2020 20:24:06.851 # +monitor master myredis 127.0.0.1 6379 quorum 1
2421:X 15 May 2020 20:24:06.852 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
啟動成功~!
如果現在 Master 節點宕機了,這個時候會從從機中根據投票演算法選擇一個作為主機。
如果原來的主機恢復運行了,只能歸到新的主機下,作為從機, 這就是哨兵模式的規則。
哨兵模式的優點
1、哨兵叢集,基於主從複製模式,所有的主從配置優點,它全有
2、主從可以切換,故障可以轉移,系統的可用性就會更好
3、哨兵模式就是主從模式的升級,手動到自動,更加健壯。
哨兵模式的缺點
1、Redis 不方便線上擴容,叢集達到一定的上限,線上擴容就會十分麻煩;
2、實現哨兵模式的配置其實也很麻煩,裡面有甚多的配置項。
哨兵模式的全部配置(完整的哨兵模式配置檔案 sentinel.conf)
# Example sentinel.conf
# 哨兵sentinel例項執行的埠 預設26379
port 26379
# 哨兵sentinel的工作目錄
dir /tmp
# 哨兵sentinel監控的redis主節點的 ip port
# master-name 可以自己命名的主節點名字 只能由字母A-z、數字0-9 、這三個字元".-_"組成。
# quorum 當這些quorum個數sentinel哨兵認為master主節點失聯 那麼這時 客觀上認為主節點失聯了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
# 當在Redis例項中開啟了requirepass foobared 授權密碼 這樣所有連線Redis例項的客戶端都要提供密碼
# 設定哨兵sentinel 連線主從的密碼 注意必須為主從設定一樣的驗證密碼
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之後 主節點沒有應答哨兵sentinel 此時 哨兵主觀上認為主節點下線 預設30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 這個配置項指定了在發生failover主備切換時最多可以有多少個slave同時對新的master進行 同步,
這個數字越小,完成failover所需的時間就越長,
但是如果這個數字越大,就意味著越 多的slave因為replication而不可用。
可以通過將這個值設為 1 來保證每次只有一個slave 處於不能處理命令請求的狀態。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障轉移的超時時間 failover-timeout 可以用在以下這些方面:
#1. 同一個sentinel對同一個master兩次failover之間的間隔時間。
#2. 當一個slave從一個錯誤的master那裡同步資料開始計算時間。直到slave被糾正為向正確的master那裡同步資料時。
#3.當想要取消一個正在進行的failover所需要的時間。
#4.當進行failover時,配置所有slaves指向新的master所需的最大時間。不過,即使過了這個超時,slaves依然會被正確配置為指向master,但是就不按parallel-syncs所配置的規則來了
# 預設三分鐘
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置當某一事件發生時所需要執行的指令碼,可以通過指令碼來通知管理員,例如當系統執行不正常時發郵件通知相關人員。
#對於指令碼的執行結果有以下規則:
#若指令碼執行後返回1,那麼該指令碼稍後將會被再次執行,重複次數目前預設為10
#若指令碼執行後返回2,或者比2更高的一個返回值,指令碼將不會重複執行。
#如果指令碼在執行過程中由於收到系統中斷訊號被終止了,則同返回值為1時的行為相同。
#一個指令碼的最大執行時間為60s,如果超過這個時間,指令碼將會被一個SIGKILL訊號終止,之後重新執行。
#通知型指令碼:當sentinel有任何警告級別的事件發生時(比如說redis例項的主觀失效和客觀失效等等),將會去呼叫這個指令碼,
#這時這個指令碼應該通過郵件,SMS等方式去通知系統管理員關於系統不正常執行的資訊。呼叫該指令碼時,將傳給指令碼兩個引數,
#一個是事件的型別,
#一個是事件的描述。
#如果sentinel.conf配置檔案中配置了這個指令碼路徑,那麼必須保證這個指令碼存在於這個路徑,並且是可執行的,否則sentinel無法正常啟動成功。
#通知指令碼
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客戶端重新配置主節點引數指令碼
# 當一個master由於failover而發生改變時,這個指令碼將會被呼叫,通知相關的客戶端關於master地址已經發生改變的資訊。
# 以下引數將會在呼叫指令碼時傳給指令碼:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>總是“failover”,
# <role>是“leader”或者“observer”中的一個。
# 引數 from-ip, from-port, to-ip, to-port是用來和舊的master和新的master(即舊的slave)通訊的
# 這個指令碼應該是通用的,能被多次呼叫,不是針對性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
快取穿透與雪崩
服務的高可用問題!
Redis 快取的使用,極大的提升了應用程式的效能和效率,特別是資料查詢方面。但同時,它也帶了一些問題。其中,最要害的是問題,就是資料一致性的問題,從嚴格意義上講,這個問題無解。如果對資料的一致性要求很高,那麼久不能使用快取。
另外一些典型的問題就是,快取穿透、快取雪崩快取擊穿,目前,是業界也都有比較流行的解決方案。
1、快取穿透(查不到資料)
快取穿透的概念簡單,使用者想要查詢一個數據,發現 Redis 記憶體資料庫沒有,也就是快取沒有命中。於是向持久層資料庫查詢。發現也沒有,於是本次查詢失敗。當用戶很多的時候,快取都沒有命中,於是都去請求了持久層資料庫。這給持久層資料庫造成很大的壓力,這時候就相當於出現了快取穿透。
解決方案:
1、布隆過濾器
布隆過濾器是一種資料結構,對所有可能查詢的引數以 hash 形式儲存,在控制層先進行校驗,不符合則丟棄,從而避免了對底層儲存系統的查詢壓力。
2、快取空物件
當儲存層不命中後,即使返回的空物件也將其快取起來,同步會同步一個過期時間,之後再訪問這個資料將會從儲存中獲取,保護了後端資料來源。
但是這種方法會存在兩個問題:
1、如果控制能夠被快取起來,這就意味著快取需要更多的空間儲存,因為這當中可能會有很多的空值的鍵;
2、即使對空值設定了過期時間,還是會存在快取層和儲存層的資料會有一段時間視窗的不一致,這對於需要保持一致性的業務會有影響。
2、快取擊穿
快取擊穿(請求量過多、快取過期)
這裡需要注意和快取穿透的區別。快取擊穿,是指一個 key 非常熱點,在不停的扛著大併發,大併發集中對這一個點進行訪問,當這個 key 在失效的瞬間,持續的大併發就穿破快取,直接請求資料庫,就像在一個屏障上鑿開了一個洞。
當某個 key 在過期的瞬間,有大量的請求併發訪問,這類資料一般是熱點資料,由於快取過期,會同時訪問資料庫來查詢最新的資料,並回寫快取,會導致資料庫瞬間壓力過大。
解決方案:
1、設定熱點資料永不過期
從快取層來看,沒有設定過期時間,所以不會出現熱點 key 過期後產生的問題。
2、加互斥鎖
分散式鎖:使用分散式鎖,保證對於每個 key 同時只有一個執行緒去查詢後端服務,其他執行緒沒有獲得分散式鎖的許可權,因此只需要等待即可。這種方式將高併發的壓力轉移到了分散式鎖,因對分散式鎖的考驗很大。
3、快取雪崩
快取雪崩,是指在某一個時間段,快取集中過期失效。
產生雪崩的原因之一,比如馬上就要雙十二零點,,很快就會有一波搶購,這波商品時間比較集中的放在了快取,假設快取一個小時。那麼到了凌晨一點鐘的時候,這批商品的快取就都會過期了。而對這批商品的訪問查詢,都落到資料庫上,對於資料庫而言,就會產生週期性的壓力波峰。於是所有的請求都會到達儲存層,儲存層的呼叫量會暴增,造成儲存層也回掉的情況。
其實集中時期,倒不是非常致命,比較致命的快取雪崩,是快取伺服器某個節點宕機或者斷網。因為自然形成 的快取雪崩,一定是某個時間段中建立快取,這個時候也是可以頂住壓力的。無非就是對資料庫產生週期性的壓力而已。而快取服務節點的宕機,對於資料庫伺服器的壓力是不可預的,很有可能瞬間就把資料庫壓垮。
解決方案
1、Redis 高可用
這個思想的含義是,既然 redis 有可能掛掉,那我多增設幾臺 redis,這樣一臺掛掉之後其他的還可以繼續工作,其實就是搭建的叢集。
2、限流降級
這個解決方案的思想是,在快取失效後,通過加鎖或者佇列來控制資料庫寫快取的執行緒數量。比如對某個 key 只允許一個執行緒查詢資料和寫快取,其他執行緒等待。
3、資料預熱
資料預熱的含義是在正式部署之前,把可能的資料線預先訪問一遍,這樣部分可能大量訪問的資料就會載入到快取。在即將發生大併發訪問前手動觸發載入快取不同的key,設定不同的過期時間,讓快取失效的時間點儘量均勻。
Redis 面試題
1、什麼是 Redis?有什麼特點?
Redis 是一款開源,高效能的 key-value 的非關係型資料庫。
特點:
1)支援持久化,可以將記憶體中的資料持久化到磁碟,重啟可以再次從磁碟中載入使用;
2)支援多種資料結構;
3)支援資料的備份:主從模式的備份;
4)高效能,讀速度達 11 萬次/秒,寫速度達到 8.1 萬次/秒
5)支援事務。
2、說說 Redis 的資料型別
一共 8 種
5 種基本資料型別:String、Hash、List、Set、Zset
3 種特殊型別:geospatial、hyperloglog、bitmap
3、Redis 和 Memcache 的區別?
1)Memcache 資料都儲存在記憶體中,斷電即失,資料不能超過記憶體大小;而 Redis 的資料可以持久化到硬碟。
2) Memcache 只支援簡單的字串,Redis 有豐富的資料結構;
3)底層實現方式不一樣,Redis 自行構建了 VM 機制,速度更快。
4、Redis 是單程序單執行緒的?
Redis 將資料放在記憶體中,單執行緒執行最高,多執行緒執行反而需要進行 CPU 上下文切換,這是個耗時操作,單執行緒效率最高。
5、說說 Redis的持久化
Redis 提供了兩種持久化機制:RDB 和 AOF
RDB 持久化機制指的是,用資料集快照的方式記錄 Redis 資料庫的所有鍵值對,在某個時間點寫入一個臨時檔案,持久化結束後,用這個臨時檔案替換上次持久化的檔案,達到資料恢復的目的。
優點:
1)只有一個檔案 dump.rdb 方便持久化;
2)容災性好,一個檔案可以儲存到安全的磁碟;
3)效能最大化,Redis 會單獨建立(fork)一個子程序進行持久化,主程序不進行任何 IO 操作,保證了效能;
4)在資料較多時,比 AOF 的啟動效率高。
缺點:
最後一次持久化的資料可能會丟失。
AOF 持久化,是以獨立日誌的方式記錄每次寫命令,並在 Redis 重啟時重新執行 AOF 檔案中的命令以達到恢復資料的目的。AOF 主要解決資料持久化的實時性。
優點:
1)資料安全,配置 appendfsync 屬性,可以選擇不同的同步策略;
2)自動修復功能, redis-check-aof工具可以解決資料一致性問題;
缺點:
1)AOF 檔案比 RDB 檔案大,且恢復速度慢;
2)資料多時,效率低於 RDB。
6、Redis 的主從複製
主從複製值的是將一臺 Redis 伺服器的資料複製其他 Redis 伺服器,前者稱之為主節點,後者稱之為從節點。
主從複製的作用:
1)資料冗餘:主從複製實現了資料的熱備份;
2)故障修復:當主節點出現故障後,從節點還可以提供服務,實現快速的故障修復。
3)負載均衡:在主從複製的基礎上,配合讀寫分離,可以由主節點提供寫操作,從節點提供讀操作,實現負載均衡,提高併發量;
4)高可用的基石:主從複製是哨兵模式的基礎。
複製原理:
從節點啟動成功連線到主節點後,會發送一個 sync 的同步命令。主節點接收到命令之後,啟動後臺的存檔程序,收集所有修改資料庫的命令,在後臺執行完畢後將整個資料檔案傳送到從節點,完成一次完全同步。
全量複製:從節點在接收到了資料檔案後,將其存檔檔案載入都記憶體中;
增量複製:主節點繼續將新收集到修改命令傳遞給從節點,完成同步。
7、說說哨兵模式
哨兵模式是為了解決手動切換主節點的問題。Redis 提供了哨兵的命令,哨兵是一個獨立的程序。哨兵能夠後臺監控主節點是否故障,如果故障需要將從節點選舉為主節點。
其原理是哨兵通過傳送命令,等待 Redis 伺服器的響應,從而監控多個 Redis 節點。
當只有一個哨兵時,還是可能會出現問題的,比如哨兵自己掛掉。為此,可以使用多哨兵模式,多個哨兵之間相互監控。當主節點宕機了,哨兵1先檢測到這個結果,系統並不會馬上進行 failover 【故障轉移】的過程。僅僅是哨兵1認為主節點不可用的現象稱之為主觀下線。當其餘的哨兵也檢測到主節點不可用之後,哨兵之間會進行一次投票選舉從節點中的一個作為新的主節點,這個過程稱之為客觀下線。
哨兵模式的優點:
1)基於主從複製,高可用;
2)主從可以切換,進行故障轉移,系統可用性好;
3)哨兵模式是主從模式的升級版,手動到自動,更加健壯。
哨兵模式的缺點:
1)不方便線上擴容;
2)實現哨兵模式需要很多的配置。
8、快取穿透、擊穿、雪崩
快取穿透:
概念:使用者需要查詢一個數據,快取中沒有,也就是沒有命中,於是向資料庫中發起請求,發現也沒有。當用戶很多的時候,快取都沒有命中,於是都去請求資料庫,這給資料庫造成很大的壓力。
解決方案:
- 布隆過濾器:是一種資料結構,對所有可能查詢的引數以 hash 方式儲存,先在控制層進行校驗,不符合則丟棄,避免了過多訪問資料庫。
- 快取空物件:當儲存層沒有命中時,即使返回空物件也將其快取起來。(意味著更多的空間儲存,即使設定了過期時間,快取和資料庫還是有段時間資料不一致。)
快取擊穿:
概念:當一個 key 非常熱點時,在不斷扛高併發,集中對這個熱點資料進行訪問,當這個 key 失效的瞬間,請求直接到達資料庫,給資料庫瞬間的高壓力。
解決方案:
- 設定熱點資料永不過期
- 加分散式鎖:保證每個 key 同時只有一個執行緒去查詢後端服務。
快取雪崩:
概念:某個時間段,快取集中失效
解決方案:
- 增加 Redis 叢集的數量
- 限流降級:在快取失效後,通過加鎖和佇列來控制資料庫寫快取的執行緒數量
- 資料預熱:正式部署之前,將資料預先訪問一遍,讓快取失效的時間儘量均勻
9、Redis 的使用場景
1)會話快取:如 單點登入,使用 Redis 模擬 session,SSO 系統生成一個 token,將使用者資訊存到 Redis 中,並設定過期時間;
2)全頁快取
3)作為訊息佇列平臺
4)排行榜和計數器
5)釋出/訂閱:比如聊天系統
6)熱點資料:比如ES中搜索的熱詞
10、Redis 快取如何保持一致性
讀資料的時候首先去 Redis 中讀取,沒有讀到再去 MySQL 中讀取,讀取都資料更新到 Redis 中作為下一次的快取。
寫資料的時候會產生資料不一致的問題。無論是先寫入 Redis 再寫入 MySQL 中,還是先寫入 MySQL 再寫入 Redis 中,這兩步操作都不能保證原子性,所以會出現 Redis 和 MySQL 中資料不一致的問題。
無論採取何種方式都不能保證強一致性,如果對 Redis 中的資料設定了過期時間,能夠保證最終一致性,對架構的優化只能降低發生的概率,不能從根本上避免不一致性。
更新快取的兩種方式:刪除失效快取、更新快取
更新快取和資料庫有兩種順序:先資料庫後快取、先快取後資料庫
兩兩組合,分為四種更新策略。