第 4-2 課:Spring Boot 和 Redis 常用操作

Redis 是目前使用最廣泛的緩存中間件,相比 Memcached,Redis 支持更多的數據結構和更豐富的數據操作,另外 Redis 有着豐富的集羣方案和使用場景,這一課我們一起學習 Redis 的常用操作。

Redis 介紹

Redis 是一個速度非常快的非關係數據庫(Non-Relational Database),它可以存儲鍵(Key)與 5 種不同類型的值(Value)之間的映射(Mapping),可以將存儲在內存的鍵值對數據持久化到硬盤,可以使用複製特性來擴展讀性能,還可以使用客戶端分片來擴展寫性能。

爲了滿足高性能,Redis 採用內存(in-memory)數據集(Dataset),根據使用場景,可以通過每隔一段時間轉儲數據集到磁盤,或者追加每條命令到日誌來持久化。持久化也可以被禁用,如果你只是需要一個功能豐富、網絡化的內存緩存。

數據模型

Redis 數據模型不僅與關係數據庫管理系統(RDBMS)不同,也不同於任何簡單的 NoSQL 鍵-值數據存儲。Redis 數據類型類似於編程語言的基礎數據類型,因此開發人員感覺很自然,每個數據類型都支持適用於其類型的操作,受支持的數據類型包括:

  • String(字符串)
  • Hash(哈希)
  • List(列表)
  • Set(集合)
  • Zset(Sorted Set:有序集合)

關鍵優勢

Redis 的優勢包括它的速度、對富數據類型的支持、操作的原子性,以及通用性:

  • 性能極高,它每秒可執行約 100,000 個 Set 以及約 100,000 個 Get 操作;
  • 豐富的數據類型,Redis 對大多數開發人員已知的大多數數據類型提供了原生支持,這使得各種問題得以輕鬆解決;
  • 原子性,因爲所有 Redis 操作都是原子性的,所以多個客戶端會併發地訪問一個 Redis 服務器,獲取相同的更新值;
  • 豐富的特性,Redis 是一個多效用工具,有非常多的應用場景,包括緩存、消息隊列(Redis 原生支持發佈/訂閱)、短期應用程序數據(比如 Web 會話、Web 頁面命中計數)等。

spring-boot-starter-data-redis

Spring Boot 提供了對 Redis 集成的組件包:spring-boot-starter-data-redis,它依賴於 spring-data-redis 和 lettuce。Spring Boot 1.0 默認使用的是 Jedis 客戶端,2.0 替換成了 Lettuce,但如果你從 Spring Boot 1.5.X 切換過來,幾乎感受不大差異,這是因爲 spring-boot-starter-data-redis 爲我們隔離了其中的差異性。

  • Lettuce:是一個可伸縮線程安全的 Redis 客戶端,多個線程可以共享同一個 RedisConnection,它利用優秀 Netty NIO 框架來高效地管理多個連接。
  • Spring Data:是 Spring 框架中的一個主要項目,目的是爲了簡化構建基於 Spring 框架應用的數據訪問,包括非關係數據庫、Map-Reduce 框架、雲數據服務等,另外也包含對關係數據庫的訪問支持。
  • Spring Data Redis:是 Spring Data 項目中的一個主要模塊,實現了對 Redis 客戶端 API 的高度封裝,使對 Redis 的操作更加便捷。

可以用以下方式來表達它們之間的關係:

Lettuce → Spring Data Redis → Spring Data → spring-boot-starter-data-redis

因此 Spring Data Redis 和 Lettuce 具備的功能,spring-boot-starter-data-redis 幾乎都會有。

快速上手

相關配置

引入依賴包

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

引入 commons-pool 2 是因爲 Lettuce 需要使用 commons-pool 2 創建 Redis 連接池。

application 配置

# Redis 數據庫索引(默認爲 0)
spring.redis.database=0
# Redis 服務器地址
spring.redis.host=localhost
# Redis 服務器連接端口
spring.redis.port=6379  
# Redis 服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制) 默認 8
spring.redis.lettuce.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制) 默認 -1
spring.redis.lettuce.pool.max-wait=-1
# 連接池中的最大空閒連接 默認 8
spring.redis.lettuce.pool.max-idle=8
# 連接池中的最小空閒連接 默認 0
spring.redis.lettuce.pool.min-idle=0

從配置也可以看出 Spring Boot 默認支持 Lettuce 連接池。

緩存配置

在這裏可以爲 Redis 設置一些全局配置,比如配置主鍵的生產策略 KeyGenerator,如不配置會默認使用參數名作爲主鍵。

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{

    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }
}

注意,我們使用了註解:@EnableCaching 來開啓緩存。

測試使用

在單元測試中,注入 RedisTemplate。String 是最常用的一種數據類型,普通的 key/value 存儲都可以歸爲此類,value 其實不僅是 String 也可以是數字。

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedisTemplate {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testString()  {
        redisTemplate.opsForValue().set("neo", "ityouknow");
        Assert.assertEquals("ityouknow", redisTemplate.opsForValue().get("neo"));
    }
}    

在這個單元測試中,我們使用 redisTemplate 存儲了一個字符串 "ityouknow",存儲之後獲取進行驗證,多次進行 set 相同的 key,鍵對應的值會被覆蓋

從上面的整個流程來看,使用 spring-boot-starter-data-redis 只需要三步就可以快速地集成 Redis 進行操作,下面介紹 Redis 如何操作各種數據類型。

各類型實踐

我們知道 Redis 支持多種數據類型,實體、哈希、列表、集合、有序集合,那麼在 Spring Boot 體系中都如何使用呢?

實體

先來看 Redis 對 Pojo 的支持,新建一個 User 對象,放到緩存中,再取出來。

@Test
public void testObj(){
    User user=new User("[email protected]", "smile", "youknow", "know","2020");
    ValueOperations<String, User> operations=redisTemplate.opsForValue();
    operations.set("com.neo", user);
    User u=operations.get("com.neo");
    System.out.println("user: "+u.toString());
}

輸出結果:

user: com.neo.domain.User@16fb356[id=<null>,userName=know,passWord=youknow,[email protected],nickName=smile,regTime=2020]

驗證發現完美支持對象的存入和讀取。

超時失效

Redis 在存入每一個數據的時候都可以設置一個超時時間,過了這個時間就會自動刪除數據,這種特性非常適合我們對階段數據的緩存。

新建一個 User 對象,存入 Redis 的同時設置 100 毫秒後失效,設置一個線程暫停 1000 毫秒之後,判斷數據是否存在並打印結果。

@Test
public void testExpire() throws InterruptedException {
    User user=new User("[email protected]", "expire", "youknow", "expire","2020");
    ValueOperations<String, User> operations=redisTemplate.opsForValue();
    operations.set("expire", user,100,TimeUnit.MILLISECONDS);
    Thread.sleep(1000);
    boolean exists=redisTemplate.hasKey("expire");
    if(exists){
        System.out.println("exists is true");
    }else{
        System.out.println("exists is false");
    }
}

輸出結果:

exists is false

從結果可以看出,Reids 中已經不存在 User 對象了,此數據已經過期,同時我們在這個測試的方法中使用了 hasKey("expire") 方法,可以判斷 key 是否存在。

刪除數據

有些時候,我們需要對過期的緩存進行刪除,下面來測試此場景的使用。首 set 一個字符串“ityouknow”,緊接着刪除此 key 的值,再進行判斷。

@Test
public void testDelete() {
    ValueOperations<String, User> operations=redisTemplate.opsForValue();
    redisTemplate.opsForValue().set("deletekey", "ityouknow");
    redisTemplate.delete("deletekey");
    boolean exists=redisTemplate.hasKey("deletekey");
    if(exists){
        System.out.println("exists is true");
    }else{
        System.out.println("exists is false");
    }
}

輸出結果:

exists is false

結果表明字符串“ityouknow”已經被成功刪除。

Hash(哈希)

一般我們存儲一個鍵,很自然的就會使用 get/set 去存儲,實際上這並不是很好的做法。Redis 存儲一個 key 會有一個最小內存,不管你存的這個鍵多小,都不會低於這個內存,因此合理的使用 Hash 可以幫我們節省很多內存。

Hash Set 就在哈希表 Key 中的域(Field)的值設爲 value。如果 Key 不存在,一個新的哈希表被創建並進行 Hset 操作;如果域(Field)已經存在於哈希表中,舊值將被覆蓋。

@Test
public void testHash() {
    HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
    hash.put("hash","you","you");
    String value=(String) hash.get("hash","you");
    System.out.println("hash value :"+value);
}

輸出結果:

hash value :you

根據上面測試用例發現,Hash set 的時候需要傳入三個參數,第一個爲 key,第二個爲 Field,第三個爲存儲的值。一般情況下 Key 代表一組數據,Field 爲 key 相關的屬性,而 Value 就是屬性對應的值。

List

Redis List 的應用場景非常多,也是 Redis 最重要的數據結構之一。 使用 List 可以輕鬆的實現一個隊列,List 典型的應用場景就是消息隊列,可以利用 List 的 Push 操作,將任務存在 List 中,然後工作線程再用 POP 操作將任務取出進行執行。

@Test
public void testList() {
    ListOperations<String, String> list = redisTemplate.opsForList();
    list.leftPush("list","it");
    list.leftPush("list","you");
    list.leftPush("list","know");
    String value=(String)list.leftPop("list");
    System.out.println("list value :"+value.toString());
}

輸出結果:

list value :know

上面的例子我們從左側插入一個 key 爲 "list" 的隊列,然後取出左側最近的一條數據。其實 List 有很多 API 可以操作,比如從右側進行插入隊列從右側進行讀取,或者通過方法 range 讀取隊列的一部分。接着上面的例子我們使用 range 來讀取。

List<String> values=list.range("list",0,2);
   for (String v:values){
       System.out.println("list range :"+v);
   }

輸出結果:

list range :know
list range :you
list range :it

range 後面的兩個參數就是插入數據的位置,輸入不同的參數就可以取出隊列中對應的數據。

Redis List 的實現爲一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷,Redis 內部的很多實現,包括髮送緩衝隊列等也都是用的這個數據結構。

Set

Redis Set 對外提供的功能與 List 類似是一個列表的功能,特殊之處在於 Set 是可以自動排重的,當你需要存儲一個列表數據,又不希望出現重複數據時,Set 是一個很好的選擇,並且 Set 提供了判斷某個成員是否在一個 Set 集合內的重要接口,這個也是 List 所不能提供的。

@Test
public void testSet() {
    String key="set";
    SetOperations<String, String> set = redisTemplate.opsForSet();
    set.add(key,"it");
    set.add(key,"you");
    set.add(key,"you");
    set.add(key,"know");
    Set<String> values=set.members(key);
    for (String v:values){
        System.out.println("set value :"+v);
    }
}

輸出結果:

set value :it
set value :know
set value :you

通過上面的例子我們發現,輸入了兩個相同的值“you”,全部讀取的時候只剩下了一條,說明 Set 對隊列進行了自動的排重操作。

Redis 爲集合提供了求交集、並集、差集等操作,可以非常方便的使用。

測試 difference

SetOperations<String, String> set = redisTemplate.opsForSet();
String key1="setMore1";
String key2="setMore2";
set.add(key1,"it");
set.add(key1,"you");
set.add(key1,"you");
set.add(key1,"know");
set.add(key2,"xx");
set.add(key2,"know");
Set<String> diffs=set.difference(key1,key2);
for (String v:diffs){
    System.out.println("diffs set value :"+v);
}

輸出結果:

diffs set value :it
diffs set value :you

根據上面這個例子可以看出,difference() 函數會把 key 1 中不同於 key 2 的數據對比出來,這個特性適合我們在金融場景中對賬的時候使用。

測試 unions

SetOperations<String, String> set = redisTemplate.opsForSet();
String key3="setMore3";
String key4="setMore4";
set.add(key3,"it");
set.add(key3,"you");
set.add(key3,"xx");
set.add(key4,"aa");
set.add(key4,"bb");
set.add(key4,"know");
Set<String> unions=set.union(key3,key4);
for (String v:unions){
    System.out.println("unions value :"+v);
}

輸出結果:

unions value :know
unions value :you
unions value :xx
unions value :it
unions value :bb
unions value :aa

根據例子我們發現,unions 會取兩個集合的合集,Set 還有其他很多類似的操作,非常方便我們對集合進行數據處理。

Set 的內部實現是一個 Value 永遠爲 null 的 HashMap,實際就是通過計算 Hash 的方式來快速排重,這也是 Set 能提供判斷一個成員是否在集合內的原因。

ZSet

Redis Sorted Set 的使用場景與 Set 類似,區別是 Set 不是自動有序的,而 Sorted Set 可以通過用戶額外提供一個優先級(Score)的參數來爲成員排序,並且是插入有序,即自動排序。

在使用 Zset 的時候需要額外的輸入一個參數 Score,Zset 會自動根據 Score 的值對集合進行排序,我們可以利用這個特性來做具有權重的隊列,比如普通消息的 Score 爲1,重要消息的 Score 爲 2,然後工作線程可以選擇按 Score 的倒序來獲取工作任務。

@Test
public void testZset(){
    String key="zset";
    redisTemplate.delete(key);
    ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
    zset.add(key,"it",1);
    zset.add(key,"you",6);
    zset.add(key,"know",4);
    zset.add(key,"neo",3);

    Set<String> zsets=zset.range(key,0,3);
    for (String v:zsets){
        System.out.println("zset value :"+v);
    }

    Set<String> zsetB=zset.rangeByScore(key,0,3);
    for (String v:zsetB){
        System.out.println("zsetB value :"+v);
    }
}

輸出結果:

zset value :it
zset value :neo
zset value :know
zset value :you
zsetB value :it
zsetB value :neo

通過上面的例子我們發現插入到 Zset 的數據會自動根據 Score 進行排序,根據這個特性我們可以做優先隊列等各種常見的場景。另外 Redis 還提供了 rangeByScore 這樣的一個方法,可以只獲取 Score 範圍內排序後的數據。

Redis Sorted Set 的內部使用 HashMap 和跳躍表(SkipList)來保證數據的存儲和有序,HashMap 裏放的是成員到 Score 的映射,而跳躍表裏存放的是所有的成員,排序依據是 HashMap 裏存的 Score,使用跳躍表的結構可以獲得比較高的查找效率,並且在實現上比較簡單。

封裝

在我們實際的使用過程中,不會給每一個使用的類都注入 redisTemplate 來直接使用,一般都會對業務進行簡單的包裝,最後提供出來對外使用。

我們舉兩個例子說明。

首先定義一個 RedisService 服務,將 RedisTemplate 注入到類中。

@Service
public class RedisService {
    @Autowired
    private RedisTemplate redisTemplate;
}

封裝簡單插入操作:

public boolean set(final String key, Object value) {
    boolean result = false;
    try {
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        operations.set(key, value);
        result = true;
    } catch (Exception e) {
        logger.error("set error: key {}, value {}",key,value,e);
    }
    return result;
}

會對其中出現的異常繼續處理,反饋給調用方。

比如我們想刪除某一類的 Key 的值。

public void removePattern(final String pattern) {
    Set<Serializable> keys = redisTemplate.keys(pattern);
    if (keys.size() > 0)
        redisTemplate.delete(keys);
}

使用 Redis 的 Pattern 來匹配出一批符合條件的緩存,然後批量進行刪除。

還有其他封裝方法,比如刪除的時候先判斷 Key 是否存在等,這些簡單的業務判斷都應該封裝在 RedisService,對外提供最簡單的 API 調用即可。

@Autowired
private RedisService redisService;

@Test
public void testString() throws Exception {
    redisService.set("neo", "ityouknow");
    Assert.assertEquals("ityouknow", redisService.get("neo"));
}

在其他服務使用的時候將 RedisService 注入其中,調用對應的方法來操作 Redis,這樣會更優雅簡單一些。

總結

Redis 是一款非常優秀的高性能緩存中間件,被廣泛的使用在各互聯網公司中,Spring Boot 對 Redis 的操作提供了很多支持,可以非常方便的去集成。Redis 擁有豐富的數據類型,方便我們在不同的業務場景中去使用,特別是提供了很多內置的高效集合操作,在業務中使用非常方便。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章