1. 程式人生 > 實用技巧 >Spring boot 論壇專案實戰_04

Spring boot 論壇專案實戰_04

Redis, 一站式高興能儲存方案

1. Redis 入門

  • Redis 是一款 基於鍵值對 的NoSQL 資料庫, 它的值支援多種資料結構:

    • 字串(Strings)、雜湊(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等

  • Redis 將所有的資料都存放在記憶體中,所以它的讀寫效能十分驚人。同時,Redis還可以將記憶體中的資料以快照或日誌的形式儲存到硬碟上,以保證資料的安全性

  • Redis 典型的應用場景包括:快取、排行榜、計數器、社交網路、訊息佇列等。

  • 參考網站:

  • Redis 常用命令:

    • 清空當前資料:flushdb

    • 字串(Strings):

      • 儲存: set key【字元拼接用“ : ”】 value

      • 獲取: get key【字元拼接用“ : ”】

      • 數字加:incr key,返回 integer

      • 數字減:decr key,返回 integer

        • 加減運算只能是 整數型別 integer

    • 雜湊(hashes):

      • 儲存:hset key【字元拼接用“ : ”】 field【hash 的 key】 value

      • 獲取:hget key【字元拼接用“ : ”】 field

    • 列表(lists):【可以理解為一個橫向的陣列】

      • 根據其存取 可左可右,可分別實現 佇列 和 棧 的特性

      • 進:可左可右

      • 左進

        • 左存多個數值: lpush key value [value ...]

        • 檢視當前列表長度: llen key

        • 檢視指定 key 的 list 裡面第 index 個值:lindex key index

        • 檢視指定範圍的值:lrange key start stop

      • 出:可左可右

      • 指定右出: rpop key

    • 集合(sets):

      • 存入集合元素:sadd key member [member ...]

      • 統計集合元素:scard key

      • 隨機彈出元素:spop key 【可用於抽獎】

      • 檢視集合所剩元素:smembers key

    • 有序集合(sorted sets):

      • 新增元素:zadd key [NX|XX] [CH] [INCR] score member [score member ...] 【分數 元素名】

      • 查詢某一個值的分數:zscore key member

      • 返回某個值的排名:zrank key member 【預設由小到大,從 0 開始】

      • 取某個範圍的資料:zrange key start stop [WITHSCORES]

    • 全域性:keys pattern

      • 檢視當前所有的 key :keys *

      • 檢視所有 test 開頭的 key:keys test*

      • 檢視某個 key 的型別:type key

      • 檢視某個 key 是否存在:EXISTS key [key ...]

      • 刪除某 key:del key [key ...]

      • 設定 某 key 的過期時間【到期自動刪除】:expire key seconds

2. Spring 整合 Redis

  • 引入依賴

    • spring-boot-starter-data-redis

  • 配置Redis

  • Spring 預設配置的 k-v 中 k:Object

    實際開放常用的 k: String 型別

    所以這裡需要自己再手動配置

    • 配置資料庫引數

    • 編寫配置類,構造RedisTemplate

      • // 基於 Spring 框架的
        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.RedisSerializer;
        ​
        @Configuration
        public class RedisConfig {
        ​
            // 接入連線工廠, 才能建立物件
            @Bean
            public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
                // 方法的例項化
                RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
                // 給物件設定連線工廠
                template.setConnectionFactory(factory);
        ​
                // 設定 key 的序列化方式, 引數是 spring 框架的 redis 下的
                // 返回一個能夠序列化字串的序列化器
                template.setKeySerializer(RedisSerializer.string());
        ​
                // 設定 value 的序列化方式
                template.setValueSerializer(RedisSerializer.json());
        ​
                // 設定 hash 的 key 的序列化方式
                template.setHashKeySerializer(RedisSerializer.string());
        ​
                // 設定 hash 的 value 的序列化方式
                template.setHashValueSerializer(RedisSerializer.json());
        ​
                // 觸發設定結束後 生效
                template.afterPropertiesSet();
                return template;
            }
        ​
        }
  • 訪問Redis

    • redisTemplate.opsForValue()

      • @Test
        public void testStrings(){
            String redisKey = "test:count";
        ​
            // String 型別的值
            redisTemplate.opsForValue().set(redisKey,1);
        ​
            System.out.println(redisTemplate.opsForValue().get(redisKey));
            System.out.println(redisTemplate.opsForValue().increment(redisKey));
            System.out.println(redisTemplate.opsForValue().decrement(redisKey));
        }

    • redisTemplate.opsForHash()

      • // 訪問 hash
            @Test
            public void  testHashes(){
                String redisKey = "test:user";
                redisTemplate.opsForHash().put(redisKey,"id",1);
                redisTemplate.opsForHash().put(redisKey,"username","張三");
        ​
                System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
                System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
        ​
            }

    • redisTemplate.opsForList()

      • @Test
            public void testLists(){
                // 左進列表
                String redisKey = "test:ids";
        ​
                // 放入資料
                redisTemplate.opsForList().leftPush(redisKey,101);
                redisTemplate.opsForList().leftPush(redisKey,102);
                redisTemplate.opsForList().leftPush(redisKey,103);
        ​
                // 取出資料
                // 統計當前元素個數
                System.out.println(redisTemplate.opsForList().size(redisKey));
                // 獲取指定位置的元素, 獲取某個索引所對應的元素
                System.out.println(redisTemplate.opsForList().index(redisKey,0));
                // 按照 索引範圍獲取元素
                System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));
                // 彈出元素, 左出
                System.out.println(redisTemplate.opsForList().leftPop(redisKey));
                System.out.println(redisTemplate.opsForList().leftPop(redisKey));
                System.out.println(redisTemplate.opsForList().leftPop(redisKey));
            }

    • redisTemplate.opsForSet()

      • @Test
            public void  testSets(){
                String redisKey = "test:teachers";
        ​
                redisTemplate.opsForSet().add(redisKey,"劉備","關羽","張飛","趙雲","孔明");
        ​
                // 統計元素個數
                System.out.println(redisTemplate.opsForSet().size(redisKey));
                // 彈出一個數據, 隨機彈出
                System.out.println(redisTemplate.opsForSet().pop(redisKey));
                // 統計現在集合中的資料都是什麼, 展示現在集合中所有元素
                System.out.println(redisTemplate.opsForSet().members(redisKey));
                
            }

    • redisTemplate.opsForZSet()

      • @Test
            public void testSortedSets(){
                String redisKey = "test:students";
        ​
                // 新增 元素, 及其對應的分數
                redisTemplate.opsForZSet().add(redisKey,"唐僧",80);
                redisTemplate.opsForZSet().add(redisKey,"悟空",90);
                redisTemplate.opsForZSet().add(redisKey,"八戒",50);
                redisTemplate.opsForZSet().add(redisKey,"沙僧",70);
                redisTemplate.opsForZSet().add(redisKey,"龍馬",60);
        ​
                // 統計元素總數
                System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
                // 查詢某個元素的分數
                System.out.println(redisTemplate.opsForZSet().score(redisKey,"八戒"));
                // 查詢某個元素的排名, 預設由小到大
                System.out.println(redisTemplate.opsForZSet().rank(redisKey,"八戒"));
                // 倒敘排名, 按分數由大到小
                System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey,"八戒"));
                // 從小到大取前三
                System.out.println(redisTemplate.opsForZSet().range(redisKey,0,2));
                // 從大到小取前三
                System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey,0,2));
                
            }
    • 對 key 的訪問

      • // 訪問公共型別 key
            @Test
            public void testKeys(){
        ​
                // 程式中一般不會用 keys * 這個命令, 這個命令一般是直接查詢
        ​
                redisTemplate.delete("test:user");
        ​
                // 判斷 key 是否存在
                System.out.println(redisTemplate.hasKey("test:user"));
        ​
                // 設定 key 的過期時間
                // TimeUnit. 設定時間 單位: 日, 時, 分, 秒, 毫秒...
                redisTemplate.expire("test:students",10, TimeUnit.SECONDS);
            }

    • 多次訪問同一個key , 可採用繫結 key 的方式,來避免複寫 redisKey:

      • // 多次訪問同一個 key
            @Test
            public void testBoundOperations(){
                String redisKey = "test:count";
        ​
                // Bound XXX Operations: XXX : 你具體要訪問的資料型別, 這裡是String
                // redisTemplate.bound XXX Ops : 繫結具體的資料型別, 這裡是String
                BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
                // 這樣就是綁定了 key 不用每次都寫了.
                operations.increment();
                operations.increment();
                operations.increment();
                operations.increment();
                operations.increment();
                System.out.println(operations.get());
        ​
            }

Redis 的事務

Redis 是 非關係型資料庫, 所以不用嚴格遵守 ACID

在實際生產環境中, 建議用程式設計式事務實現, 宣告式只能針對方法整體不能 對單獨程式碼行控制

// 程式設計式事務
@Test
    public void testTransactional() {
        Object obj = redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                String redisKey = "test:tx";
                // 啟用事務
                operations.multi();
​
                operations.opsForSet().add(redisKey,"張三");
                operations.opsForSet().add(redisKey,"李四");
                operations.opsForSet().add(redisKey,"王五");
​
                // 事務過程是不能允許讀的
                System.out.println(operations.opsForSet().members(redisKey));
                //operations.exec() : 提交事務
                return operations.exec();
            }
        });
​
        System.out.println(obj);
    }

  • 列印結果可以看出:

    • 第一次 查詢結構為空, 保證了事務的原子性

    • 第二次列印結果: 前面三個 “1”: 每次操作影響的元素個數; 後面就是這個集合內的具體元素

3. 點贊

將點贊資料存入 Redis 提升效能, 優於存在記憶體中

非同步請求

  • 點贊

    • 支援對帖子、評論點贊

    • 第1次點贊,第二次取消點贊【雙擊取消操作】

  • 首頁點贊數量

    • 統計帖子的點贊數量

  • 詳情頁點贊數量

    • 統計點贊數量

    • 顯示點贊狀態

4. 我收到的贊

  • 重構點贊功能

    • 以使用者為key, 記錄點贊數量

    • increment(key), decrement(key)

  • 開發個人主頁

    • 以使用者為key, 查詢點贊數量

5. 關注、取消關注

  • 需求

    • 開發關注、取消關注功能

    • 統計使用者的關注數、粉絲數

  • 關鍵

    • 若 A 關注了 B, 則 A 是 B 的 Follower(粉絲) , B 是 A 的 Followee(目標)。

    • 關注的目標可以是使用者、帖子、題目等,在實現時將這些目標抽象為實體。

6. 關注列表、粉絲列表

  • 業務層

    • 查詢某個使用者關注的人,支援分頁。

    • 查詢某個使用者的粉絲,支援分頁。

  • 表現層

    • 處理 “查詢關注的人”、“查詢粉絲” 請求。

    • 編寫 “查詢關注的人”、“查詢粉絲” 模板。

7. 優化登入模組

  • 使用Redis 儲存驗證碼

    • 驗證碼需要頻繁的訪問與重新整理,對效能要求較高

    • 驗證碼不需永久儲存,通常在很短的時間後就會失效

      • 設定過期時間

    • 分散式部署,存在Session 共享的問題

  • 使用Redis 儲存登入憑證【不刪,保留使用者記錄】

    • 處理每次請求時,都要查詢使用者的登入憑證,訪問的效率非常高

  • 使用Redis 快取使用者資訊【暫存,要刪+】

    • 處理每次請求時,都要根據憑證查詢使用者資訊,訪問的頻率非常高