2020重新出發(fā),NOSQL,Redis的常用數(shù)據(jù)類型

Redis的數(shù)據(jù)類型String 數(shù)據(jù)結(jié)構(gòu)和常用命令hash 數(shù)據(jù)結(jié)構(gòu)和常用命令List 數(shù)據(jù)結(jié)構(gòu)和常用命令Set 數(shù)據(jù)結(jié)構(gòu)和常用命令Zset 數(shù)據(jù)結(jié)構(gòu)和常用命令Redis HyperLogLog常用命令

Redis的數(shù)據(jù)類型

Redis 是一種基于內(nèi)存的數(shù)據(jù)庫,并且提供一定的持久化功能,它是一種鍵值數(shù)據(jù)庫,使用 key 作為索引找到當(dāng)前緩存的數(shù)據(jù),并且返回給程序調(diào)用者。

當(dāng)前的 Redis 支持 5 種基礎(chǔ)數(shù)據(jù)類型和 3 種特殊數(shù)據(jù)類型,它們分別是字符串(String)、哈希結(jié)構(gòu)(hash)、列表(List)、集合(set)、有序集合(zset)和基數(shù)(HyperLogLog)、GEO(geospatial)、位圖(bitmap)。

Redis 定義的這數(shù)據(jù)類型是十分有用的,它除了提供簡單的存儲功能,還能對存儲的數(shù)據(jù)進(jìn)行一些計算。

比如字符串可以支持浮點數(shù)的自增、自減、字符求子串,集合求交集、并集,有序集合進(jìn)行排序等,所以使用它們有利于對一些不太大的數(shù)據(jù)集合進(jìn)行快速計算,簡化編程,同時它也比數(shù)據(jù)庫要快得多,所以它們對系統(tǒng)性能的提升十分有意義。

  • STRING(字符串):可以是保存字符串、整數(shù)和浮點數(shù) ,可以對字符串進(jìn)行操作

    • 比如增加字符或者求子串:

    • 如果是整數(shù)或者浮點數(shù),可以實現(xiàn)計算,比如自增等

  • LIST(列表):它是一個鏈表,它的每一個節(jié)點都包含一個字符串

    • Redis 支持從鏈表的兩端插入或者彈出節(jié)點,或者通過偏移對它進(jìn)行裁剪;

    • 還可以讀取一個或者多個節(jié)點,根據(jù)條件刪除或者查找節(jié)點等

  • SET(集合):它是一個收集器,但是是無序的,在它里而每一個元素都是一個字符串,而且是獨一無二,各不相同的

    • 可以新增、讀取、刪除單個元素:檢測一個元素是否在集合中;

    • 計算它和其他集合的交集、并集和差集等;

    • 隨機從集合中讀取元素

  • HASH(哈希散列表):它類似于 Java 語言中的 Map,是一個鍵值對應(yīng)的無序列表

    • 可以増、刪、査、改單個鍵值對,也可以獲取所有的鍵值對
  • ZSET(有序集合):它是一個有序的集合,可以包含字符 串、整數(shù)、浮點數(shù)、分值(score),元素 的排序是依據(jù)分值的大小來決定的

    • 可以增、刪、査、改元素,根據(jù)分值的范圍或者成員 來獲取對應(yīng)的元索
  • geospatial(Geo):Redis 在 3.2 推出 Geo 類型,該功能可以推算出地理位置信息,兩地之間的距離。

    • 規(guī)則:兩極無法直接添加,一般會下載城市數(shù)據(jù),直接通過 Java 程序一次性導(dǎo)入。

    • 有效的經(jīng)度從 -180 度到 180 度。有效的緯度從 -85.05112878 度到 85.05112878 度。當(dāng)坐標(biāo)位置超出指定范圍時,該命令將會返回一個錯誤。

  • HyperLogLog(基數(shù)):它的作用是計算重復(fù)的值,以確定存儲的數(shù)量

    • 只提供基數(shù)的運算,不提供返回的功能
  • bitmap (位圖):是通過最小的單位bit來進(jìn)行0或者1的設(shè)置,表示某個元素對應(yīng)的值或者狀態(tài)。一個bit的值,或者是0,或者是1;也就是說一個bit能存儲的最多信息是2。

    • bitmap 常用于統(tǒng)計用戶信息比如活躍粉絲和不活躍粉絲、登錄和未登錄、是否打卡等。

String 數(shù)據(jù)結(jié)構(gòu)和常用命令

字符串(String)是 Redis 最基本的數(shù)據(jù)結(jié)構(gòu),它將以一個鍵和一個值存儲于 Redis 內(nèi)部,它猶如 Java 的 Map 結(jié)構(gòu),讓 Redis 通過鍵去找到值。Redis 字符串的數(shù)據(jù)結(jié)構(gòu)如圖 1 所示。

Redis字符串?dāng)?shù)據(jù)結(jié)構(gòu)

圖 1 Redis 字符串?dāng)?shù)據(jù)結(jié)構(gòu)

Redis 會通過 key 去找到對應(yīng)的字符串,比如通過 key1 找到 value1,又如在 Java 互聯(lián)網(wǎng)中,假設(shè)產(chǎn)品的編號為 0001,只要設(shè)置 key 為 product_0001,就可以通過 product_0001 去保存該產(chǎn)品到 Redis 中,也可以通過 product_0001 從 redis 中找到產(chǎn)品信息。

常用命令如下:

命 令 說 明 備 注
set key value 設(shè)置鍵值對 最常用的寫入命令
get key 通過鍵獲取值 最常用的讀取命令
del key 通過 key,刪除鍵值對 刪除命令,返冋刪除數(shù),注意,它是個通用的命令,換句話說在其他數(shù)據(jù)結(jié)構(gòu)中,也可以使用它
strlen key 求 key 指向字符串的長度 返回長度
getset key value 修改原來 key 的對應(yīng)值,并將舊值返回 如果原來值為空,則返回為空,并設(shè)置新值
getrange key start end 獲取子串 記字符串的長度為 len,把字符串看作一個數(shù)組,而 Redis 是以 0 開始計數(shù)的,所以 start 和 end 的取值范圍 為 0 到 len-1
append key value 將新的字符串 value,加入到原來 key 指向的字符串末 返回 key 指向新字符串的長度

為了讓大家更為明確,在 Redis 提供的客戶端進(jìn)行測試如圖 2 所示。

Redis操作字符串重用命令

圖 2 Redis 操作字符串重用命令

這里我們看到了字符串的常用操作,為了在 Spring 中測試這些命令,首先配置 Spring 關(guān)于 Redis 字符串的運行環(huán)境,配置 Spring 關(guān)于 Redis 字符串的運行環(huán)境代碼如下所示。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="yml" cid="n104" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="50" />
<property name="maxTotal" value="100" />
<property name="maxWaitMillis" value="20000" />
</bean>
?
<bean id="connectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="localhost" />
<property name="port" value="6379" />
<property name="poolConfig" ref="poolConfig" />
</bean>
<bean id="jdkSerializationRedisSerializer"
class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
<bean id="stringRedisSerializer"
class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="keySerializer" ref="stringRedisSerializer" />
<property name="valueSerializer" ref="jdkSerializationRedisSerializer" />
</bean></pre>

注意,這里給 Spring 的 RedisTemplate 的鍵值序列化器設(shè)置為了 String 類型,所以它就是一種字符串的操作。假設(shè)把這段 Spring 的配置代碼保存為一個獨立為文件 applicationContext.xml,使用 Spring 測試 Redis 字符串操作代碼如下所示。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n106" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> package com.test;
?
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
?
import com.pojo.Role;
?
public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
"applicationContext.xml");
RedisTemplate redisTemplate = applicationContext
.getBean(RedisTemplate.class);
// 設(shè)值
redisTemplate.opsForValue().set("key1", "value1");
redisTemplate.opsForValue().set("key2", "value2");
// 通過key獲取值
String value1 = (String) redisTemplate.opsForValue().get("key1");
System.out.println(value1);
// 通過key刪除值
redisTemplate.delete("key1");
// 求長度
Long length = redisTemplate.opsForValue().size("key2");
System.out.println(length);
// 設(shè)值新值并返回舊值
String oldValue2 = (String) redisTemplate.opsForValue().getAndSet(
"key2", "new_value2");
System.out.println(oldValue2);
// 通過key獲取值.
String value2 = (String) redisTemplate.opsForValue().get("key2");
System.out.println(value2);
// 求子串
String rangeValue2 = redisTemplate.opsForValue().get("key2", 0, 3);
System.out.println(rangeValue2);
// 追加字符串到末尾,返回新串長度
int newLen = redisTemplate.opsForValue().append("key2", "_app");
System.out.println(newLen);
String appendValue2 = (String) redisTemplate.opsForValue().get("key2");
System.out.println(appendValue2);
}
}</pre>

這是主要的目的只是在 Spring 操作 Redis 鍵值對,其操作就等同于圖 2 所示的命令一樣。

在 Spring 中,redisTemplate.opsForValue() 所返回的對象可以操作簡單的鍵值對,可以是字符串,也可以是對象,具體依據(jù)你所配置的序列化方案。

由于配置 Spring 關(guān)于 Redis 字符串的運行環(huán)境代碼所配置的是字符串,所以以字符串來操作 Redis,其測試結(jié)果如下:

運行結(jié)果

圖 3 運行結(jié)果

結(jié)果和我們看到的命令行的結(jié)果一樣的,作為開發(fā)者要熟悉這些方法。

上面介紹了字符串最常用的命令,但是 Redis 除了這些之外還提供了對整數(shù)和浮點型數(shù)字的功能。如果字符串是數(shù)字(整數(shù)或者浮點數(shù)),那么 Redis 還能支持簡單的運算。不過它的運算能力比較弱,目前版本只能支持簡單的加減法運算,如下 。

  • incr key :在原字段上加 1 ,只能對整數(shù)操作

  • incrby key increment : 在原字段上加上整數(shù)(increment) , 只能對整數(shù)操作

  • decr key :在原字段上減 1 ,只能對整數(shù)操作

  • decrby key decrement : 在原字段上減去整數(shù)(decrement), 只能對整數(shù)操作

  • incrbyfloat keyincrement : 在原字段上加上浮點數(shù)(increment), 可以操作浮點數(shù)或者整數(shù)

對操作浮點數(shù)和整數(shù)進(jìn)行了測試,如圖 4 所示。

操作浮點數(shù)和整數(shù)

圖 4 操作浮點數(shù)和整數(shù)

在測試過程中,如果開始把 val 設(shè)置為浮點數(shù),那么 incr、decr、incrby、decrby 的命令都會失敗。Redis 并不支持減法、乘法、除法操作,功能十分有限,這點需要我們注意。

由于 Redis 的功能比較弱,所以經(jīng)常會在 Java 程序中讀取它們,然后通過 Java 進(jìn)行計算并設(shè)置它們的值。

注意,所有關(guān)于減法的方法,原有值都必須是整數(shù),否則就會引發(fā)異常,

hash 數(shù)據(jù)結(jié)構(gòu)和常用命令

Redis 中哈希(hash)結(jié)構(gòu)就如同 Java 的 map 一樣,一個對象里面有許多鍵值對,它是特別適合存儲對象的,如果內(nèi)存足夠大,那么一個 Redis 的 hash 結(jié)構(gòu)可以存儲 2 的 32 次方減 1 個鍵值對(40 多億)。

一般而言,不會使用到那么大的一個鍵值對,所以我們認(rèn)為 Redis 可以存儲很多的鍵值對。在 Redis 中,hash 是一個 String 類型的 field 和 value 的映射表,因此我們存儲的數(shù)據(jù)實際在 Redis 內(nèi)存中都是一個個字符串而已。

Redis hash 結(jié)構(gòu)命令,如下。

命 令 說 明 備 注
hdel key field1[field2......] 刪除 hash 結(jié)構(gòu)中的某個(些)字段 可以進(jìn)行多個字段的刪除
hexists key field 判斷 hash 結(jié)構(gòu)中是否存在 field 字段 存在返回 1,否則返回 0
hgetall key 獲取所有 hash 結(jié)構(gòu)中的鍵值 返回鍵和值
hincrby key field increment 指定給 hash 結(jié)構(gòu)中的某一字段加上一個整數(shù) 要求該字段也是整數(shù)字符串
hincrbyfloat key field increment 指定給 hash 結(jié)構(gòu)中的某一字段加上一個浮點數(shù) 要求該字段是數(shù)字型字符串
hkeys key 返回 hash 中所有的鍵 ——
hlen key 返回 hash 中鍵值對的數(shù)量 ——
hmget key field1[field2......] 返回 hash 中指定的鍵的值,可以是多個 依次返回值
hmset key field1 value1 [field2 field2......] hash 結(jié)構(gòu)設(shè)置多個鍵值對 ——
hset key filed value 在 hash 結(jié)構(gòu)中設(shè)置鍵值對 單個設(shè)值
hsetnx key field value 當(dāng) hash 結(jié)構(gòu)中不存在對應(yīng)的鍵,才設(shè)置值 ——
hvals key 獲取 hash 結(jié)構(gòu)中所有的值 ——

可以看出,在 Redis 中的哈希結(jié)構(gòu)和字符串有著比較明顯的不同。

首先,命令都是以 h 開頭,代表操作的是 hash 結(jié)構(gòu)。其次,大多數(shù)命令多了一個層級 field,這是 hash 結(jié)構(gòu)的一個內(nèi)部鍵,也就是說 Redis 需要通過 key 索引到對應(yīng)的 hash 結(jié)構(gòu),再通過 field 來確定使用 hash 結(jié)構(gòu)的哪個鍵值對。

下面通過 Redis 的這些操作命令來展示如何使用它們,如圖 2 所示。

Redis的hash結(jié)構(gòu)命令展示

圖 2 Redis 的 hash 結(jié)構(gòu)命令展示

從圖 2 中可以看到,Redis 關(guān)于哈希結(jié)構(gòu)的相關(guān)命令。這里需要注意的是:

哈希結(jié)構(gòu)的大小,如果哈希結(jié)構(gòu)是個很大的鍵值對,那么使用它要十分注意,尤其是關(guān)于 hkeys、hgetall、hvals 等返回所有哈希結(jié)構(gòu)數(shù)據(jù)的命令,會造成大量數(shù)據(jù)的讀取。這需要考慮性能和讀取數(shù)據(jù)大小對 JVM 內(nèi)存的影響。

對于數(shù)字的操作命令 hincrby 而言,要求存儲的也是整數(shù)型的字符串,對于 hincrbyfloat 而言,則要求使用浮點數(shù)或者整數(shù),否則命令會失敗。

我們用 Spring 來完成圖 2 的功能,代碼如下所示。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n200" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> public static void testRedisHash() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationcontext.getBean(RedisTemplate.class);
String key = "hash";
Map<String, String> map = new HashMap<String,String>();
map.put("f1", "val1");
map.put("f2", "val2");
// 相當(dāng)于hmset命令
redisTemplate.opsForHash().putAll(key, map);
// 相當(dāng)于hset命令
redisTemplate.opsForHash().put(key, "f3", "6");
printValueForhash (redisTemplate, key, "f3");
// 相當(dāng)于 hexists key filed 命令
boolean exists = redisTemplate.opsForHash().hasKey(key, "f3");
System.out.println(exists);
// 相當(dāng)于hgetall命令
Map keyValMap = redisTemplate.opsForHash().entries(key);
//相當(dāng)于hincrby命令
redisTemplate.opsForHash().increment(key, "f3",2);
printValueForhash (redisTemplate, key, "f3");
//相當(dāng)于hincrbyfloat命令
redisTemplate.opsForHash().increment (key, "f3", 0.88);
printValueForhash(redisTemplate, key, "f3");
//相當(dāng)于hvals命令
List valueList = redisTemplate.opsForHash().values(key);
//相當(dāng)于hkeys命令
Set keyList = redisTemplate.opsForHash().keys(key);
List<String> fieldList = new ArrayList<String>();
fieldList.add("f1");
fieldList.add("f2");
//相當(dāng)于hmget命令
List valueList2 = redisTemplate.opsForHash().multiGet(key, keyList);
//相當(dāng)于hsetnx命令
boolean success = redisTemplate.opsForHash () .putlfAbsent(key, "f4", "val4");
System.out.println(success);
//相當(dāng)于hdel命令
Long result = redisTemplate.opsForHash().delete(key, "fl", "f2");
System.out.println(result);
}
private static void printValueForhash(RedisTemplate redisTemplate,String key,String field) {
//相當(dāng)于hget命令
Object value = redisTemplate.opsForHash().get(key,field);
System.out.println(value);
}</pre>

以上代碼做了比較詳細(xì)的注解,也不難理解,不過需要注意以下幾點內(nèi)容:

  1. hmset 命令,在 Java 的 API 中,是使用 map 保存多個鍵值對在先的。

  2. hgetall 命令會返回所有的鍵值對,并保存到一個 map 對象中,如果 hash 結(jié)構(gòu)很大,那么要考慮它對 JVM 的內(nèi)存影響。

  3. hincrby 和 hincrbyFloat 命令都采用 increment 方法,Spring 會識別它具體使用何種方法。

  4. redisTemplate.opsForHash().values(key) 方法相當(dāng)于 hvals 命令,它會返回所有的值,并保存到一個 List 對象中;而 redisTemplate.opsForHash().keys(key) 方法相當(dāng)于 hkeys 命令,它會獲取所有的鍵,保存到一個 Set 對象中。

  5. 在 Spring 中使用 redisTemplate.opsForHash().putAll(key,map) 方法相當(dāng)于執(zhí)行了 hmset 命令,使用了 map,由于配置了默認(rèn)的序列化器為字符串,所以它也只會用字符串進(jìn)行轉(zhuǎn)化,這樣才能執(zhí)行對應(yīng)的數(shù)值加法,如果使用其他序列化器,則后面的命令可能會拋出異常。

  6. 在使用大的 hash 結(jié)構(gòu)時,需要考慮返回數(shù)據(jù)的大小,以避免返回太多的數(shù)據(jù),引發(fā) JVM 內(nèi)存溢出或者 Redis 的性能問題。

運行以上代碼,可以得到這樣的輸出結(jié)果:

運行結(jié)果

圖 3 運行結(jié)果

操作成功了,按照類似的代碼就可以在 Spring 中順利操作 Redis 的 hash 結(jié)構(gòu)了。

List 數(shù)據(jù)結(jié)構(gòu)和常用命令

鏈表(List)結(jié)構(gòu)是 Redis 中一個常用的結(jié)構(gòu),它可以存儲多個字符串,而且它是有序的,能夠存儲 2 的 32 次方減 1 個節(jié)點(超過 40 億個節(jié)點)。

Redis 鏈表是雙向的,因此即可以從左到右,也可以從右到左遍歷它存儲的節(jié)點,鏈表結(jié)構(gòu)如圖 1 所示。

鏈表結(jié)構(gòu)

圖 1 鏈表結(jié)構(gòu)

由于是雙向鏈表,所以只能夠從左到右,或者從右到左地訪問和操作鏈表里面的數(shù)據(jù)節(jié)點。但是使用鏈表結(jié)構(gòu)就意味著讀性能的喪失,所以要在大量數(shù)據(jù)中找到一個節(jié)點的操作性能是不佳的,因為鏈表只能從一個方向中去遍歷所要節(jié)點。

比如從查找節(jié)點 10 000 開始查詢,它需要按照節(jié)點 1、節(jié)點 2、節(jié)點 3……直至節(jié)點 10 000,這樣的順序查找,然后把一個個節(jié)點和你給出的值比對,才能確定節(jié)點所在。如果這個鏈表很大,如有上百萬個節(jié)點,可能需要遍歷幾十萬次才能找到所需要的節(jié)點,顯然查找性能是不佳的。

而鏈表結(jié)構(gòu)的優(yōu)勢在于插入和刪除的便利,因為鏈表的數(shù)據(jù)節(jié)點是分配在不同的內(nèi)存區(qū)域的,并不連續(xù),只是根據(jù)上一個節(jié)點保存下一個節(jié)點的順序來索引而已,無需移動元素。其新增和刪除的操作如圖 2 所示。

鏈表的新增和刪除操作

圖 2 鏈表的新增和刪除操作

圖 2 的阿拉伯?dāng)?shù)字代表新增的步驟,而漢字?jǐn)?shù)字代表刪除步驟。

  • 新增節(jié)點:對插入圖中的節(jié)點 4 而言,先看從左到右的指向,先讓節(jié)點 4 指向節(jié)點 1 原來的下一個節(jié)點,也就是節(jié)點 2,然后讓節(jié)點 1 指向節(jié)點 4,這樣就完成了從右到左的指向修改。再看從右到左,先讓節(jié)點 4 指向節(jié)點 1,然后節(jié)點 2 指向節(jié)點 4,這個時候就完成了從右到左的指向,那么節(jié)點 1 和節(jié)點 2 之間的原有關(guān)聯(lián)關(guān)系都已經(jīng)失效,這樣就完成了在鏈表中新增節(jié)點4的功能。

  • 刪除節(jié)點:對刪除圖中的節(jié)點 3 而言,首先讓節(jié)點 2 從左到右指向后續(xù)節(jié)點,然后讓后續(xù)節(jié)點指向節(jié)點 2,這樣節(jié)點 3 就脫離了鏈表,也就是斷絕了與節(jié)點 2 和后繼節(jié)點的關(guān)聯(lián)關(guān)系,然后對節(jié)點 3 進(jìn)行內(nèi)存回收,無須移動任何節(jié)點,就完成了刪除。

由此可見,鏈表結(jié)構(gòu)的使用是需要注意場景的,對于那些經(jīng)常需要對數(shù)據(jù)進(jìn)行插入和刪除的列表數(shù)據(jù)使用它是十分方便的,因為它可以在不移動其他節(jié)點的情況下完成插入和刪除。而對于需要經(jīng)常查找的,使用它性能并不佳,它只能從左到右或者從右到左的查找和比對。

因為是雙向鏈表結(jié)構(gòu),所以 Redis 鏈表命令分為左操作和右操作兩種命令,左操作就意味著是從左到右,右操作就意味著是從右到左。

Redis 關(guān)于鏈表的命令如表 1 所示。

命 令 說 明 備 注
lpush key node1 [node2.]..... 把節(jié)點 node1 加入到鏈表最左邊 如果是 node1、node2 ...noden 這樣加入, 那么鏈表開頭從左到右的順序是 noden...node2、node1
rpush key node1[node2]...... 把節(jié)點 node1 加入到鏈表的最右邊 如果是 node1、node2....noden 這樣加 入,那么鏈表結(jié)尾從左到右的順序是 node1、node2,node3...noden
lindex key index 讀取下標(biāo)為 index 的節(jié)點 返回節(jié)點字符串,從 0 開始算
llen key 求鏈表的長度 返回鏈表節(jié)點數(shù)
lpop key 刪除左邊第一個節(jié)點,并將其返回 ——
rpop key 刪除右邊第一個節(jié)點,并將其返回 ——
linsert key before after pivot node 插入一個節(jié)點 node,并且可以指定在值為pivot 的節(jié)點的前面(before)或者后面(after)) 如果 list 不存在,則報錯;如果沒有值為對應(yīng) pivot 的,也會插入失敗返回 -1
lpushx list node 如果存在 key 為 list 的鏈表,則插入節(jié)點 node, 并且作為從左到右的第一個節(jié)點 如果 list 不存在,則失敗
rpushx list node 如果存在 key 為 list 的鏈表,則插入節(jié)點 node,并且作為從左到右的最后個節(jié)點 如果 list 不存在,則失敗
lrange list start end 獲取鏈表 list 從 start 下標(biāo)到 end 下標(biāo)的節(jié)點值 包含 start 和 end 下標(biāo)的值
lrem list count value 如果 count 為 0,則刪除所有值等于 value 的節(jié) 點:如果 count 不是 0,則先對 count 取絕對值,假設(shè)記為 abs,然后從左到右刪除不大于 abs 個等于 value 的節(jié)點 注意,count 為整數(shù),如果是負(fù)數(shù),則 Redis 會先求取其絕對值,然后傳遞到后臺操作
lset key index node 設(shè)置列表下標(biāo)為 index 的節(jié)點的值為 node ——
ltrim key start stop 修剪鏈表,只保留從 start 到 stop 的區(qū)間的節(jié)點,其余的都刪除掉 包含 start 和 end 的下標(biāo)的節(jié)點會保留

表所列舉的就是常用的鏈表命令,其中以“l(fā)”開頭的代表左操作,以“r”開頭的代表右操作。對于很多個節(jié)點同時操作的,需要考慮其花費的時間,鏈表數(shù)據(jù)結(jié)構(gòu)對于查找而言并不適合于大數(shù)據(jù),而 Redis 也給了比較靈活的命令對其進(jìn)行操作。Redis 關(guān)于鏈表的操作命令,如圖 3 所示。

Redis關(guān)于鏈表的操作命令

圖 3 Redis關(guān)于鏈表的操作命令

這里展示了關(guān)于 Redis 鏈表的常用命令,只是對于大量數(shù)據(jù)操作的時候,我們需要考慮插入和刪除內(nèi)容的大小,因為這將是十分消耗性能的命令,會導(dǎo)致 Redis 服務(wù)器的卡頓。對于不允許卡頓的一些服務(wù)器,可以進(jìn)行分批次操作,以避免出現(xiàn)卡頓。

需要指出的是,之前這些操作鏈表的命令都是進(jìn)程不安全的,因為當(dāng)我們操作這些命令的時候,其他 Redis 的客戶端也可能操作同一個鏈表,這樣就會造成并發(fā)數(shù)據(jù)安全和一致性的問題,尤其是當(dāng)你操作一個數(shù)據(jù)量不小的鏈表結(jié)構(gòu)時,常常會遇到這樣的問題。

為了克服這些問題,Redis 提供了鏈表的阻塞命令,它們在運行的時候,會給鏈表加鎖,以保證操作鏈表的命令安全性,如表所示。

命 令 說 明 備 注
blpop key timeout 移出并獲取列表的第一個元索,如果列表沒有元素會阻塞列表直到等待超時或發(fā)現(xiàn)可彈出元索為止 相對于 lpop 命令,它的操作是進(jìn)程安全的
brpop key timeout 移出并獲取列表的最后一個元素,如果列表沒有元素會阻塞列表直到等待超時或發(fā)現(xiàn)可彈出元素為止 相對于 rpop 命令,它的操作是進(jìn)程安全的
rpoplpush key sre dest 按從左到右的順序,將一個鏈表的最后一個元素移除,并插入到目標(biāo)鏈表最左邊 不能設(shè)置超時時間
brpoplpush key src dest timeout 按從左到右的順序,將一個鏈表的最后一個元素移除,并插入到目標(biāo)鏈表最左邊,并可以設(shè)置超時時間 可設(shè)置超時時間

當(dāng)使用這些命令時,Redis 就會對對應(yīng)的鏈表加鎖,加鎖的結(jié)果就是其他的進(jìn)程不能再讀取或者寫入該鏈表,只能等待命令結(jié)束。加鎖的好處可以保證在多線程并發(fā)環(huán)境中數(shù)據(jù)的一致性,保證一些重要數(shù)據(jù)的一致性,比如賬戶的金額、商品的數(shù)量。

不過在保證這些的同時也要付出其他線程等待、線程環(huán)境切換等代價,這將使得系統(tǒng)的并發(fā)能力下降,關(guān)于多線程并發(fā)鎖,未來還會提及,這里先看 Redis 鏈表阻塞操作命令,如圖 4 所示。

Redis鏈表阻塞操作命令

圖 4 Redis 鏈表阻塞操作命令

在實際的項目中,雖然阻塞可以有效保證了數(shù)據(jù)的一致性,但是阻塞就意味著其他進(jìn)程的等待,CPU 需要給其他線程掛起、恢復(fù)等操作,更多的時候我們希望的并不是阻塞的處理請求,所以這些命令在實際中使用得并不多,后面還會深入探討關(guān)于高并發(fā)鎖的問題。

使用 Spring 去操作 Redis 鏈表的命令,這里繼續(xù)保持代碼清單18-5關(guān)于 RedisTemplate 的配置,在此基礎(chǔ)上獲取 RedisTemplate 對象,然后輸入以下代碼,它實現(xiàn)的是圖 3 所示的命令功能,請讀者仔細(xì)體會。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n333" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> public static void testList() {
ApplicationContext applicationcontext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationcontext.getBean(RedisTemplate.class);
try {
//刪除鏈表,以便我們可以反復(fù)測試
redisTemplate.delete("list");
//把node3插入鏈表list
redisTemplate. opsForList ().leftPush ("list", "node3");
List<String> nodeList = new ArrayList<String>();
for (int i = 2; i >= 1; i--){
nodeList.add("nnode" + i);
}
//相當(dāng)于lpush把多個價值從左插入鏈表
redisTemplate.opsForList().leftPushAll("list", nodeList);
//從右邊插入一個節(jié)點
redisTemplate.opsForList().rightPush("list", "node4");
//獲取下標(biāo)為0的節(jié)點
String nodel = (String) redisTemplate.opsForList() .index("list", 0);
//獲取鏈表長度
long size = redisTemplate.opsForList ().size ("listn");
//從左邊彈出一個節(jié)點
String lpop = (String) redisTemplate.opsForList().leftPop("list");
//從右邊彈出一個節(jié)點
String rpop = (String) redisTemplate.opsForList().rightPop("list");
//注意,需要使用更為底層的命令才能操作linsert命令
//使用linsert命令在node2前插入一個節(jié)點
redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),RedisListCommands.Position.BEFORE,"node2".getBytes("utf-8"),"before_node".getBytes("utf-8"));
//使用linsert命令在node2后插入一個節(jié)點
redisTemplate.getConnectionFactory().getConnection().linsert("list".getBytes("utf-8"),RedisListCommands.Position.AFTER,"node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));
//判斷l(xiāng)ist是否存在,如果存在則從左邊插入head節(jié)點
redisTemplate.opsForList().leftPushlfPresent("list", "head");
//判斷l(xiāng)ist是否存在,如果存在則從右邊插入end節(jié)點
redisTemplate.opsForList().rightPushlfPresent("list", "end");
//從左到右,或者下標(biāo)從0到10的節(jié)點元素
List valueList = redisTemplate.opsForList().range("list", 0, 10);
nodeList.clear();
for (int i = 1; i <= 3; i++) {
nodeList.add("node");
}
//在鏈表左邊插入三個值為node的節(jié)點
redisTemplate.opsForList().leftPushAll.("list", nodeList);
//從左到右刪除至多三個node節(jié)點
redisTemplate.opsForList().remove("list", 3,"node");
//給鏈表下標(biāo)為0的節(jié)點設(shè)置新值
redisTemplate.opsForList().set("list",0, "new_head_value");
} catch (UnsupportedEncodingException ex) {
ex.printStackTrace();
}
//打印鏈表數(shù)據(jù)
printList(redisTemplate, "list");
}
public static void printList(RedisTemplate redisTemplate, String key) {
//鏈表長度
Long size = redisTemplate.opsForList().size(key);
}</pre>

這里所展示的是 RedisTemplate 對于 Redis 鏈表的操作,其中 left 代表左操作,right 代表右操作。有些命令 Spring 所提供的 RedisTemplate 并不能支持,比如 linsert 命令,這個時候可以使用更為底層的方法去操作,正如代碼中的這段:

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n335" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> // 使用linsert命令在node2前插入一個節(jié)點
redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),
RedisListCommands.Position.BEFORE,"node2".getBytes("utf-8"),
"before_node".getBytes("utf-8"));</pre>

在多值操作的時候,往往會使用 list 進(jìn)行封裝,比如 leftPushAll 方法,對于很大的 list 的操作需要注意性能,比如 remove 這樣的操作,在大的鏈表中會消耗 Redis 系統(tǒng)很多的性能。

正如之前的探討一樣,Redis 還有對鏈表進(jìn)行阻塞操作的命令,這里 Spring 也給出了支持,代碼如下所示。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n338" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> public static void testBList() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
// 清空數(shù)據(jù),可以重復(fù)測試
redisTemplate.delete ("list1");
redisTemplate.delete ("list2");
//初始化鏈表 list1
List<String> nodeList = new ArrayList<String>();
for (int i=1; i<=5; i++) {
nodeList.add("node" + i);
}
redisTemplate.opsForList().leftPushAll("list1", nodeList);
// Spring 使用參數(shù)超時時間作為阻塞命令區(qū)分,等價于 blpop 命令,并且可以設(shè)置時間參數(shù) redisTemplate.opsForList().leftPop ("list1", 1, TimeUnit.SECONDS);
// Spring 使用參數(shù)超時時間作為阻塞命令區(qū)分,等價于 brpop 命令,并且可以設(shè)置時間參數(shù)
redisTemplate.opsForList().rightPop("list1", 1, TimeUnit.SECONDS);
nodeList.clear();
// 初始化鏈表 list2
for (int i=1; i<=3; i++) {
nodeList.add("dato" + i);
}
redisTemplate.opsForList().leftPushAll("list2", nodeList);
// 相當(dāng)于 rpoplpush 命令,彈出 list1 最右邊的節(jié)點,插入到 list2 最左邊
redisTemplate.opsForList().rightPopAndLeftPush("list1","list2");
// 相當(dāng)于brpoplpush命令,注意在 Spring 中使用超時參數(shù)區(qū)分 redisTemplate.opsForList().rightPopAndLeftPush("list1", "list2",1,TimeUnit.SECONDS);
// 打印鏈表數(shù)據(jù)
printList(redisTemplate, "list1");
printList(redisTemplate, "list2");
}</pre>

這里展示了 Redis 關(guān)于鏈表的阻塞命令,在 Spring 中它和非阻塞命令的方法是一致的,只是它會通過超時參數(shù)進(jìn)行區(qū)分,而且我們還可以通過方法設(shè)置時間的單位,使用還是相當(dāng)簡單的。注意,它是阻塞的命令,在多線程的環(huán)境中,它能在一定程度上保證數(shù)據(jù)的一致而性能卻不佳。

Set 數(shù)據(jù)結(jié)構(gòu)和常用命令

Redis 的集合(set)不是一個線性結(jié)構(gòu),而是一個哈希表結(jié)構(gòu),它的內(nèi)部會根據(jù) hash 分子來存儲和查找數(shù)據(jù),理論上一個集合可以存儲 2 的 32 次方減 1 個節(jié)點(大約 42 億)個元素,因為采用哈希表結(jié)構(gòu),所以對于 Redis 集合的插入、刪除和查找的復(fù)雜度都是 0(1),只是我們需要注意 3 點。

  • 對于集合而言,它的每一個元素都是不能重復(fù)的,當(dāng)插入相同記錄的時候都會失敗。

  • 集合是無序的。

  • 集合的每一個元素都是 String 數(shù)據(jù)結(jié)構(gòu)類型。

Redis 的集合可以對于不同的集合進(jìn)行操作,比如求出兩個或者以上集合的交集、差集和并集等。集合命令,如表 所示。

命 令 說 明 備 注
sadd key member1 [member2 member3......] 給鍵為 key 的集合増加成員 可以同時増加多個
scard key 統(tǒng)計鍵為 key 的集合成員數(shù)
sdiffkey1 [key2] 找出兩個集合的差集 參數(shù)如果是單key,那么 Redis 就返回這個 key 的所有元素
sdiftstore des key1 [key2] 先按 sdiff 命令的規(guī)則,找出 key1 和 key2 兩 個集合的差集,然后將其保存到 des 集合中。
sinter key1 [key2] 求 key1 和 key2 兩個集合的交集。 參數(shù)如果是單 key,那么 Redis 就返冋這個 key 的所有元素
sinterstore des key1 key2 先按 sinter 命令的規(guī)則,找出 key1 和 key2 兩個集合的交集,然后保存到 des 中
sismember key member 判斷 member 是否鍵為 key 的集合的成員 如果是返回 1,否則返回 0
smembers key 返回集合所有成員 如果數(shù)據(jù)量大,需要考慮迭代遍歷的問題
smove src des member 將成員 member 從集合 src 遷移到集合 des 中
spop key 隨機彈出集合的一個元素 注意其隨機性,因為集合是無序的
srandmember key [count] 隨機返回集合中一個或者多個元素,count 為限制返回總數(shù),如果 count 為負(fù)數(shù),則先求其絕對值 count 為整數(shù),如果不填默認(rèn)為 1,如果 count 大于等于集合總數(shù),則返回整個集合
srem key member1[member2......] 移除集合中的元素,可以是多個元素 對于很大的集合可以通過它刪除部分元素,避免刪除大量數(shù)據(jù)引發(fā) Redis 停頓
sunion key1 [key2] 求兩個集合的并集 參數(shù)如果是單 key,那么 Redis 就返回這個 key 的所有元素
sunionstore des key1 key2 先執(zhí)行 sunion 命令求出并集,然后保存到鍵為 des 的集合中

表 中命令的前綴都包含了一個 s,用來表達(dá)這是集合的命令,集合是無序的,并且支持并集、交集和差集的運算,下面通過命令行客戶端來演示這些命令,如圖 1 所示。

通過命令行客戶端演示這些命令

圖 1 通過命令行客戶端演示這些命令

交集、并集和差集保存命令的用法,如下圖 2 所示。

交集、并集和差集保存命令的用法

圖 2 交集、并集和差集保存命令的用法

這里的命令主要是求差集、并集和交集,并保存到新的集合中。至此就展示了表 1 中的所有命令,下面將在 Spring 中操作它們,代碼如下所示。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n420" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> // 請把 RedisTemplate 值序列化器設(shè)置為 StringRedisSerializer 測試該代碼片段
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
Set set = null;
// 將元素加入列表
redisTemplate.boundSetOps ("set1").add ("vl","v2","v3","v4","v5", "v6");
redisTemplate.boundSetOps ("set2").add( "v0","v2","v4","v6","v8");
//求集合長度
redisTemplate.opsForSet().size ("set1");
//求差集
set = redisTemplate.opsForSet().difference ("set1","set2");
//求并集
set = redisTemplate.opsForSet().intersect ("set1","set2");
//判斷是否集合中的元素
boolean exists = redisTemplate.opsForSet().isMember("set1", "vl");
//獲取集合所有元素
set = redisTemplate.opsForSet().members ("set1");
//從集合中隨機彈出一個元素
String val = (String)redisTemplate.opsForSet().pop("set1");
//隨機獲取一個集合的元素
val = (String) redisTemplate.opsForSet().randomMember("set1");
//隨機獲取2個集合的元素
List list = redisTemplate.opsForSet ().randomMembers ("set1", 2L);
//刪除一個集合的元素,參數(shù)可以是多個
redisTemplate.opsForSet().remove ("setl","v1");
//求兩個集合的并集
redisTemplate.opsForSet().union ("set1","set2");
//求兩個集合的差集,并保存到集合diff_set中
redisTemplate.opsForSet().differenceAndStore("set1", "set2", "diff_set");
//求兩個集合的交集,并保存到集合inter_set中
redisTemplate.opsForSet().intersectAndStore("set1","set2" "inter_set");
//求兩個集合的并集,并保存到集合union_set中
redisTemplate.opsForSet().unionAndStore ("set1", "set2", "union_set");</pre>

上面的注釋已經(jīng)較為詳細(xì)地描述了代碼的含義,這樣我們就可以在實踐中使用 Spring 操作 Redis 的集合了。

Zset 數(shù)據(jù)結(jié)構(gòu)和常用命令

有序集合(zset)和集合(set)類似,只是說它是有序的,和無序集合的主要區(qū)別在于每一個元素除了值之外,它還會多一個分?jǐn)?shù)。分?jǐn)?shù)是一個浮點數(shù),在 Java 中是使用雙精度表示的,根據(jù)分?jǐn)?shù),Redis 就可以支持對分?jǐn)?shù)從小到大或者從大到小的排序。

這里和無序集合一樣,對于每一個元素都是唯一的,但是對于不同元素而言,它的分?jǐn)?shù)可以一樣。元素也是 String 數(shù)據(jù)類型,也是一種基于 hash 的存儲結(jié)構(gòu)。

集合是通過哈希表實現(xiàn)的,所以添加、刪除、查找的復(fù)雜度都是 0(1)。集合中最大的成員數(shù)為 2 的 32 次方減 1(40 多億個成員),有序集合的數(shù)據(jù)結(jié)構(gòu)如圖 1 所示。

有序集合的數(shù)據(jù)結(jié)構(gòu)

圖 1 有序集合的數(shù)據(jù)結(jié)構(gòu)

有序集合是依賴 key 標(biāo)示它是屬于哪個集合,依賴分?jǐn)?shù)進(jìn)行排序,所以值和分?jǐn)?shù)是必須的,而實際上不僅可以對分?jǐn)?shù)進(jìn)行排序,在滿足一定的條件下,也可以對值進(jìn)行排序。

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

有些時候 Redis 借助數(shù)據(jù)區(qū)間的表示方法來表示包含或者不包含,比如在數(shù)學(xué)的區(qū)間表示中,[2,5] 表示包含 2,但是不包含 5 的區(qū)間。具體如表所示。

命 令 說 明 備 注
zadd key score1 value1 [score2 value2......] 向有序集合的 key,增加一個或者多個成員 如果不存在對應(yīng)的 key,則創(chuàng)建鍵為 key 的有序集合
zcard key 獲取有序集合的成員數(shù)
zcount key min max 根據(jù)分?jǐn)?shù)返回對應(yīng)的成員列表 min 為最小值,max 為最大值,默認(rèn)為包含 min 和 max 值,采用數(shù)學(xué)區(qū)間表示的方法,如果需要不包含,則在分?jǐn)?shù)前面加入“(”,注意不支持“[”表示
zincrby key increment member 給有序集合成員值為 member 的分?jǐn)?shù)增加 increment
zinterstore desKey numkeys key1 [key2 key3......] 求多個有序集合的交集,并且將結(jié)果保存到 desKey 中 numkeys 是一個整數(shù),表示多少個有序集合
zlexcount key min max 求有序集合 key 成員值在 min 和 max 的范圍 這里范圍為 key 的成員值,Redis 借助數(shù)據(jù)區(qū)間的表示方法,“[”表示包含該值,“(”表示不包含該值
zrange key start stop [withscores] 按照分值的大?。◤男〉酱螅┓祷爻蓡T,加入 start 和 stop 參數(shù)可以截取某一段返回。如果輸入可選項 withscores,則連同分?jǐn)?shù)一起返回 這里記集合最人長度為 len,則 Redis 會將集合排序后,形成一個從 0 到 len-1 的下標(biāo),然后根據(jù) start 和 stop 控制的下標(biāo)(包含 start 和 stop)返回
zrank key member 按從小到大求有序集合的排行 排名第一的為 0,第二的為 1……
zrangebylex key min max [limit offset count] 根據(jù)值的大小,從小到大排序,min 為最小值,max 為最大值;limit 選項可選,當(dāng) Redis 求出范圍集合后,會生產(chǎn)下標(biāo) 0 到 n,然后根據(jù)偏移量 offset 和限定返回數(shù) count,返回對應(yīng)的成員 這里范圍為 key 的成員值,Redis 借助數(shù)學(xué)區(qū)間的表示方法,“[”表示包含該值,“(”表示不包含該值
zrangebyscore key min max [withscores] [limit offset count] 根據(jù)分?jǐn)?shù)大小,從小到大求取范圍,選項 withscores 和 limit 請參考 zrange 命令和 zrangebylex 說明 根據(jù)分析求取集合的范圍。這里默認(rèn)包含 min 和 max,如果不想包含,則在參數(shù)前加入“(”, 注意不支持“[”表示
zremrangebyscore key start stop 根據(jù)分?jǐn)?shù)區(qū)間進(jìn)行刪除 按照 socre 進(jìn)行排序,然后排除 0 到 len-1 的下標(biāo),然后根據(jù) start 和 stop 進(jìn)行刪除,Redis 借助數(shù)學(xué)區(qū)間的表示方法,“[”表示包含該值,“(” 表示不包含該值
zremrangebyrank key start stop 按照分?jǐn)?shù)排行從小到大的排序刪除,從 0 開始計算
zremrangebylex key min max 按照值的分布進(jìn)行刪除
zrevrange key start stop [withscores] 從大到小的按分?jǐn)?shù)排序,參數(shù)請參見 zrange 與 zrange 相同,只是排序是從大到小
zrevrangebyscore key max min [withscores] 從大到小的按分?jǐn)?shù)排序,參數(shù)請參見 zrangebyscore 與 zrangebyscore 相同,只是排序是從大到小
zrevrank key member 按從大到小的順序,求元素的排行 排名第一位 0,第二位 1......
zscore key member 返回成員的分?jǐn)?shù)值 返回成員的分?jǐn)?shù)
zunionstore desKey numKeys key1 [key2 key3 key4......] 求多個有序集合的并集,其中 numKeys 是有序集合的個數(shù) ——

在對有序集合、下標(biāo)、區(qū)間的表示方法進(jìn)行操作的時候,需要十分小心命令,注意它是操作分?jǐn)?shù)還是值,稍有不慎就會出現(xiàn)問題。

這里命令比較多,也有些命令比較難使用,在使用的時候,務(wù)必要小心,不過好在我們使用 zset 的頻率并不是太高,下面是測試結(jié)果——有序集合命令展示,如圖所示。

有序集合命令展示

圖 2 有序集合命令展示

出演示的例子。測試代碼如下所示。

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n515" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> 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--;
// 計算分?jǐn)?shù)和值
Double score1 = Double.valueOf(i);
String value1 = "x" + i;
Double score2 = Double.valueOf(j);
String value2 = j % 2 == 1 ? "y" + j : "x" + j;
// 使用 Spring 提供的默認(rèn) 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);
// 統(tǒng)計總數(shù)
Long size = null;
size = redisTemplate.opsForZSet().zCard("set1");
// 計分?jǐn)?shù)為score,那么下面的方法就是求 3<=score<=6的元素
size = redisTemplate.opsForZSet().count("zset1", 3, 6);
Set set = null;
// 從下標(biāo)一開始截取5個元素,但是不返回分?jǐn)?shù),每一個元索是String
set = redisTemplate.opsForZSet().range("zset1", 1, 5);
printSet(set);
// 截取集合所有元素,并且對集合按分?jǐn)?shù)排序,并返回分?jǐn)?shù),每一個元素是TypedTuple
set = redisTemplate.opsForZSet().rangeWithScores("zset1", 0, -1);
printTypedTuple(set);
// 將zset1和zset2兩個集合的交集放入集合inter_zset
size = redisTemplate.opsForZSet().intersectAndStore("zset1", "zset2","inter_zset");
// 區(qū)間
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);
// 限制返回個數(shù)
Limit limit = Limit.limit();
// 限制返回個數(shù)
limit.count(4);
// 限制從第五個開始截取
limit.offset(5);
// 求區(qū)間內(nèi)的元素,并限制返回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);
// 刪除元素,返回刪除個數(shù)
size = redisTemplate.opsForZSet().remove("zset1", "x5", "x6");
System.err.println("delete = " + size);
// 按照排行刪除從0開始算起,這里將刪除第排名第2和第3的元素
size = redisTemplate.opsForZSet().removeRange("zset2", 1, 2);
// 獲取所有集合的元素和分?jǐn)?shù),以-1代表全部元素
set = redisTemplate.opsForZSet().rangeWithScores("zset2", 0, -1);
printTypedTuple(set);
// 刪除指定的元素
size = redisTemplate.opsForZSet().remove("zset2", "y5", "y3");
System.err.println(size);
// 給集合中的一個元素的分?jǐn)?shù)加上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();
    }</pre>

Redis HyperLogLog常用命令

基數(shù)是一種算法。舉個例子,一本英文著作由數(shù)百萬個單詞組成,你的內(nèi)存卻不足以存儲它們。

英文單詞本身是有限的,在這本書的幾百萬個單詞中有許許多多重復(fù)單詞,扣去重復(fù)的單詞,這本書中也就是幾千到一萬多個單詞而已,那么內(nèi)存就足夠存儲它。

比如數(shù)字集合 {1,2,5,7,9,1,5,9} 的基數(shù)集合為 {1,2,5,7,9} 那么基數(shù)(不重復(fù)元素)就是 5,基數(shù)的作用是評估大約需要準(zhǔn)備多少個存儲單元去存儲數(shù)據(jù),但是基數(shù)的算法一般會存在一定的誤差(一般是可控的)。Redis 對基數(shù)數(shù)據(jù)結(jié)構(gòu)的支持是從版本 2.8.9 開始的。

基數(shù)并不是存儲元素,存儲元素消耗內(nèi)存空間比較大,而是給某一個有重復(fù)元素的數(shù)據(jù)集合(一般是很大的數(shù)據(jù)集合)評估需要的空間單元數(shù),所以它沒有辦法進(jìn)行存儲,加上在工作中用得不多,所以簡要介紹一下 Redis 的 HyperLogLog 命令就可以了,如表所示。

命 令 說 明 備 注
pfadd key element 添加指定元素到 HyperLogLog 中 如果已經(jīng)存儲元索,則返回為 0,添加失敗
pfcount key 返回 HyperLogLog 的基數(shù)值
pfmerge desKey key1 [key2 key3...] 合并多個 HyperLogLog,并將其保存在 desKey 中

在命令行中演示一下它們,如圖 1 所示。

Redis的HyperLogLog命令演示

圖 1 Redis 的 HyperLogLog 命令演示

分析一下邏輯,首先往一個鍵為 h1 的 HyperLogLog 插入元素,讓其計算基數(shù),到了第 5 個命令“pfadd h1 a”的時候,由于在此以前已經(jīng)添加過,所以返回了 0。它的基數(shù)集合是 {a,b,c,d},故而求集合長度為 4;之后再添加了第二個基數(shù),它的基數(shù)集合是{a,z},所以在 h1 和 h2 合并為 h3 的時候,它的基數(shù)集合為 {a,b,c,d,z},所以求取它的基數(shù)就是 5。

在 Spring 中操作基數(shù),代碼如下所示。

<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="java" cid="n544" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 8px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">ApplicationContext applicationContext = new ClassPathXmlApplicationcontext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
redisTemplate.opsForHyperLogLog().add("HyperLogLog", "a", "b" , "c", "d", "a");
redisTemplate.opsForHyperLogLog().add("HyperLogLog2", "a"); redisTemplate.opsForHyperLogLog().add("HyperLogLog2", "z");
Long size = redisTemplate.opsForHyperLogLog().size("HyperLogLog");
System.err.println(size);
size = redisTemplate.opsForHyperLogLog().size("HyperLogLog2");
System.err.println(size);
redisTemplate.opsForHyperLogLog().union ("des_key","HyperLogLog","HyperLogLog2");
size = redisTemplate.opsForHyperLogLog().size("des_key");
System.err.println(size);</pre>

從上面的代碼可以看到,增加一個元素到基數(shù)中采用 add 方法,它可以是一個或者多個元素,而求基數(shù)大小則是采用了 size 方法,合并基數(shù)則采用了 union 方法,其第一個是目標(biāo)基數(shù)的 key,然后可以是一到多個 key。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容