Redis有序集合詳解

風葉翩翩發表於2020-03-20

有序集合和集合類似,只是說它是有序的,和無序集合的主要區別在於每一個元素除了值之外,它還會多一個分數。分數是一個浮點數,在 Java 中是使用雙精度表示的,根據分數,Redis 就可以支援對分數從小到大或者從大到小的排序。

這裡和無序集合一樣,對於每一個元素都是唯一的,但是對於不同元素而言,它的分數可以一樣。元素也是 String 資料型別,也是一種基於 hash 的儲存結構。

集合是通過雜湊表實現的,所以新增、刪除、查詢的複雜度都是 0(1)。集合中最大的成員數為 2 的 32 次方減 1(40 多億個成員),有序集合的資料結構如圖所示。
在這裡插入圖片描述

有序集合是依賴 key 標示它是屬於哪個集合,依賴分數進行排序,所以值和分數是必須的,而實際上不僅可以對分數進行排序,在滿足一定的條件下,也可以對值進行排序。

Redis基礎命令

有序集合和無序集合的命令是接近的,只是在這些命令的基礎上,會增加對於排序的操作,這些是我們在使用的時候需要注意的細節。

下面講解這些常用的有序集合的部分命令。有些時候 Redis 藉助資料區間的表示方法來表示包含或者不包含,比如在數學的區間表示中,[2,5] 表示包含 2,但是不包含 5 的區間。

Redis有序集合的部分命令
在這裡插入圖片描述
在這裡插入圖片描述

在對有序集合、下標、區間的表示方法進行操作的時候,需要十分小心命令,注意它是操作分數還是值,稍有不慎就會出現問題。

這裡命令比較多,也有些命令比較難使用,在使用的時候,務必要小心,不過好在我們使用 zset 的頻率並不是太高,下面是測試結果——有序集合命令展示。

在這裡插入圖片描述

spring-data-redis對有序集合的封裝

在 Spring 中使用 Redis 的有序集合,需要注意的是 Spring 對 Redis 有序集合的元素的值和分數的範圍(Range)和限制(Limit)進行了封裝,在演示如何使用 Spring 操作有序集合前要進一步瞭解它的封裝。

先介紹一個主要的介面——TypedTuple,它不是一個普通的介面,而一個內部介面,它是 org.springframework.data.redis.core.ZSetOperations 介面的內部介面,它定義了兩個方法,程式碼如下所示。

public interface ZSetOperations<K,V>{
    ......
public interface TypedTuple<V> extends Comparable<TypedTuple<V>< {
    V getValue();

    Double getScore();
}
......
}

這裡的 getValue() 是獲取值,而 getScore() 是獲取分數,但是它只是一個介面,而不是一個實現類。spring-data-redis 提供了一個預設的實現類—— DefaultTypedTuple,同樣它會實現 TypedTuple 介面,在預設的情況下 Spring 就會把帶有分數的有序集合的值和分數封裝到這個類中,這樣就可以通過這個類物件讀取對應的值和分數了。

Spring 不僅對有序集合元素封裝,而且對範圍也進行了封裝,方便使用。它是使用介面 org.springframework.data.redis.connection.RedisZSetCommands 下的內部類 Range 進行封裝的,它有一個靜態的 range() 方法,使用它就可以生成一個 Range 物件了,只是要清楚 Range 物件的幾個方法才行,為此我們來看看下面的虛擬碼。

//設定大於等於min
public Range gte(Object min)
//設定大於min
public Range gt(Object min)
//設定小於等於max
public Range lte(Object max)
//設定小於max
public Range lt(Object max)

這 4 個方法就是最常用的範圍方法。下面討論一下限制,它是介面 org.springframework.data.redis.connection.RedisZSetCommands 下的內部類,它是一個簡單的 POJO,它存在兩個屬性,它們的 getter 和 setter 方法,如下面的程式碼所示。

// ......
public interface RedisZSetCommands {
    // ......
public class Limit {
    int offset;
    int count;
//setter和getter方法
}
//......
}

通過屬性的名稱很容易知道:offset 代表從第幾個開始擷取,而 count 代表限制返回的總數量。

通過 Spring 操作有序集合

我們討論了 spring-data-redis 專案對有序集合的封裝。在測試程式碼前,要把 RedisTemplate 的 keySerializer 和 valueSerializer 屬性都修改為字串序列化器 StringRedisSerializer,測試程式碼如下所示。

public static void testZset() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                "applicationContext.xml");
    RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
    // Spring提供介面 TypedTuple操作有序集合
    Set<TypedTuple> set1 = new HashSet<TypedTuple>();
    Set<TypedTuple> set2 = new HashSet<TypedTuple>();
    int j = 9;
    for (int i = 1; i <= 9; i++) {
        j--;
        // 計算分數和值
        Double score1 = Double.valueOf(i);
        String value1 = "x" + i;
        Double score2 = Double.valueOf(j);
        String value2 = j % 2 == 1 ? "y" + j : "x" + j;
        // 使用 Spring 提供的預設 TypedTuple--DefaultTypedTuple
        TypedTuple typedTuple1 = new DefaultTypedTuple(value1, score1);
        set1.add(typedTuple1);
        TypedTuple typedTuple2 = new DefaultTypedTuple(value2, score2);
        set2.add(typedTuple2);
    }
    // 將元素插入有序集合zset1
    redisTemplate.opsForZSet().add("zset1", set1);
    redisTemplate.opsForZSet().add("zset2", set2);
    // 統計總數
    Long size = null;
    size = redisTemplate.opsForZSet().zCard("set1");
    // 計分數為score,那麼下面的方法就是求 3<=score<=6的元素
    size = redisTemplate.opsForZSet().count("zset1", 3, 6);
    Set set = null;
    // 從下標一開始擷取5個元素,但是不返回分數,每一個元索是String
    set = redisTemplate.opsForZSet().range("zset1", 1, 5);
    printSet(set);
    // 擷取集合所有元素,並且對集合按分數排序,並返回分數,每一個元素是TypedTuple
    set = redisTemplate.opsForZSet().rangeWithScores("zset1", 0, -1);
    printTypedTuple(set);
    // 將zset1和zset2兩個集合的交集放入集合inter_zset
    size = redisTemplate.opsForZSet().intersectAndStore("zset1", "zset2","inter_zset");
    // 區間
    Range range = Range.range();
    range.lt("x8");// 小於
    range.gt("x1"); // 大於
    set = redisTemplate.opsForZSet().rangeByLex("zset1", range);
    printSet(set);
    range.lte("x8"); // 小於等於
    range.gte("xl"); // 大於等於
    set = redisTemplate.opsForZSet().rangeByLex("zset1", range);
    printSet(set);
    // 限制返回個數
    Limit limit = Limit.limit();
    // 限制返回個數
    limit.count(4);
    // 限制從第五個開始擷取
    limit.offset(5);
    // 求區間內的元素,並限制返回4條
    set = redisTemplate.opsForZSet().rangeByLex("zset1", range, limit);
    printSet(set);
    // 求排行,排名第1返回0,第2返回1
    Long rank = redisTemplate.opsForZSet().rank("zset1", "x4");
    System.err.println("rank = " + rank);
    // 刪除元素,返回刪除個數
    size = redisTemplate.opsForZSet().remove("zset1", "x5", "x6");
    System.err.println("delete = " + size);
    // 按照排行刪除從0開始算起,這裡將刪除第排名第2和第3的元素
    size = redisTemplate.opsForZSet().removeRange("zset2", 1, 2);
    // 獲取所有集合的元素和分數,以-1代表全部元素
    set = redisTemplate.opsForZSet().rangeWithScores("zset2", 0, -1);
    printTypedTuple(set);
    // 刪除指定的元素
    size = redisTemplate.opsForZSet().remove("zset2", "y5", "y3");
    System.err.println(size);
    // 給集合中的一個元素的分數加上11
    Double dbl = redisTemplate.opsForZSet().incrementScore("zset1", "x1",11);
    redisTemplate.opsForZSet().removeRangeByScore("zset1", 1, 2);
    set = redisTemplate.opsForZSet().reverseRangeWithScores("zset2", 1, 10);
    printTypedTuple(set);
}

/**
* 列印TypedTuple集合
* @param set
* -- Set<TypedTuple>
*/
public static void printTypedTuple(Set<TypedTuple> set) {
    if (set != null && set.isEmpty()) {
        return;
    }
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        TypedTuple val = (TypedTuple) iterator.next();
        System.err.print("{value = " + val.getValue() + ", score = "
                + val.getScore() + "}\n");
    }
}

/**
* 列印普通集合
* @param set普通集合
*/
public static void printSet(Set set) {
    if (set != null && set.isEmpty()) {
        return;
    }
    Iterator iterator = set.iterator();
    while (iterator .hasNext()) {
    Object val = iterator.next();
    System. out.print (val +"\t");
    }
    System.out.println();
}

上面的程式碼演示了大部分 Spring 對有序集合的操作,並給出了比較清晰的註釋,大家認真思考之後就能熟悉如何通過 Spring 操作有序集合了。

相關文章