【主流技術】Redis 在 Spring 框架中的實踐

2022-07-11 09:00:14

前言

在Java Spring 專案中,資料與遠端資料庫的頻繁互動對伺服器的記憶體消耗比較大,而 Redis 的特性可以有效解決這樣的問題。

Redis 的幾個特性:

  1. Redis 以記憶體作為資料儲存媒介,讀寫資料的效率極高;
  2. Redis 支援 key-value 等多種資料結構,提供字串,雜湊,列表,佇列,集合結構直接存取於記憶體,可持久化(RDB 和 AOF);
  3. 支援主從模式,可以設定叢集。

下面,我將和大家一起學習分享 Redis 的相關知識。

一、Redis 概述

1.1Redis 是什麼?

Redis(Remote Dictionary Server ),即遠端字典服務,是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的紀錄檔型、Key-Value 資料庫,並提供多種語言的API。

Redis會週期性地把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了 master-slave (主從)同步。

1.2Redis 能做什麼?

Redis適用的幾個場景:

  1. 資料庫;
  2. 對談快取;
  3. 訊息佇列;
  4. 釋出、訂閱訊息;
  5. 載入商品列表、評論等。

1.3基礎知識

  • Redis 預設有16個資料庫,在組態檔中表明瞭 database 16。預設使用第0個,且可以使用 select 進行切換資料庫。
[root@localhost bin]# redis-cli -p 6379    #進入使用者端
127.0.0.1:6379> 
127.0.0.1:6379> select 3    #選擇資料庫
OK
127.0.0.1:6379[3]> dbsize    #檢視資料庫大小
(integer) 0
  • Redis 的執行緒相關知識

Redis 在6.0版本以後開始支援多執行緒,Redis 是基於記憶體操作,CPU 效能不是 Redis 的瓶頸,Redis 的效能取決機器的記憶體和網路頻寬是否良好。

Redis 預設是關閉多執行緒的,即預設只使用單執行緒。

為什麼 Redis 是單執行緒還這麼快?

  1. 誤區1:高效能的伺服器一定是多執行緒的?
  2. 誤區2:多執行緒(CPU排程,上下文切換)一定比單執行緒的效率高?

原因:Redis 所有的資料都是存放在記憶體中的,單執行緒的操作沒有上下文的切換,從而達到很高的效率。


二、Redis 安裝與基本命令

2.1Windows 安裝

  • 方式一

GitHub 地址:Release 5.0.10 · redis/redis · GitHub

解壓縮後:雙擊 redis-server.exe 即可在 Windows 上開啟服務。

  • 方式二

在 Redis 的安裝目錄切換到 cmd,執行:redis-server.exe redis.windows.conf;

在保持上一個視窗開啟的情況下,再次在 Redis 目錄下開啟另一個 cmd 視窗,執行:redis-cli.exe -h 127.0.0.1 -p 6379;

當輸入 ping 時,返回 PONG 代表服務建立成功。

注:雖然在 Windows 上使用很方便,但官方推薦在 Linux 上使用 Redis 。

2.2Linux 安裝

官網下載最新:Download | Redis redis-7.0.2.tar.gz

放入 Linux 資料夾中,比如可以放入 home/dingding 目錄下後解壓:

tar -zxvf redis-5.0.8.tar.gz

可以進入解壓後的檔案後:

看到組態檔:redis.conf,上述操作只是解壓操作,真正安裝往下看:

安裝 Redis :

1.還是在上述解壓目錄中,首先安裝 gcc-c++:

yum install gcc-c++

2.make 設定好所有的環境:

make

3.檢查 make 後是否成功安裝所需環境:

make install

安裝好後的預設路徑為:/usr/local/bin,進入後可以看到 redis-cli、redis-server 等內容。

4.將之前 redis 解壓目錄資料夾(home/dingding)中的 redis.conf 檔案複製到 redis 的安裝目錄(/usr/local/bin)下:

cp /opt/redis-7.0.2/redis.conf bin

注:或者可以在 bin目錄中新建一個 config 資料夾:mkdir config,上述命令則變為:

cp /opt/redis-7.0.2/redis.conf config

5.Redis 不是預設後臺啟動的,當前 /bin 目錄下應該有了複製過來的 redis.conf檔案,此時需要修改設定資訊:

vim redis.conf

將 daemonize no 改為 daemonize yes 即可保證 Redis 在後臺時可以執行:

遠端連線 Redis 服務:將 bind 127.0.0.1註釋掉,同時*protected-mode yes* 改成 protected-mode no

6.在 bin 目錄下,啟動 Redis 服務:需要用指定檔案下的 redis.conf 檔案來啟動:

redis-server bin/redis.conf

或者:

redis-server config/redis.conf

7.在 bin 目錄下,進入 redis client 終端,連線預設的埠號:

redis-cli -p 6379

8.在 任意 目錄下,確定 Redis 服務是否開啟:

ps -ef|grep redis

9.在 任意 目錄下,關閉 Redis 服務:

shutdown
exit

2.3 Redis-benchmark 效能測試

Redis-benchmark 是 Redis 中自帶的效能測試外掛,可能通過一系列的命令來測試 Redis 的效能。

這些命令都在 Redis 的 Linux 安裝目錄下進行。

  • 測試100個並行連線,進行100000個請求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

Redis-BenchMark 測試結果

測試結果解讀:

100000次請求在4.84秒內完成,共100個並行使用者端,平均每秒完成27000+次請求,每次寫入3個位元組,以上都是在1臺伺服器上完成。


三、Redis 之 String 型別

3.1 Redis-key 詳解

127.0.0.1:6379> flushall         //清空資料庫
OK
127.0.0.1:6379> set name zzz     //設定key為name,value為zzz
OK
127.0.0.1:6379> set age 3        //設定key為age,值為3
OK
127.0.0.1:6379> keys *           //檢視所有的 key
1) "name"
2) "age" 
127.0.0.1:6379> exists name      //檢查是否存在 名為 name 的 key
(integer) 1                      // 1 表示存在
127.0.0.1:6379> exists name1
(integer) 0
127.0.0.1:6379> move name 1      //移除 name
(integer) 1                      // 1 表示移除成功
(1.16s)
127.0.0.1:6379> keys *           //檢視所有的 key
1) "age"
127.0.0.1:6379> expire name 10   //設定 name 的過期時間為 10 秒鐘,單點登入可以使用 Redis 的過期功能
(integer) 1
127.0.0.1:6379> ttl name         //檢視過期時間,已經過期2秒鐘
(integer) -2
127.0.0.1:6379> get name         //此時 name 已不存在 
(nil)
127.0.0.1:6379> type name        //檢視 key 中所儲存的 value 的資料型別
string                           // string 型別
127.0.0.1:6379> type age         //檢視 key 中所儲存的 value 的資料型別
string                           // string 型別

Redis 所有命令可以在:Commands | Redis 中去檢視。

3.2 String 型別

由於在 Linux 環境中是使用終端命令來對 Redis 進行一些操作的,所以下面通過對一些 String 型別的命令的操作來進行講解。

127.0.0.1:6379> set key1 v1     //設定 key-value
OK
127.0.0.1:6379> get key1        //通過 get-key 來獲取 value
"v1"
127.0.0.1:6379> append key1 "hello"     //追加字串,如果 key 不存在,就相當於重新 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> append key1 "+apluemxa"     //追加字串
(integer) 16
127.0.0.1:6379> get key1
"v1hello+apluemxa"

下面再對 Redis 中一些現有的自增、自減、設定步長操作進行講解:

127.0.0.1:6379> set views 0      //設定 key 為0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views       //自增
(integer) 1
127.0.0.1:6379> incr views
(integer) 5
127.0.0.1:6379> get views
"5"
127.0.0.1:6379> incrby views 20   //設定步長,並指定增量
(integer) 23
127.0.0.1:6379> get views
"23"
127.0.0.1:6379> DECRBY views 8    //設定步長,並指定減量
(integer) 15

下面是對字串範圍 range 做一個講解:

127.0.0.1:6379> set key1 "helle,world!"       //設定 key 以及 value
OK
127.0.0.1:6379> get key1         //通過 key 獲取 value
"helle,world!"
127.0.0.1:6379> GETRANGE key1 0 3       //擷取字串 [0,3](陣列下標)
"hell"
127.0.0.1:6379> GETRANGE key1 0 -1      //表示擷取所有的字串,與 get key 作用一致
"helle,world!"
127.0.0.1:6379> GETRANGE key1 2 6       //擷取字串 [2,6](陣列下標)
"lle,w"
127.0.0.1:6379> SETRANGE key1 2 xx      //替換字串為 xx,2為陣列下標
(integer) 12
127.0.0.1:6379> get key1         //檢視被替換後的字串
"hexxe,world!"

下面再對設定過期時間、判斷是否存在(分散式鎖中使用)進行講解:

127.0.0.1:6379> setex key3 30 "hello"     //設定 key 的 value 為30,過期時間為30秒
OK
127.0.0.1:6379> ttl key3
(integer) 20
127.0.0.1:6379> setnx mykey "redis"      //如果 mykey 不存在則建立
(integer) 1
(0.55s)
127.0.0.1:6379> keys *
1) "mykey"
2) "key1"
127.0.0.1:6379> setnx mykey "MySQL"      //如果 mykey 已存在,則提示建立失敗,返回0
(integer) 0

下面再對 Redis 的批次操作命令進行講解:

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3     //同時建立多個 key-value
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3        //同時獲取多個 value
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       // 因為上述的 k1 v1 已經存在,所以上述命令執行失敗,k4 沒有建立成功
(nil)

最後再介紹幾個 Redis 的高階實戰用法:

set user:1{name:zhuzqc,age:123}   //設定一個 user:1 物件,其 value 使用一個 json 字串來儲存

127.0.0.1:6379> mset user:1:name zhuzqc user:1:age 123   //同時為 user 物件設定 key為 name 和 age,並設定value
OK
127.0.0.1:6379> mget user:1:name user:1:age   //通過 user 物件的 key,獲取對應的 value
1) "zhuzqc"
2) "123"
127.0.0.1:6379> getset db redis     //先 get 後 set,若沒有值則返回 nil
(nil)
127.0.0.1:6379> get db      //上述命令中已 set 了值
"redis"
127.0.0.1:6379> getset db mysql     //同理先 get 到 db 的 value
"redis"
127.0.0.1:6379> get db      //再重新 set db 的 value
"mysql"

3.3小結

本節講解的是 Redis 中關於 String 型別資料型別的基本命令以及進階用法,主要包括:

  1. 計數器(自增,設定步長)

    如:某網站的瀏覽量統計

    uid:1234:views:0 incr views    //使用者id、瀏覽量、瀏覽量自增
    
  2. 物件儲存

    如:過期時間、是否存在(分散式鎖)

  3. 批次操作

    如:同時建立多個 key-value、先設定 key 再設定 value


四、Redis 使用場景(拓展)

4.1Redis 基本事務操作

前提:Redis 單條命令保證原子性,但是 Redis 的事務不保證原子性。

比如在關係型資料庫 MySQL 中,事務是具有原子性的,所有的命令都是一起成功或者失敗。

  • Redis 事務的本質:
  1. 一組命令一起執行的集合(事務不保證原子性);
  2. 一個事務中的所有命令都會按照順序執行;
  3. 其它3大特性:一次性、順序性和排他性;
  4. Redis 事務沒有隔離級別的概念,所有命令只有發起執行時才會被執行(exec);
  • Redis 的執行過程:
  1. 開啟事務(multi);
  2. 命令入隊(按照順序執行);
  3. 執行事務(exec)。

基本操作過程如下:

127.0.0.1:6379> multi        #redis開啟事務
OK
127.0.0.1:6379(TX)> FLUSHDB       #命令開始入隊
QUEUED 
127.0.0.1:6379(TX)> keys *
QUEUED 
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec       #執行事務,上述命令開始按順序執行並給出展示執行結果
1) OK
2) (empty array)
3) OK
4) OK
5) "v2"

編譯型異常(程式碼有問題或者命令有錯):事務中所有的命令都不會被執行;

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3 v33    #正確的 getset 語句
QUEUED
127.0.0.1:6379(TX)> getset k3     #錯誤的語句:編譯時(未執行)發生錯誤
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec      #事務中所有的命令都不會執行
(error) EXECABORT Transaction discarded because of previous errors.

執行時異常:如果事務佇列中存在語法性錯誤,那麼執行事務時,其它命令可以正常執行,錯誤的命令會丟擲異常。

127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> INCR k1     #對字串的 value 使用自增,語法不報錯
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec     #執行事務後,只有錯的命令未被執行,其餘命令都被執行了
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v3"

4.2Redis 實現樂觀鎖

悲觀鎖

  • 簡單來說,就是什麼時候都會出問題,無論做什麼都會加鎖!

樂觀鎖

  • 很樂觀,認為無論什麼時候都不會出問題,所以不會上鎖!
  • 在資料進行更新的期間,會對資料進行判斷,期間這個資料是否被修改(如 mysql 中的 version)。
  • 步驟一:獲取 version;
  • 步驟二:更新的時候比較 version。

Redis 監視(watch)測試

  • 正常步驟執行的過程:
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> set cost 0
OK
127.0.0.1:6379> watch money     #監視 money 物件
OK 
127.0.0.1:6379> multi      #開啟事務,資料正常變動
OK
127.0.0.1:6379(TX)> DECRBY money 28
QUEUED
127.0.0.1:6379(TX)> INCRBY cost 28
QUEUED
127.0.0.1:6379(TX)> EXEC     #執行後得到正常的資料 
1) (integer) 972
2) (integer) 28
  • 多執行緒情況下對值進行更改:
127.0.0.1:6379> watch money       #使用 watch 進行樂觀鎖操作 
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> DECRBY money 100     #另一個執行緒操作
QUEUED
127.0.0.1:6379(TX)> INCRBY money 100
QUEUED
127.0.0.1:6379(TX)> exec      #執行失敗
(nil)
#########################################  解決辦法  ##########################################
127.0.0.1:6379> unwatch     #先解鎖
OK
127.0.0.1:6379> watch money      #再次監視,獲取最新的值
OK
127.0.0.1:6379> multi     #啟用事務
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY cost 10
QUEUED
127.0.0.1:6379(TX)> exec     #執行成功
1) (integer) 480
2) (integer) 48

4.3 Jedis 相關操作

什麼是 Jedis?

Jedis 是官方推薦的 Java 連線開發工具,本質上是一個 Java 操作 Redis 的中介軟體。

如果要使用 Java 操作 Redis ,那麼應該要先熟悉 Jedis 的一些操作。

  • 匯入依賴
     <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>4.2.3</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.83</version>
            </dependency>
    
  • 編碼測試
    1. 連線資料庫(Rdies 可以看作是一種資料庫)
    2. 操作命令
    3. 斷開連線
/**
 * @author Created by zhuzqc on 2022/6/20 23:04
 */
public class TestPing {
    public static void main(String[] args) {
        //1、new 一個 Jedis 物件
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //2、所有 Jedis 命令都在 jedis 物件中去操作
        System.out.println(jedis.ping());
    }
}
  • 常用 API
    1. 關於 String

      /**
       * @author Created by zhuzqc on 2022/6/20 23:15
       */
      public class TestAPI {
          public static void main(String[] args) {
              Jedis jedis = new Jedis("127.0.0.1",6379);
              //新增 key-value
              jedis.set("name","zhuzqc");
              jedis.set("age","123");
              //使用 append 新增字串
              jedis.append("name","-alumna");
              //批次增加、獲取 key-value
              jedis.mset("k1","v1","k2","v2");
              jedis.mget("k1","k2");
              //分散式鎖的是否存在、設定過期時間
              jedis.setnx("k1","v1");
              jedis.setex("k2",10,"v2");
              //先 get 後 set
              jedis.getSet("k2","vv22");
              //擷取 k2 的字串
              System.out.println(jedis.getrange("name", 2, 5));
          }
      }
      
    2. 常用方法

      public Long hlen(String key) {
              this.checkIsInMultiOrPipeline();
              this.client.hlen(key);
              return this.client.getIntegerReply();
          }
      
          public Set<String> hkeys(String key) {
              this.checkIsInMultiOrPipeline();
              this.client.hkeys(key);
              return (Set)BuilderFactory.STRING_SET.build(this.client.getBinaryMultiBulkReply());
          }
      
          public List<String> hvals(String key) {
              this.checkIsInMultiOrPipeline();
              this.client.hvals(key);
              List<String> lresult = this.client.getMultiBulkReply();
              return lresult;
          }
      
          public Map<String, String> hgetAll(String key) {
              this.checkIsInMultiOrPipeline();
              this.client.hgetAll(key);
              return (Map)BuilderFactory.STRING_MAP.build(this.client.getBinaryMultiBulkReply());
          }
      
  • 關於事務
    /**
     * @author Created by zhuzqc on 2022/6/20 23:32
     */
    public class TestTX {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("127.0.0.1",6379);
            //自定義 JSON 資料
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("name","Jackson");
            jsonObject.put("age","123");
            //強轉為 String
            String result = jsonObject.toString();
            try {
                //開啟事務
                Transaction multi = jedis.multi();
                multi.set("user1",result);
                //程式碼丟擲異常則事務都不執行
                int i = 1/0;
                multi.set("user2",result);
                //執行事務
                multi.exec();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //執行時報錯,不影響其它事務執行
                System.out.println(jedis.get("user3"));
                System.out.println(jedis.get("user2"));
                System.out.println(jedis.get("user1"));
                jedis.close();
            }
        }
    }
    

五、Spring Boot 整合 Redis

5.1基本概念與操作

在 Spring Boot 中運算元據的框架(元件)一般有:Spring-data、JPA、JDBC、Redis等。

說明:在Spring 2.x 版本後,原來使用的 Jedis 被替換成為了 lettuce。

原因:

  • Jedis:採用直連的方式,多個執行緒操作是不安全的,使用 Jedis pool 連線池可以避免執行緒不安全。(BIO模式
  • lettuce:採用netty,範例可以在多個執行緒中進行共用,不存線上程不安全的情況,可以減少執行緒數量,效能較高。(NIO模式

原始碼解讀:

    @Bean
    //自己可定義一個 redisTemplate 來替換預設的
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    //兩個泛型都是Object,使用時需要強轉為<String,Object>
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

匯入依賴:

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

組態檔:

# Redis 設定
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0

簡單測試:

  /**
     * 獲取某個專案中會使用到的 accessToken
     * @return accessToken
     */
    public String getAccessToken() throws Exception {

        String accessToken = null;
        Jedis jedis = null;
        try {
            // 如果 redis 物件中已經存在 accessToken,則直接返回
            jedis =jedisPool.getResource();
            accessToken = jedis.get("accessToken:" + appKey);
            if (StringUtils.isNotBlank(accessToken)){
                return accessToken;
            }

            // 獲取 accessToken 所必須的引數
            Client client = createClient();
            GetAccessTokenRequest getAccessTokenRequest = new GetAccessTokenRequest()
                    .setAppKey(appKey)
                    .setAppSecret(appSecret);
            GetAccessTokenResponse res = client.getAccessToken(getAccessTokenRequest);

            //獲取到 accessToken 後將其以 K-V 的形式存入 redis 物件中,並設定過期時間為30分鐘
            accessToken = res.getBody().getAccessToken();
            jedis.set("accessToken:" + appKey, accessToken);
            jedis.expire("accessToken:" + appKey, 1800);

        // 異常捕獲
        } catch (TeaException err) {
            if (!com.aliyun.teautil.Common.empty(err.code) &&              
                !com.aliyun.teautil.Common.empty(err.message)) {
                log.error("code:{}, message:{}", err.code, err.message);
            }
        } catch (Exception _err) {
            TeaException err = new TeaException(_err.getMessage(), _err);
            if (!com.aliyun.teautil.Common.empty(err.code) && 
                !com.aliyun.teautil.Common.empty(err.message)) {
                log.error("code:{}, message:{}", err.code, err.message);
            }
        }
        //釋放 redisPool 資源池中的 redis 物件資源,保證 redis 物件數量夠用
        finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return accessToken;
    }

自定義 Redis 設定類:

/**
 * @author Created by zhuzqc on 2022/6/21 22:27
 */
@Configuration
public class RedisConfig {
    /**
     * 自定義 redisTemplate
     * */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        //設定具體的序列化方式,如 JSON 的序列化
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(om);
        //設定 String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //設定 key 的時候使用 String 序列化
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        //設定 value 的時候使用 JSON 序列化
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);

        template.setConnectionFactory(redisConnectionFactory);
        template.afterPropertiesSet();
        return template;
    }
}

使用 Redis 工具類:

    @Test
    public void Test_1(){
        redisUtil.set("nnaammee","zzzzzz");
        System.out.println(redisUtil.get("nnaammee"));
    }

5.2Redis 持久化操作

Redis 是基於記憶體的資料庫,如果不將記憶體中的資料庫狀態儲存到磁碟,那麼一旦伺服器程序退出,伺服器中的資料庫狀態也會消失,所以 Redis 提供了資料持久化的能力 。

5.2.1RDB(Redis DataBase)

RDB 是 Redis 預設的持久化方案。在指定的時間間隔內,執行指定次數的寫操作,則會將記憶體中的資料寫入到磁碟中(觸發SnapShot 快照)。即在指定目錄下生成一個dump.rdb檔案。Redis 重啟會通過載入dump.rdb檔案恢復資料。

組態檔(原理)

#   save ""
#   save 指定時間間隔 <執行指定次數>
save 900 1
save 300 10
save 60 10000

分析:save <指定時間間隔> <執行指定次數更新操作>

滿足條件就將記憶體中的資料同步到硬碟中:

官方出廠設定預設是 900秒內有1個更改、00秒內有10個更改、60秒內有10000個更改,則將記憶體中的資料快照寫入磁碟。

# 指定本地資料庫檔名,一般採用預設的 dump.rdb
dbfilename dump.rdb

# 預設開啟資料壓縮
rdbcompression yes

觸發機制

  1. 在指定的時間間隔內,執行指定次數的寫操作;
  2. 執行save(阻塞, 只管儲存快照,其他的等待) 或者是bgsave (非同步)命令;
  3. 執行flushall 命令,清空資料庫所有資料(不推薦);
  4. 執行shutdown 命令,保證伺服器正常關閉且不丟失任何資料。

RDB 資料恢復

將上述dump.rdb 檔案拷貝到redis的安裝目錄的bin目錄下,重啟redis服務即可恢復資料。

RDB 優缺點

優點:

  1. 適合大規模的資料恢復;
  2. 如果對資料完整性和一致性要求不高,RDB是很好的選擇。

缺點:

  1. 由於 RDB 可能在最後一次備份時遇到伺服器宕機的情況,這時資料的完整性和一致性不高;
  2. 由於 Redis 在備份時會獨立建立一個子程序將資料寫入臨時檔案,再將臨時檔案替換為需要備份的檔案時,需要佔用雙份的記憶體。
5.2.2AOF(Append Only File)

Redis 預設不開啟 AOF。

AOF 的出現是為了彌補 RDB 的不足(資料的不一致性),所以它採用紀錄檔的形式來記錄每個寫操作,並追加到檔案中。

Redis 重啟的會根據紀錄檔檔案的內容將寫指令從前到後執行一次以完成資料的恢復工作。

組態檔(原理)

開啟 redis.conf 檔案,找到 APPEND ONLY MODE 對應內容:

# redis 預設關閉,開啟需要手動把 no 改為 yes
appendonly yes

# 指定本地資料庫檔名,預設值為 appendonly.aof
appendfilename "appendonly.aof"

# 指定更新紀錄檔條件
# appendfsync always    # 同步持久化,每次發生資料變化會立刻寫入到磁碟中。(效能較差,但資料完整性比較好)
appendfsync everysec    # 預設每秒非同步記錄一次
# appendfsync no    # 不同步

# 設定重寫觸發機制
auto-aof-rewrite-percentage 100    #當AOF檔案大小是上次rewrite後大小的一倍
auto-aof-rewrite-min-size 64mb    #且檔案大於64M時觸發。(一般都設定為2G,64M太小)

AOF 資料恢復

正常情況下,將 appendonly.aof 檔案拷貝到 redis 的安裝目錄的 bin 目錄下,重啟redis服務即可。

注:如果因為某些原因導致 appendonly.aof 檔案格式異常,從而導致資料還原失敗,可以通過命令 redis-check-aof --fix appendonly.aof 進行修復。

AOF 重寫機制

當 AOF 檔案的大小超過所設定的閾值時,Redis 就會對 AOF 檔案的內容壓縮。

Redis 會 fork 出一條新程序,讀取記憶體中的資料,並重新寫到一個臨時檔案中。並沒有讀取舊檔案,最後替換舊的 AOF 檔案。

5.3 Redis 叢集(主從複製)

5.3.1基本概念

主從複製:是指將一臺 Redis 伺服器端的資料,複製到其它 Redis 伺服器的過程。

讀寫分離:一般專案中的業務資料 80% 的情況下都是在進行讀的操作,多個從節點進行讀操作,可以減輕 Redis 伺服器壓力。

前者稱為主節點(Master/laeder),後者稱為從節點(slave/follower),最低數量標準為「一主二從」。

在這個過程中資料的複製是單向的,資料只能從主節點複製到從節點。

其中 Master 節點以寫為主,Slave 節點以讀為主。

預設情況下每臺 Redis 都是主節點,一個主節點可以有多個從節點(也可以無從節點),但每個從節點只能有唯一的主節點。

5.3.2主要作用
  1. 資料備份:主從複製實現了資料的熱備份(系統處於正常運轉狀態下的備份),是除 Redis 持久化外的另一種方式。
  2. 故障恢復:主節點出現故障時,可以由從節點提供服務,實現資料的快速恢復。
  3. 負載均衡:在主從複製的基礎上配合讀寫分離,可以分擔伺服器負載,尤其是在讀多寫少的情況下,大大提高了 Redis 伺服器的並行量。
  4. 高可用(叢集):主從複製和哨兵模式是叢集能夠實施的基礎。
5.3.3環境設定

叢集的設定只設定從庫、不用設定主庫。

檢查 Redis 伺服器資訊:

127.0.0.1:6379> info replication
# Replication
role:master     # 當前角色預設是 Master
connected_slaves:0     # 當前從機為 0 個
master_failover_state:no-failover
master_replid:e87ac50f5b4f345c1a2a8f3a7374b25ef109219f
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

修改 conf 檔案中以下幾處:

  1. 埠(每個從節點的埠需要不同);
  2. pid file的名稱;
  3. log 檔名;
  4. dump.rdb 名稱;
5.3.4主從設定

設定原則:從機認定一個主機即可。

如:一主(79)二從(80、81):

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379    #認79為主機
OK
127.0.0.1:6380> info replication
# Replication
role:slave     #當前角色為從機
master_host:127.0.0.1
master_port:6379
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379   #認79為主機
OK
127.0.0.1:6381> 
127.0.0.1:6381> info replication
# Replication
role:slave     #當前角色為從機
master_host:127.0.0.1
master_port:6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2    # 該主機有兩個從機
slave0:ip=127.0.0.1,port=6380,state=online,offset=350,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=350,lag=1

注意:在 Redis 伺服器的組態檔中對主、從進行設定才能真正生效,上述命令設定是臨時的。

5.3.5測試主從複製
# 主節點可以讀也可以寫
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"

# 從節點可以獲取到主節點的資料
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6380> get k1
"v1"

# 從節點只允許讀,不允許寫
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica.

# 主節點宕機後,從節點仍然可以獲取到主節點資料
127.0.0.1:6379> shutdown
127.0.0.1:6380> get key2
"shutdown"
5.3.6複製原理

Slave 啟動連線成功到 Master 後會傳送一個 sync 的同步命令,Master 接收到命令後將會傳送整個資料檔案到 Slave,並完成一次完全同步。

5.4小結

  • Redis 是一個高效能的 key-value 儲存系統。
  • 它支援儲存的 value 型別包括:string (字串)、list (連結串列)、set (集合)、zset (sorted set 有序集合)和 hash(雜湊型別)。
  • Redis 提供了Java、Python、Ruby、Erlang、PHP等使用者端,使用起來很很方便。
  • Redis 支援支援持久化、主從同步:資料可以從主伺服器向任意數量的從伺服器上同步,保證 Redis 伺服器的高可用。