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 所示。

圖 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 所示。

圖 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é)果如下:

圖 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 所示。

圖 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 所示。

圖 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)容:
hmset 命令,在 Java 的 API 中,是使用 map 保存多個鍵值對在先的。
hgetall 命令會返回所有的鍵值對,并保存到一個 map 對象中,如果 hash 結(jié)構(gòu)很大,那么要考慮它對 JVM 的內(nèi)存影響。
hincrby 和 hincrbyFloat 命令都采用 increment 方法,Spring 會識別它具體使用何種方法。
redisTemplate.opsForHash().values(key) 方法相當(dāng)于 hvals 命令,它會返回所有的值,并保存到一個 List 對象中;而 redisTemplate.opsForHash().keys(key) 方法相當(dāng)于 hkeys 命令,它會獲取所有的鍵,保存到一個 Set 對象中。
在 Spring 中使用 redisTemplate.opsForHash().putAll(key,map) 方法相當(dāng)于執(zhí)行了 hmset 命令,使用了 map,由于配置了默認(rèn)的序列化器為字符串,所以它也只會用字符串進(jìn)行轉(zhuǎn)化,這樣才能執(zhí)行對應(yīng)的數(shù)值加法,如果使用其他序列化器,則后面的命令可能會拋出異常。
在使用大的 hash 結(jié)構(gòu)時,需要考慮返回數(shù)據(jù)的大小,以避免返回太多的數(shù)據(jù),引發(fā) JVM 內(nèi)存溢出或者 Redis 的性能問題。
運行以上代碼,可以得到這樣的輸出結(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 所示。

圖 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 所示。

圖 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 所示。

圖 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 所示。

圖 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 所示。

圖 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。