【1w字+乾貨】第一篇,基礎:讓你的 Redis 不再只是安裝吃灰到解除安裝(Linux環境)

BWH_Steven發表於2021-01-25

Redis 基礎以及進階的兩篇已經全部更新好了,為了字數限制以及閱讀方便,分成兩篇釋出。

本篇主要內容為:NoSQL 引入 Redis ,以及在 Linux7 環境下的安裝,配置,以及總結了非常詳細的型別,用法以及例子,最後通過 Jedis 以及 Springboot 中的 RedisTemplate 在 IDEA 中遠端操作
Redis。

第二篇會主要涉及到配置檔案,釋出訂閱,主從複製,哨兵等一些進階用法的一個基本使用。

一 簡述 NoSQL

Redis是一個使用ANSI C編寫的開源、支援網路、基於記憶體、可選永續性的鍵值對儲存資料庫 ——維基百科

可以簡單的說,Redis就是一款高效能的NoSQL資料庫

(一) 什麼是NoSQL?

我們前面所學習的MySQL資料庫是典型的的SQL資料庫也就是傳統的關係型資料庫,而我們今天學習的Redis資料庫則是一款NoSQL資料庫,也叫作非關係型資料庫,它與我們熟悉的MySQL等的概念完全是不一樣的,它是一項全新的資料庫理念,我們帖一組百度百科的解釋

NoSQL,泛指非關係型的資料庫。隨著網際網路web2.0網站的興起,傳統的關聯式資料庫在處理web2.0網站,特別是超大規模和高併發的SNS型別的web2.0純動態網站已經顯得力不從心,出現了很多難以克服的問題,而非關係型的資料庫則由於其本身的特點得到了非常迅速的發展。NoSQL資料庫的產生就是為了解決大規模資料集合多重資料種類帶來的挑戰,尤其是大資料應用難題 ——百度百科

說明:我們現在所看到的的部落格,RSS,P2P,微博,抖音等均屬於 Web2.0的產物,Web2.0相比較過去的Web1.0更加註重於使用者的互動,使用者不僅可以瀏覽,還可以上傳一些資源到網站上,例如圖片文字或者說短視訊等,使得使用者也參與到了網站內容的製造中去了

(二) 為什麼使用NoSQL?

  • 部署成本低:部署操作簡單,以開源軟體為主

  • 儲存格式豐富:支援 key-value形式、文件、圖片等眾多形式,包括物件或者集合等格式

  • 速度快:資料儲存在快取中,而不是硬碟中,而且例如Redis基於鍵值對,同時不需要經過SQL層解析,效能非常高

  • 無耦合性,易擴充套件

    • 在SQL中,一個正在使用的資料是不允許刪除的,但NoSQL卻可以操作

(三) NoSQL可以替代SQL嗎?

有人會說,NoSQL = Not SQL ,但是我更傾向這樣理解 NoSQL = Not only SQL ,我們不能以一個絕對的結論來判定兩項技術的好壞,每一項技術的產生都有其特定的原因,在我看來,NoSQL更適合作為SQL資料庫的補充,由於海量資料的出現,效能的要求高了起來,而NoSQL這種產物,對於結構簡單但是資料量大的資料處理起來要比傳統的SQL快很多,但是同樣的,其邏輯運算就必須很簡單,否則它也是力不從心的

在我看來,可以簡單的說,NoSQL就是以功能換取效能,但是需要處理複雜的業務邏輯還需要使用關係型資料庫,所以說想要在模型中完全用NoSQL替代SQL是不現實的,兩者更像是互補的關係

SQL的好處:

  1. 支援在一個表以及多表之前進行復雜的查詢操作
  2. 支援對事物的處理,能保證資料的安全要求
  3. 學習成本低,資料較多

市面上的NoSQL產品非常多,我們今天所要介紹的就是其中一款基於鍵值儲存的資料庫——Redis

(四)NoSQL資料庫的四大分類表格分析

分類 Examples舉例 典型應用場景 資料模型 優點 缺點
鍵值(key-value) Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB 內容快取,主要用於處理大量資料的高訪問負載,也用於一些日誌系統等等。 Key 指向 Value 的鍵值對,通常用hash table來實現 查詢速度快 資料無結構化,通常只被當作字串或者二進位制資料
列儲存資料庫 Cassandra, HBase, Riak 分散式的檔案系統 以列簇式儲存,將同一列資料存在一起 查詢速度快,可擴充套件性強,更容易進行分散式擴充套件 功能相對侷限
文件型資料庫 CouchDB, MongoDb Web應用(與Key-Value類似,Value是結構化的,不同的是資料庫能夠了解Value的內容) Key-Value對應的鍵值對,Value為結構化資料 資料結構要求不嚴格,表結構可變,不需要像關係型資料庫一樣需要預先定義表結構 查詢效能不高,而且缺乏統一的查詢語法。
圖形(Graph)資料庫 Neo4J, InfoGrid, Infinite Graph 社交網路,推薦系統等。專注於構建關係圖譜 圖結構 利用圖結構相關演算法。比如最短路徑定址,N度關係查詢等 很多時候需要對整個圖做計算才能得出需要的資訊,而且這種結構不太好做分散式的叢集方案。

二 初識Redis

(一) 什麼是 Redis

我們在一開始提到了,Redis就是一款高效能的NoSQL資料庫,那麼它的應用場景是什麼呢?

  • 用於使用者內容快取,可以處理大量資料的高訪問負載,例如:資料查詢,新聞,商品內容

  • 任務佇列,例如:秒殺,12306

  • 線上好友列表

  • 應用、網站訪問統計排行

由於其基於鍵值儲存,那麼可以支援的儲存的型別有什麼呢?

  • 字串型別 - String

  • 列表 - list:linkedlist

  • 集合 - set

  • 有序集合 - sortedset

  • 雜湊 - hash:map

(二)下載安裝

說明:

推薦使用 Linux 進行部署,所以我們後面也會詳細介紹 Linux 中的安裝配置方式,但是如果只是想快速學習語法,也可以勉強使用 Windows 版本,安裝會簡單很多。

Redis is written in ANSI C and works in most POSIX systems like Linux, *BSD, and OS X, without external dependencies. Linux and OS X are the two operating systems where Redis is developed and tested the most, and we recommend using Linux for deployment . Redis may work in Solaris-derived systems like SmartOS, but the support is best effort. There is no official support for Windows builds.

官網說明地址:https://redis.io/topics/introduction

(1) linux 推薦

官網:https://redis.io(推薦)

  • 訪問可能較慢

中文網:http://www.redis.net.cn

  • 版本有一些滯後,例如官網已經 6.0.9 了,中文網首頁仍掛著 5.0.4

A:下載

# 下載 redis-6.0.9 壓縮包
wget http://download.redis.io/releases/redis-6.0.9.tar.gz 

補充:

B:解壓

一般來說,我們程式都會放在 /opt 目錄下,所以我們先將這個壓縮檔案移動過去再解壓

# 移動此檔案到根目錄下的 opt 目錄中
mv redis-6.0.9.tar.gz /opt
# 解壓此檔案
tar -zxvf redis-6.0.9.tar.gz

解壓後 opt 目錄下就多出一個 redis-6.0.9 的資料夾,我們開啟它,就可以看到一些檔案在其中,其中 redis.conf 是我們一會要用的配置檔案,暫時先不理會

解壓後的檔案貌似也不能執行啊,這是當然的,因為這些檔案還沒有經過編譯和安裝,在編譯之前,首先要檢查一下 GCC 的版本

C:檢查 GCC 版本(Redis 6 以下可以忽略)

如果你選擇的是 Redis 6 以上的版本,例如這裡選擇的 6.0.9,你的 gcc 版本如果太低就會導致後面編譯出錯,最起碼你的 gcc 要到 5.3 的版本以上

如果沒有 gcc 先進行安裝

yum -y install gcc
 
yum -y install gcc-c++

安裝完成後,通過 gcc -v 檢視到安裝到的版本是 4.x.x 版本的,所以要升級,舊版本的 Redis 可以不去做升級這一步

依次執行下面每一條命令

# 升級到gcc 9.3
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils

# scl命令啟用只是臨時的,退出shell或重啟就會恢復原系統gcc版本
scl enable devtoolset-9 bash

# 長期使用 gcc 9.3 還需要進行如下操作
echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile
source /etc/profile

檢視一下更新後的版本

D:編譯安裝

依次執行編譯和安裝,

# 編譯
make

# 安裝
make install

make 會慢一下,耐心等待一下,如果出了錯誤,一般都是 gcc 的問題

安裝後的內容一般都在 /usr/local/bin

E:拷貝配置檔案

我們把原來的配置檔案就放在那個解壓檔案中,我們自己用的,單獨複製一份出來,方便我們操作和更改

我們先去 /usr/local/bin 中建立一個新的資料夾,然後把之前解壓後的資料夾中的 redis.conf 拷貝過來

# 跳轉到指定目錄下
cd /usr/local/bin
# 新建一個資料夾
mkdir myconfig
# 複製 /opt/redis-6.0.9/redis.conf 到 當前目錄的 myconfig 資料夾下
cp /opt/redis-6.0.9/redis.conf myconfig

看一下過程

F:開啟後臺執行

為了保證我們的redis可以後臺執行,我們去編輯拷貝過來的 redis.conf 配置檔案

vim redis.conf

在其中找到 daemonize no

將 no 修改為 yes,儲存退出

G:執行 Redis

下面先執行一下其服務端(保證當前在 usr/local/bin 目錄下)

# 執行服務端
redis-server myconfig/redis.conf
  • 加 myconfig/redis.conf 就是為了制定其啟動使用的配置檔案

接著執行其客戶端

# 執行客戶端
redis-cli -p 6379
  • 因為本身就是本機,所以只需要指定埠就行了,不需要指定ip

可以簡單測試一下,例如 set get 一下,能拿到值就代表成功了

H:關閉服務以及檢查程式是否存在

先看一下執行中時,程式的存在情況

# 檢視redis 程式
ps -ef|grep redis

在客戶端中,可以通過 shutdown 和 exit 執行關閉(這個是在Redis客戶端中執行)

# 關閉
127.0.0.1:6379> shutdown
not connected> exit

# 再次檢視一下程式狀況
[root@centos7 bin]# ps -ef|grep redis

(2) windows 不推薦

我們可以去github中尋找windows版本,不過版本會有所滯後,官方起碼是沒有支援更新的,可能微軟還想著能拽他一把。最新的版本好像也都是好幾年前的了

https://github.com/microsoftarchive/redis/releases

解壓即可用:分別啟動 redis-server.exe 和 redis-cli.exe 就能直接測試使用了嗎,有問題修改redis.windows.conf 配置檔案

  • redis-server.exe:redis伺服器端
  • redis-cli.exe:redis的客戶端
  • redis.windows.conf:配置檔案

三 Redis 通用命令

(一) 開閉命令

(1) 啟動 Redis 服務

redis-server [--port 6379]

有時候引數會過多,建議使用配置檔案啟動

redis-server [xx/redis.conf]

例如:redis-server myconfig/redis.conf

(2) 客戶端連線 Redis

redis-cli [-h 127.0.0.1 -p 6379]

例如 :redis-cli -p 6379

(3) 停止 Redis

在客戶端中(標誌有 127.0.0.1:6379>)直接輸入 shutown 等即可

# 關閉
127.0.0.1:6379> shutdown
not connected> exit

若在目錄中(前面為 $ 等),可以執行

redis-cli shutdown
kill redis-pid

(4) 測試連通性

返回 PONG 即連通了

127.0.0.1:6379> ping
PONG

(二) key 以及通用操作

注:每一種型別的儲存方式是不太一樣的,所以這裡的操作不會講到新增儲存,下面會在每種型別中詳細講解。

只是想簡單先測試,可以先暫時用這幾個命令(這是 String 型別的)

  • 可以使用 set key value 新增

    • set 為命令,key 為鍵,value 為值

    • 例如:set test ideal-20

  • get key 獲取到值

(1) 獲取所有鍵

  • 語法:keys pattern
127.0.0.1:6379> keys *
1) "test"
2) "test2"
  • * 作為萬用字元,表示任意字元,因為其會遍歷所有鍵,然後顯示所有鍵列表,時間複雜度O(n),資料量過大的環境,謹慎使用

(2) 獲取鍵總數

  • 語法:dbsize
127.0.0.1:6379> dbsize
(integer) 2
  • 內部變數儲存此值,執行獲取操作時,非遍歷,因此時間複雜度O(1)

(3) 判斷當前 key 是否存在

  • 語法:exists key [key ...]
127.0.0.1:6379> exists test
(integer) 1
  • 最後返回的是存在的個數

(4) 查詢鍵型別

  • 語法: type key
127.0.0.1:6379> type test
string

(5) 移動鍵

  • 語法:move key db
127.0.0.1:6379> move test2 3
(string) 1
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> keys *
1) "ideal-20-2"
  • 注:Redis 預設有 16 個資料庫 move 代表移動到其中哪個去,然後 select 代表切換到這個資料庫

(6) 刪除鍵

  • 語法:del key [key ...]
127.0.0.1:6379> del test2
(integer) 1

(7) 設定過期時間

  • 秒語法:expire key seconds
  • 毫秒語法:pexpire key milliseconds
127.0.0.1:6379> expire test 120
(integer) 1

(8) 查詢key的生命週期(秒)

  • 秒語法:ttl key
  • 毫秒語法:pttl key
127.0.0.1:6379> ttl test
(integer) 116

(9) 設定永不過期

  • 語法:persist key
127.0.0.1:6379> persist test
(integer) 1
127.0.0.1:6379> ttl test
(integer) -1

(10) 更改鍵的名稱

  • 語法:rename key newkey
127.0.0.1:6379> rename test ideal
OK
127.0.0.1:6379> keys *
1) "ideal"

(11) 清除當前資料庫

  • 語法:flushdb
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)

(12) 清除全部資料庫的內容

  • 語法:flushall
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> 

四 常見支援型別操作

(一) 字串型別 - string

(1) 儲存

  • 語法:set key value [EX seconds] [PX milliseconds] [NX|XX]
    • 後面還可以選擇性的跟隨過期時間
127.0.0.1:6379> set address beijing 5000
OK

(2) 獲取

  • 語法:get key
127.0.0.1:6379> get address
“beijing”

(3) 刪除

  • 語法:del key
127.0.0.1:6379> del address
(string) 1

(4) 遞增或遞減

如果字串中的值為數字型別,可以進行遞增遞減,其它型別會報錯

  • 遞增語法:incr key
  • 遞增語法(指定步長):incrby key step
  • 遞減語法:decr key
  • 遞減語法(指定步長):decrby key step
127.0.0.1:6379> set age 21 
OK
127.0.0.1:6379> incr age # 遞增
(integer) 22 
127.0.0.1:6379> incrby age 5 # 遞增 5
(integer) 27
127.0.0.1:6379> decr age # 遞減
(integer) 26
127.0.0.1:6379> decrby age 5 # 遞減 5
(integer) 21

(5) 追加內容

  • 語法:append key value
127.0.0.1:6379> set ideal hello
OK
127.0.0.1:6379> append ideal ,ideal-20 # 追加內容
(integer) 14
127.0.0.1:6379> get ideal
"hello,ideal-20"

(6) 擷取部分字串

  • 語法:getrange key start end
127.0.0.1:6379> get ideal
"hello,ideal-20"
127.0.0.1:6379> getrange ideal 0 3
"hell"
127.0.0.1:6379> getrange ideal 0 -1
"hello,ideal-20"

(7) 替換部分字串

  • 語法:setrange key start
127.0.0.1:6379> get ideal
"hello,ideal-20"
127.0.0.1:6379> setrange ideal 6 bwh # 從下標為6的位置開始替換
(integer) 14
127.0.0.1:6379> get ideal
"hello,bwhal-20"

(8) 獲取值的長度

  • 語法:strlen key
127.0.0.1:6379> strlen addr1
(integer) 7

(9) 不存在的時候才設定

  • 語法:setnx key value
    • 不存在,則建立
    • 存在,則失敗
127.0.0.1:6379> setnx address guangdong # address鍵 不存在,則建立
(integer) 1
127.0.0.1:6379> get address
"guangdong"
127.0.0.1:6379> setnx address beijing # address鍵 存在,則失敗
(integer) 0
127.0.0.1:6379> get address
"guangdong"

(10) 同時儲存獲取多個值

  • 同時儲存多個值:mset key1 value1 key2 value2 ...
  • 同時獲取多個值:mget key1 key2
  • 同時儲存多個值(保證不存在):msetnx key1 value1 key2 value2 ...
    • 此操作為原子性操作,要失敗全部失敗
127.0.0.1:6379> mset addr1 beijing addr2 guangdong addr3 shanghai # 同時儲存多個值
OK
127.0.0.1:6379> keys *
1) "addr3"
2) "addr2"
3) "addr1"

127.0.0.1:6379> mget addr1 addr2 addr3 # 同時獲取多個值
1) "beijing"
2) "guangdong"
3) "shanghai"

127.0.0.1:6379> msetnx age1 20 age2 25 age3 30 # 第一次同時儲存多個值(保證不存在)
(integer) 1
127.0.0.1:6379> msetnx age4 35 age5 40 age1 45 # 第二次同時儲存多個值(保證不存在),失敗了
(integer) 0
127.0.0.1:6379> 

(11) 設定物件

  • 語法:key value (key 例如:user:1 ,value為一個json字串)
127.0.0.1:6379> set user:1 {name:zhangsan,age:20} # 存一個物件
OK
127.0.0.1:6379> keys *
1) "user:1"
127.0.0.1:6379> get user:1
"{name:zhangsan,age:20}"
  • 以上這種 user:1 的設計在 Redis 中是允許的,例子如下
  • 語法:物件名:{id}:{filed}
127.0.0.1:6379> mset user:1:name lisi user:1:age 25
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "lisi"
2) "25"

(12) 先 get 後 set

  • 語法:getset
    • 先取到原來的值,然後再把新值覆蓋,如果原先沒有值返回 nil
127.0.0.1:6379> getset addr beijing # 原先沒有值,返回 nil
(nil)
127.0.0.1:6379> get addr
"beijing"
127.0.0.1:6379> getset addr guangdong # 原先有值,返回原先的值,然後覆蓋新值
"beijing"
127.0.0.1:6379> get addr
"guangdong"

(二) 列表型別 - list

(1) 新增

A:從左或從右新增元素

  • lpush key value:將元素新增到列表左邊
  • Rpush key value:將元素新增到列表右邊

下面演示新增到左邊的,右邊的是一樣的就不演示了

127.0.0.1:6379> lpush list1 A
(integer) 1
127.0.0.1:6379> lpush list1 B
(integer) 2
127.0.0.1:6379> lpush list1 C
(integer) 3
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "B"
3) "A"

B:插入新值到某個值前後

  • 語法:linsert list before/after value newvalue
127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
3) "C"

127.0.0.1:6379> linsert list1 before C XXX # 在 C 前插入 XXX
(integer) 4

127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
3) "XXX"
4) "C"

(2) 獲取:

A:根據區間獲取值

  • 語法:lrange key start end
127.0.0.1:6379> lrange list1 0 -1 # 獲取所有值
1) "C"
2) "B"
3) "A"
 
127.0.0.1:6379> lrange list1 0 1 # 獲取指定區間的值
1) "C"
2) "B"

B:根據下標獲取值

  • 語法:lindex list 下標
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "B
127.0.0.1:6379> lindex list1 0
"C"
127.0.0.1:6379> lindex list1 1
"B"

C:獲取列表的長度

  • 語法 llen list
127.0.0.1:6379> llen list1
(integer) 1

(3) 刪除

A:移除最左或最右的元素

  • lpop key:刪除列表最左邊的元素,且返回元素
  • rpop key:刪除列表最右邊的元素,且返回元素
127.0.0.1:6379> lrange list1 0 -1
1) "D"
2) "C"
3) "B"
4) "A"

127.0.0.1:6379> lpop list1 # 刪除列表最左邊的元素,且返回元素
"D"
127.0.0.1:6379> rpop list1 # 刪除列表最右邊的元素,且返回元素
"A"

127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "B"

B:移除指定的值

  • 語法:lrem list num value
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "C"
3) "B"
4) "A"

127.0.0.1:6379> lrem list1 1 A # 刪除1個A
(integer) 1
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "C"
3) "B"

127.0.0.1:6379> lrem list1 2 C # 刪除2個C
(integer) 2
127.0.0.1:6379> lrange list1 0 -1
1) "B"
127.0.0.1:6379> 

C:移除最後一個元素且新增到另一個list

  • rpoplpush list1 list2
127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
3) "C"

127.0.0.1:6379> rpoplpush list1 list2 # 移除 list1 中最後一個元素,且新增到list2 中去
"C"

127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
127.0.0.1:6379> lrange list2 0 -1
1) "C"

(4) 根據下標範圍擷取 list

  • 語法:ltrim list start end
127.0.0.1:6379> rpush list1 A
(integer) 1
127.0.0.1:6379> rpush list1 B
(integer) 2
127.0.0.1:6379> rpush list1 C
(integer) 3
127.0.0.1:6379> rpush list1 D
(integer) 4

127.0.0.1:6379> ltrim list1 1 2 # 擷取下標為1到2的值
OK

127.0.0.1:6379> lrange list1 0 -1
1) "B"
2) "C"

(5) 替換指定下標的值

語法:lset list 下標 value

127.0.0.1:6379> exists list1 # 判斷是否存在此list
(integer) 0
127.0.0.1:6379> lset list1 0 beijing # 不存在,替換報錯
(error) ERR no such key

127.0.0.1:6379> lpush list1 guangdong # 建立一個list
(integer) 1
127.0.0.1:6379> lindex list1 0
"guangdong"

127.0.0.1:6379> lset list1 0 beijing # 存在,替換成功
OK
127.0.0.1:6379> lindex list1 0
"beijing"

(三) 集合型別 - set

set:一種無序(不保證有序)集合,且元素不能重複

(1) 新增

  • 語法:sadd key value
127.0.0.1:6379> sadd set1 A
(integer) 1
127.0.0.1:6379> sadd set1 B
(integer) 1
127.0.0.1:6379> sadd set1 C
(integer) 1
127.0.0.1:6379> sadd set1 C # set的值不能重複
(integer) 0
127.0.0.1:6379> smembers set1 # 查詢指定set的所有值,亂序
1) "B"
2) "A"
3) "C"

(2) 獲取

A:獲取set集合中的所有元素

  • 語法:smembers key
127.0.0.1:6379> smesmbers set1 # 查詢指定set的所有值,亂序
1) "B"
2) "A"
3) "C"

B:獲取元素的個數

  • 語法:scard set
127.0.0.1:6379> scard set1
(integer) 3

C:隨機獲取元素

  • 語法:sembers set [num]
    • 預設獲取一個隨機元素,後跟數字,代表隨機獲取幾個元素
127.0.0.1:6379> smembers set1
1) "D"
2) "B"
3) "A"
4) "C"

127.0.0.1:6379> srandmember set1 # 獲取一個隨機元素
"D"
127.0.0.1:6379> srandmember set1 # 獲取一個隨機元素
"B"

127.0.0.1:6379> srandmember set1 2 # 獲取兩個隨機元素
1) "A"
2) "D"

(3) 刪除

A:刪除set集合中某元素

  • 語法:srem key value
127.0.0.1:6379> srem set1 C # 刪除 C 這個元素
(integer) 1

127.0.0.1:6379> smembers set1
1) "B"
2) "A"

B:隨機刪除一個元素

  • 語法:spop set
127.0.0.1:6379> smembers set1
1) "D"
2) "B"
3) "A"
4) "C"
127.0.0.1:6379> spop set1 # 隨機刪除一個元素
"A"
127.0.0.1:6379> spop set1 # 隨機刪除一個元素
"B"
127.0.0.1:6379> smembers set1
1) "D"
2) "C"

(4) 移動指定值到另一個set

  • 語法:smove set1 set2 value
127.0.0.1:6379> smembers set1
1) "D"
2) "C"

127.0.0.1:6379> smove set1 set2 D # 從 set1 移動 D 到 set2
(integer) 1

127.0.0.1:6379> smembers set1
1) "C" 
127.0.0.1:6379> smembers set2
1) "D"

(5) 交集 並集 差集

  • sinter set1 set2:交集
  • sunion set1 set2:並集
  • sdiff set1 set2:差集
127.0.0.1:6379> sadd set1 A
(integer) 1
127.0.0.1:6379> sadd set1 B
(integer) 1
127.0.0.1:6379> sadd set1 C
(integer) 1

127.0.0.1:6379> sadd set2 B
(integer) 1
127.0.0.1:6379> sadd set2 C
(integer) 1
127.0.0.1:6379> sadd set2 D
(integer) 1
127.0.0.1:6379> sadd set2 E
(integer) 1

127.0.0.1:6379> sinter set1 set2 # 交集
1) "B"
2) "C"
127.0.0.1:6379> sunion set1 set2 # 並集
1) "D"
2) "E"
3) "C"
4) "B"
5) "A"
127.0.0.1:6379> sdiff set1 set2 # 差集
1) "A"

(四) 有序集合型別 - sortedset/zset

此型別和 set 一樣也是 string 型別元素的集合,且不允許重複的元素

不同的是每個元素都會關聯一個double型別的分數,redis正是通過分數來為集合中的成員進行從小到大的排序

有序集合的成員是唯一,但分數(score)卻可以重複

(1) 新增

  • 語法:zadd key score value [score value ... ...]
127.0.0.1:6379> zadd sortedset1 20 zhangsan # 新增一個
(integer) 1
127.0.0.1:6379> zadd sortedset1 10 lisi 60 wangwu # 新增多個
(integer) 2

(2) 獲取

A:獲取所有值(預設排序)

  • 語法:zrange sortedset start end [withscores]
    • 根據那個值的大小進行了排序,例如上面的 10 20 60
127.0.0.1:6379> zrange sortedset1 0 -1
1) "lisi"
2) "zhangsan"
3) "wangwu"

B:獲取所有值(從小到大和從大到小)

  • zrangebyscore sortedset -inf +inf:從小到大
  • zrevrange sortedset 0 -1:從大到小
127.0.0.1:6379> zrangebyscore sortedset1 -inf +inf # 從小到大
1) "lisi"
2) "zhangsan"
3) "wangwu"

127.0.0.1:6379> zrevrange sortedset1 0 -1 # 從大到小
1) "wangwu"
2) "zhangsan"
3) "lisi"

C:獲取值且附帶數值

  • zrangebyscore sortedset -inf +inf withscores:從小到大且附帶值
127.0.0.1:6379> zrangebyscore sortedset1 -inf +inf withscores # 顯示從小到大且附帶值
1) "lisi"
2) "10"
3) "zhangsan"
4) "20"
5) "wangwu"
6) "60"

127.0.0.1:6379> zrangebyscore sortedset1 -inf 20 withscores # 顯示從小到大,且數值小於20的
1) "lisi"
2) "10"
3) "zhangsan"
4) "20"
127.0.0.1:6379> 

D:獲取有序集合中的個數

  • 語法:zcard sortedset
127.0.0.1:6379> zcard sortedset1
(integer) 2

E:獲取指定區間成員數量

  • 語法:zcount sortedset start end (strat 和 end是指那個數值,而不是什麼下標)
127.0.0.1:6379> zcount sortedset1 10 60
(integer) 3

(2) 刪除

  • zrem key value
127.0.0.1:6379> zrange sortedset1 0 -1
1) "lisi"
2) "zhangsan"
3) "wangwu"

127.0.0.1:6379> zrem sortedset1 wangwu # 刪除 wangwu 這個元素
(integer) 1

127.0.0.1:6379> zrange sortedset1 0 -1
1) "lisi"
2) "zhangsan"

(五) 雜湊型別 - hash

(1) 新增

A:普通新增

  • 語法:hset hash field value
127.0.0.1:6379> hset hash1 username admin
(integer) 1
127.0.0.1:6379> hset hash1 password admin
(integer) 1

B:不存在才可以新增

  • 語法:hsetnx hash filed value
127.0.0.1:6379> hsetnx hash1 username admin888 # 已存在,失敗
(integer) 0

127.0.0.1:6379> hsetnx hash1 code 666 # 不存在,成功
(integer) 1

(2) 獲取

A:獲取指定的field對應的值

  • 語法:hget hash field [ key field ... ...]
127.0.0.1:6379> hget hash1 password
"admin"

B:獲取所有的field和value

  • 語法:hgetall hash
127.0.0.1:6379> hgetall hash1
1) "username"
2) "admin"
3) "password"
4) "admin"

C:獲取 hash 的欄位數量

  • 語法:hlen hash
127.0.0.1:6379> hlen hash1
(integer) 2

D:只獲取所有 field 或 value

  • hkeys hash:獲取所有 field 欄位
  • hvals hash:獲取所有 value 值
127.0.0.1:6379> hkeys hash1 # 獲取所有 field 欄位
1) "username"
2) "password"

127.0.0.1:6379> hvals hash1 # 獲取所有 value 值
1) "admin"
2) "admin"

(3) 刪除

  • 語法:hdel hash field
127.0.0.1:6379> hdel hash1 username
(integer) 1

(4) 自增自減

  • hincrby hash field 增量
127.0.0.1:6379> hsetnx hash1 code 666
(integer) 1
127.0.0.1:6379> hincrby hash1 code 2
(integer) 668
127.0.0.1:6379> hincrby hash1 code -68
(integer) 600

五 三種特殊資料型別

(一) Geospatial(地理位置)

使用經緯度,作為地理座標,然後儲存到一個有序集合 zset/sortedset 中去儲存,所以 zset 中的命令也是可以使用的

  • 特別是需要刪除一個位置時,沒有GEODEL命令,是因為你可以用ZREM來刪除一個元素(其結構就是一個有序結構)
  • Geospatial 這個型別可以用來實現儲存城市座標,一般都不是自己錄入,因為城市資料都是固定的,所以都是通過 Java 直接匯入的,下面都是一些例子而已
  • Geospatial 還可以用來實現附近的人這種概念,每一個位置就是人當前的經緯度,還有一些能夠測量距離等等的方法,後面都會提到

命令列表:

(1) 儲存經緯度

  • 語法:geoadd key longitud latitude member [..]

    • longitud——經度、 latitude——緯度

    • 有效的經度從-180度到180度。

    • 有效的緯度從-85.05112878度到85.05112878度。

127.0.0.1:6379> geoadd china:city 116.413384 39.910925 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 113.271431 23.135336 guangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 113.582555 22.276565 zhuhai
(integer) 1
127.0.0.1:6379> geoadd china:city 112.556391 37.876989 taiyuan
(integer) 1

(2) 獲取集合中一個或者多個成員的座標

  • 語法:geopos key member [member..]
127.0.0.1:6379> geopos china:city beijing zhuhai
1) 1) "116.41338318586349487"
   2) "39.9109247398676743"
2) 1) "116.41338318586349487"
   2) "39.9109247398676743"

(3) 返回兩個給定位置之間的距離

  • 語法:geodist key member1 member2 [unit]

    • 單位預設為米,可以修改,跟在 member 後即可,例如 km

    • 指定單位的引數 unit 必須是以下單位的其中一個:

      • m 表示單位為米

      • km 表示單位為千米

      • mi 表示單位為英里

      • ft 表示單位為英尺

127.0.0.1:6379> geodist china:city guangzhou taiyuan
"1641074.3783"
127.0.0.1:6379> geodist china:city guangzhou taiyuan km
"1641.0744"

(4) 查詢附近的元素(給定經緯度和長度)

  • 含義:以給定的經緯度為中心, 返回集合包含的位置元素當中

  • 語法:georadius key longitude latitude radius m|km|mi|ft [WITHCOORD][WITHDIST] [WITHHASH] [COUNT count]

    • 與中心的距離不超過給定最大距離的所有位置元素

    • 通過georadius就可以完成 附近的人功能(例如這個位置我們輸入的是人當前的位置)

      • withcoord:帶上座標

      • withdist:帶上距離,單位與半徑單位相同

      • count :只顯示前n個(按距離遞增排序)

127.0.0.1:6379> georadius china:city 113.582555 22.276565 500 km
1) "zhuhai"
2) "guangzhou"
127.0.0.1:6379> georadius china:city 113.582555 22.276565 500 km withdist withcoord count 1
1) 1) "zhuhai"
   2) "0.0002"
   3) 1) "113.58255296945571899"
      2) "22.27656546780746538"
      
127.0.0.1:6379> georadius china:city 113.582555 22.276565 500 km withdist withcoord count 2
1) 1) "zhuhai"
   2) "0.0002"
   3) 1) "113.58255296945571899"
      2) "22.27656546780746538"
2) 1) "guangzhou"
   2) "100.7111"
   3) 1) "113.27143281698226929"
      2) "23.13533660075498233"

(5) 查詢附近的元素(指定已有成員和長度)

  • 含義:5 與 4 相同,給定的不是經緯度而是集合中的已有成員

  • 語法:GEORADIUSBYMEMBER key member radius...

127.0.0.1:6379> georadiusbymember china:city zhuhai 500 km
1) "zhuhai"
2) "guangzhou"

(6) 返回一個或多個位置元素的Geohash表示

  • 語法:geohash key member1 [member2..]
127.0.0.1:6379> geohash china:city zhuhai
1) "weby8xk63k0"

(二) Hyperloglog(基數統計)

HyperLogLog 是用來做基數(資料集中不重複的元素的個數)統計的演算法,其底層使用string資料型別

HyperLogLog 的優點是:

  • 在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、並且是很小的
    • 花費 12 KB 記憶體,就可以計算接近 2^64 個不同元素的基數

因為 HyperLogLog 只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素

一個常見的例子:

  • 傳統實現,儲存使用者的id,然後每次進行比較。當使用者變多之後這種方式及其浪費空間,而我們的目的只是計數,Hyperloglog就能幫助我們利用最小的空間完成。

(1) 新增

含義:新增指定元素到 HyperLogLog 中

語法:PFADD key element1 [elememt2..]

127.0.0.1:6379> pfadd test1 A B C D E F G
(integer) 1
127.0.0.1:6379> pfadd test2 C C C D E F G
(integer) 1

(2) 估算myelx的基數

含義:返回給定 HyperLogLog 的基數估算值

語法:PFCOUNT key [key]

127.0.0.1:6379> pfcount test1
(integer) 7
127.0.0.1:6379> pfcount test2
(integer) 5

(3) 合併

含義:將多個 HyperLogLog 合併為一個 HyperLogLog

語法:PFMERGE destkey sourcekey [sourcekey..]

127.0.0.1:6379> pfmerge test test1 test2
OK
127.0.0.1:6379> pfcount test
(integer) 9

(三) BitMaps(點陣圖)

BitMaps 使用位儲存,資訊狀態只有 0 和 1

  • Bitma p是一串連續的2進位制數字(0或1),每一位所在的位置為偏移(offset),在bitmap上可執行AND,OR,XOR,NOT以及其它位操作
  • 這種型別的應用場景很多,例如統計員工是否打卡,或者登陸未登入,活躍不活躍,都可以考慮使用此型別

(1) 設定值

  • 含義:為指定key的offset位設定值
  • 語法:setbit key offset value
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 0
(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 1
(integer) 0

(2) 獲取

  • 含義:獲取offset位的值
  • 語法:getbit key offset
127.0.0.1:6379> getbit sign 4
(integer) 1
127.0.0.1:6379> getbit sign 2
(integer) 0

(3) 統計

  • 含義:統計字串被設定為1的bit數,也可以指定統計範圍按位元組
  • 語法:bitcount key [start end]
127.0.0.1:6379> bitcount sign
(integer) 4

六 事務

(一) 定義

定義:Redis 事務的本質是一組命令的集合

  • 事務支援一次執行多個命令,一個事務中所有命令都會被序列化
  • 在事務執行過程,會按照順序序列化執行佇列中的命令,其他客戶端提交的命令請求不會插入到事務執行命令序列中

即:redis事務就是一次性、順序性、排他性的執行一個佇列中的一系列命令

首先

(二) 特點

(1)不保證原子性

可能受到關係型資料庫的影響,大家會將事務一概而論的認為資料庫中的事務都是原子性的,但其實,Redis 中的事務是非原子性的

原子性:所有的操作要麼全做完,要麼就全不做,事務必須是一個最小的整體,即像化學組中的原子,是構成物質的最小單位。

  • 資料庫中的某個事務中要更新 t1表、t2表的某條記錄,當事務提交,t1、t2兩個表都被更新,只要其中一個表操作失敗,事務就會回滾

非原子性:與原子性反之

  • 資料庫中的某個事務中要更新 t1表、t2表的某條記錄,當事務提交,t1、t2兩個表都被更新,若其中一個表操作失敗,另一個表操作繼續,事務不會回滾

(2) 不支援事務回滾

多數事務失敗是由語法錯誤或者資料結構型別錯誤導致的,而語法的錯誤是在命令入隊前就進行檢測,而型別錯誤是在執行時檢測的,Redis為提升效能而採用這種簡單的事務

(3) 事務沒有隔離級別的概念

批量操作在傳送 EXEC 命令前被放入佇列快取,並不會被實際執行,也就不存在事務內的查詢要看到事務裡的更新,事務外查詢不能看到

(三) 相關命令

(1) 開啟事務

含義:開啟事務,下一步就會將內容逐個放入佇列中去,然後通過 exec 命令,原子化的執行

命令:multi

由於 multi 和 exec 需要搭配使用,所以在第二點一起演示示例

(2) 執行事務

含義:執行事務中的所有操作

命令:exec

A:正常開啟且執行一個事務

首先先存一個 k1 和 k2,開啟事務後,對這兩個值進行修改,然後將這個事務執行,最後發現兩個 OK ,然後用 get 檢視一下值的變化

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v11 # 修改 k1
QUEUED
127.0.0.1:6379> set k2 v22 # 修改 k2
QUEUED
127.0.0.1:6379> exec # 執行事務
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"

B:語法錯誤導致的事務失敗

語法錯誤(編譯器錯誤)導致的事務失敗,會使得保持原值,例如下文事務中,修改 k1 沒問題,但是修改 k2 的時候出現了語法錯誤,set 寫成了 sett ,執行 exec 也會出錯,最後發現 k1 和 k2 的值都沒有變

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v11 # 修改正常
QUEUED
127.0.0.1:6379> sett k2 v22 # 修改有語法錯誤
(error) ERR unknown command `sett`, with args beginning with: `k2`, `v22`, 
127.0.0.1:6379> exec # 執行事務
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"

C:型別錯誤導致的事務失敗

型別錯誤(執行時錯誤)導致的事務異常,例如下面 k2 被當做了一個 list 處理,這樣在執行時就會報錯,最後事務提交失敗,但是並不會回滾,結果就是 k1 修改成功,而 k2 失敗

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> lpush k2 v22 # 型別錯誤
QUEUED
127.0.0.1:6379> exec # 執行事務
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v2"

(3) 取消事務

這個沒什麼好說的,就是取消執行

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 k11
QUEUED
127.0.0.1:6379> set k2 k22
QUEUED
127.0.0.1:6379> discard
OK

(4) watch 監控

Redis的命令是原子性的,而事務是非原子性的,通過 watch 這個命令可以實現 Redis 具有回滾的一個效果

做法就是,在 multi 之前先用 watch 監控某些鍵值對,然後繼續開啟以及執行事務

  • 如果 exec 執行事務時,這些被監控的鍵值對沒發生改變,它會執行事務佇列中的命令
  • 如果 exec 執行事務時,被監控的鍵值對發生了變化,則將不會執行事務中的任何命令,然後取消事務中的操作

我們監控 k1,然後再事務開啟之前修改了 k1,又想在事務中修改 k1 ,可以看到最後結果中,事務中的操作就都沒有執行了

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> watch k1 # 監控 k1
OK
127.0.0.1:6379> set k1 v111111 # k1 被修改了
OK
127.0.0.1:6379> multi # 開啟事務
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec # 執行事務
(nil)
127.0.0.1:6379> get k1
"v111111"
127.0.0.1:6379> get k2
"v2"

(5) unwatch 取消監控

取消後,就可以正常執行了

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> watch k1 # 監控
OK
127.0.0.1:6379> set k1 v111111
OK
127.0.0.1:6379> unwatch # 取消監控
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"

七 IDEA 中使用 Jedis 操作 Redis

Jedis is a blazingly small and sane Redis java client.

Jedis was conceived to be EASY to use.

Jedis 是一款可以讓我們在java中操作redis資料庫的工具,下載其jar包,或者引入到 maven 中即可,使用還是非常簡單的

(一) 引入依賴和編碼

我這裡建立了一個空專案,然後建立一個普通的 maven 模組用來演示 jedis

首先引入 jedis 依賴,後面需要所以還引入了fastjson

版本自己去 maven 中去查就可以了,因為我們 linux 中安裝的 redis 是一個新的版本,所以我們依賴也用了最新的

<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.4.0</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.75</version>
    </dependency>
</dependencies>

建立測試類

  • 如果是本機,例如 windows 下,啟動 win 下的 redis 服務,然後用 127.0.0.1 訪問
  • 如果是遠端機器,虛擬機器或者雲伺服器,使用對應 ip 訪問
  • new Jedis 時空構造代表預設值 "localhost", 6379埠
public class Demo01 {
    public static void main(String[] args) {
        // 遠端 linux(虛擬機器)
        Jedis jedis = new Jedis("192.168.122.1", 6379);
        // 測一下是否連通
        System.out.println(jedis.ping());
    }
}

(二) 連線 linux 的操作步驟

如果直接輸入ip和埠,會連線不上,所以需要先做以下操作

① 保證 6379 埠開放

以 centos 7.9 為例,其他版本例如 6.x 可以查一下具體命令,以及防火牆是不是給攔截了,只要保證埠能訪問到就可以了

  • 開啟6379埠
firewall-cmd --zone=public --add-port=6379/tcp --permanent
# 顯示 
succss
  • 重啟防火牆
firewall-cmd --reload
# 顯示
success
  • 檢查埠是否開啟
firewall-cmd --query-port=6379/tcp
# 顯示
yes
  • 重啟 redis 服務
[root@centos7 bin]# redis-cli shutdown
[root@centos7 bin]# redis-server myconfig/redis.conf 

你還可以在 window 的機器上,使用 telnet 192.168.122.1 6379 的方式測試一下是否能訪問到,如果出現錯誤,請檢查埠和防火牆的問題

② 修改 redis 配置檔案

  • 註釋繫結的ip地址(註釋掉 bind 127.0.0.1 這一句)

  • 設定保護模式為 no(protected-mode 把 yes 改為 no)

    • Linux上的 redis 處於安全保護模式,這就讓你無法從虛擬機器外部去輕鬆建立連線,所以在 redis.conf 中設定保護模式(protected-mode)為 no

再次在 IDEA 中訪問,就可以訪問到了

(三) 常見 API

(1) 字串型別 - String

// 儲存
jedis.set("address","beijing");

// 獲取
String address = jedis.get("address");

// 關閉連線
jedis.close();

補充:setex() 方法可以儲存資料,並且指定過期時間

// 將aaa-bbb存入,且10秒後過期
jedis.setex("aaa",10,"bbb")

(2) 列表型別 - list

// 儲存
jedis.lpush("listDemo","zhangsan","lisi","wangwu");//從左
jedis.rpush("listDemo","zhangsan","lisi","wangwu");//從右
	
// 獲取
List<String> mylist = jedis.lrange("listDemo", 0, -1);
	        
// 刪除,並且返回元素
String e1 = jedis.lpop("listDemo");//從左
String e2 = jedis.rpop("listDemo");//從右

// 關閉連線
jedis.close();

(3) 集合型別 - set

// 儲存
jedis.sadd("setDemo","zhangsan","lisi","wangwu");

// 獲取
Set<String> setDemo = jedis.smembers("setDemo");

// 關閉連線
jedis.close();

(4) 有序集合型別 - sortedset/zset

// 儲存
jedis.zadd("sortedsetDemo",20,"zhangsan");
jedis.zadd("sortedsetDemo",10,"lisi");
jedis.zadd("sortedsetDemo",60,"wangwu");
	
// 獲取
Set<String> sortedsetDemo = jedis.zrange("sortedsetDemo", 0, -1);

// 關閉連線
jedis.close();

(5) 雜湊型別 - hash

// 儲存
jedis.hset("hashDemo","name","lisi");
jedis.hset("hashDemo","age","20");
	
// 獲取
String name = jedis.hget("hashDemo", "name");

// 獲取所有資料
Map<String, String> user = jedis.hgetAll("hashDemo");
	
Set<String> keySet = user.keySet();
for (String key : keySet) {
	//獲取value
	String value = user.get(key);
	System.out.println(key + ":" + value);
}

// 關閉連線
jedis.close();

(四) Jedis 執行事務

public class Demo01 {
    public static void main(String[] args) {
        // 遠端 linux(虛擬機器)
        Jedis jedis = new Jedis("192.168.122.1", 6379);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name", "zhangsan");
        jsonObject.put("age", "21");

        // 開啟事務
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();

        try {
            multi.set("userA", result);
            multi.set("userB", result);
            // 執行事務
            multi.exec();
        } catch (Exception e) {
            // 放棄事務
            multi.discard();
        } finally {
            System.out.println(jedis.get("userA"));
            System.out.println(jedis.get("userB"));
            // 關閉連線
            jedis.close();
        }
    }
}

為了將結果顯示出來,在關閉前新增兩句輸出語句

執行結果:

{"name":"zhangsan","age":"21"}
{"name":"zhangsan","age":"21"}

(五) Jedis 連線池

為什麼我們要使用連線池呢?

我們要使用Jedis,必須建立連線,我們每一次進行資料互動的時候,都需要建立連線,Jedis雖然具有較高的效能,但建立連線卻需要花費較多的時間,如果使用連線池則可以同時在客戶端建立多個連線並且不釋放,連線的時候只需要通過一定的方式獲取已經建立的連線,用完則歸還到連線池,這樣時間就大大的節省了

下面就是我們直接建立了一個連線池,不過我們使用時一般都會封裝一個工具類

@Test
public void testJedisPool(){
    // 0.建立一個配置物件
    JedisPoolConfig config = new JedisPoolConfig();
    config.setMaxTotal(50);
    config.setMaxIdle(10);

    // 1.建立Jedis連線池物件
    JedisPool jedisPool = new JedisPool(config,"192.168.122.1",6379);

    // 2.獲取連線
    Jedis jedis = jedisPool.getResource();
    
    // 3. 儲存
    jedis.set("name","zhangsan");
    // 4. 輸出結果
    System.out.println(jedis.get("name"));
    
    //5. 關閉 歸還到連線池中
    jedis.close();;
}

(一) 連線池工具類

直接使用工具類就可以了

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * JedisPool工具類
 * 載入配置檔案,配置連線池的引數
 * 提供獲取連線的方法
 */
public class JedisPoolUtils {

    private static JedisPool jedisPool;

    static {
        //讀取配置檔案
        InputStream is = JedisPoolUtils.class.getClassLoader().getResourceAsStream("jedis.properties");
        //建立Properties物件
        Properties pro = new Properties();
        //關聯檔案
        try {
            pro.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //獲取資料,設定到JedisPoolConfig中
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(Integer.parseInt(pro.getProperty("maxTotal")));
        config.setMaxIdle(Integer.parseInt(pro.getProperty("maxIdle")));

        //初始化JedisPool
        jedisPool = new JedisPool(config, pro.getProperty("host"), Integer.parseInt(pro.getProperty("port")));
    }


    /**
     * 獲取連線方法
     */
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

別忘了配置檔案

host=192.168.122.1
port=6379
maxTotal=50
maxIdle=100

呼叫程式碼

@Test
public void testJedisPoolUtil(){

    // 0. 通過連線池工具類獲取
    Jedis jedis = JedisPoolUtils.getJedis();

    // 1. 使用
    jedis.set("name","lisi");
    System.out.println(jedis.get("name"));

    // 2. 關閉 歸還到連線池中
    jedis.close();;

}

八 SpringBoot 整合 Redis

(一) 簡單使用(存在序列化問題)

① 建立 Springboot專案或模組,引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

可以看到,這個官方的 starter 引入了 Redis,但是並沒有引入 Jedis,而是引入了 Lettuce

Jedis:採用的直連,多個執行緒操作的話,是不安全的。如果要避免不安全,使用jedis pool連線池!更像BIO模式

Lettuce:採用netty,例項可以在多個執行緒中共享,不存線上程不安全的情況!可以減少執行緒資料了,更像NIO模式

② 編寫配置檔案

# 配置redis
spring.redis.host=192.168.122.1
spring.redis.port=6379

③ 測試程式碼

@SpringBootTest
class Redis02BootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("name","zhangsan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
}

執行結果:

zhangsan

(二) 使用自定義 RedisTemplate 模板(推薦)

上述操作,在 IDEA 中的結果肯定是沒問題的,但是我們去 Linux 中去看一下 Redis 的內容,卻發現 key 都是亂碼,例如儲存的 name 卻變成了如下內容

127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04name"

這就是序列化的問題,下面我們會分析這個問題,這裡先給出解決方案,即自定義 RedisTemplate 模板

① 自定義 RedisConfig 類

@Configuration
public class RedisConfig {

    /**
     * 自定義 RedisTemplate 怒ban
     *
     * @param factory
     * @return
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 為開發方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);

        // Json序列化配置
        Jackson2JsonRedisSerializer 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.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

② 呼叫

@SpringBootTest
class Redis02BootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        redisTemplate.opsForValue().set("address","beijing");
        System.out.println(redisTemplate.opsForValue().get("address"));
    }
}

我們分別儲存了 name2 和 address 這兩個key,去終端中檢視一下

127.0.0.1:6379> keys *
1) "address"
2) "\xac\xed\x00\x05t\x00\x04name"
3) "name2"

可以看到,問題被解決了

(三) 封裝一個工具類

我們操作追求的是便捷,而每次都是用一大堆 redisTemplate.opsForValue().xxxxx 很長的命令,所以封裝一個工具類能更加事半功倍,具體工具類我就不在這裡貼了,因為太長了,後面我會傳到 github上去然後更新連結,當然了百度上其實一搜一大把

例如下面,我們使用工具類後就可以很簡單的使用封裝 redisTemplate 後的方法了

    @Autowired
    private RedisUtil redisUtil;

    @Test
    void contextLoads() {
        redisUtil.set("address2", "zhuhai");
        System.out.println(redisUtil.get("address2"));

    }

(四) 簡單分析原理

這一塊的簡單分析,主要想要弄清楚四個內容

  • ① 是不是不再使用 Jedis 了,而是使用 Lettuce
  • ② 檢視配置檔案的配置屬性
  • ③ 如何操作 Redis
  • ④ 序列化問題(即儲存亂碼問題)

在以前 Springboot 的文章中,關於自動配置的原理中可知,整合一個內容的時候,都會有一個自動配置類,然後再 spring.factories 中能找到它的完全限定類名

進入 spring.factories,查詢關於 redis 的自動裝配類

進入 RedisAutoConfiguration 類後,在註解中就能看到 RedisProperties 類的存在,這很顯然是關於配置檔案的類

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
// 這個註解!!!
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    // ...... 省略
}

進入 RedisProperties 類

alt + 7 可以在 IDEA 中檢視到這個類中的屬性方法等,這裡檢視一下其屬性

例如地址埠,超時時間等等配置都清楚了,例如我們當時的配置檔案是這樣配的

# 配置redis
spring.redis.host=192.168.122.1
spring.redis.port=6379

繼續回到類,檢視下面一個註解,發現有兩個類

  • LettuceConnectionConfiguration

  • JedisConnectionConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
// 這個註解!!!
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    // ...... 省略
}

先來進入 Jedis 這個看看,GenericObjectPool 和 Jedis 這兩個內容都是預設缺失的,所以是不會生效的

再來看看 LettuceConnectionConfiguration,是沒有問題的,所以確實現在預設的實現都是使用 Lettuce 了

繼續回到 RedisAutoConfiguration

其 Bean 只有兩個

  • RedisTemplate
  • StringRedisTemplate

這種 XxxTemplate ,例如 JdbcTemplate、RestTemplate 等等都是通過 Template 來操作這些元件,所以這裡的兩個 Template 也是這也昂,分別用來操作 Redis 和 Redis 的 String 資料型別(因為 String 型別很常用)

// 註解略
public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}
}

特別注意這一個註解

@ConditionalOnMissingBean(name = "redisTemplate")

它的意思就是說如果沒有自定義,就預設使用這個,這也就是告訴我們,我們可以自己自定義Template,來覆蓋掉預設的,前面的使用中我們知道,使用預設的 Template 會涉及到亂碼,也就是序列化問題

因為在網路中傳輸的物件需要序列化,否則就是亂碼

我們進入預設的 RedisTemplate 看看

首先看到的就是一些關於序列化的引數

往下看,可以看到預設的序列化方式使用的是 Jdk 序列化,而我們自定義中使用的是 Json 序列化

而預設的RedisTemplate中的所有序列化器都是使用這個序列化器

RedisSerializer 中為我們提供了多種序列化方式

所以後來我們就自定了 RedisTemplate 模板,重新定義了各種型別的序列化方式,這也是我們推薦的做法

相關文章