springboot中RedisTemplate的使用

Redis 簡(jiǎn)介

Redis 是一個(gè)開(kāi)源(BSD 許可)、內(nèi)存存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)服務(wù)器,可用作數(shù)據(jù)庫(kù),高速緩存和消息隊(duì)列代理。它支持字符串、哈希表、列表、集合、有序集合等數(shù)據(jù)類型。內(nèi)置復(fù)制、Lua 腳本、LRU 收回、事務(wù)以及不同級(jí)別磁盤持久化功能,同時(shí)通過(guò) Redis Sentinel 提供高可用,通過(guò) Redis Cluster 提供自動(dòng)分區(qū)。

Redis 使用場(chǎng)景

微服務(wù)以及分布式被廣泛使用后,Redis 的使用場(chǎng)景就越來(lái)越多了,這里我羅列了主要的幾種場(chǎng)景。

  1. 分布式緩存:在分布式的系統(tǒng)架構(gòu)中,將緩存存儲(chǔ)在內(nèi)存中顯然不當(dāng),因?yàn)榫彺嫘枰c其他機(jī)器共享,這時(shí) Redis 便挺身而出了,緩存也是 Redis 使用最多的場(chǎng)景。
  2. 分布式鎖:在高并發(fā)的情況下,我們需要一個(gè)鎖來(lái)防止并發(fā)帶來(lái)的臟數(shù)據(jù),Java 自帶的鎖機(jī)制顯然對(duì)進(jìn)程間的并發(fā)并不好使,此時(shí)可以利用 Redis 單線程的特性來(lái)實(shí)現(xiàn)我們的分布式鎖,如何實(shí)現(xiàn),可以參考這篇文章。
  3. Session 存儲(chǔ)/共享:Redis 可以將 Session 持久化到存儲(chǔ)中,這樣可以避免由于機(jī)器宕機(jī)而丟失用戶會(huì)話信息。(個(gè)人認(rèn)為更重要的是因?yàn)榉?wù)集群的出現(xiàn),需要一個(gè)分布式Session來(lái)作為統(tǒng)一的會(huì)話
  4. 發(fā)布/訂閱:Redis 還有一個(gè)發(fā)布/訂閱的功能,您可以設(shè)定對(duì)某一個(gè) key 值進(jìn)行消息發(fā)布及消息訂閱,當(dāng)一個(gè) key 值上進(jìn)行了消息發(fā)布后,所有訂閱它的客戶端都會(huì)收到相應(yīng)的消息。這一功能最明顯的用法就是用作實(shí)時(shí)消息系統(tǒng)。
  5. 任務(wù)隊(duì)列:Redis 的 lpush+brpop 命令組合即可實(shí)現(xiàn)阻塞隊(duì)列,生產(chǎn)者客戶端使用 lrpush 從列表左側(cè)插入元素,多個(gè)消費(fèi)者客戶端使用 brpop 命令阻塞式的”搶”列表尾部的元素,多個(gè)客戶端保證了消費(fèi)的負(fù)載均衡和高可用性。
  6. 限速,接口訪問(wèn)頻率限制:比如發(fā)送短信驗(yàn)證碼的接口,通常為了防止別人惡意頻刷,會(huì)限制用戶每分鐘獲取驗(yàn)證碼的頻率,例如一分鐘不能超過(guò) 5 次。

當(dāng)然 Redis 的使用場(chǎng)景并不僅僅只有這么多,還有很多未列出的場(chǎng)景,如計(jì)數(shù)、排行榜等。

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

前面也提到過(guò),Redis 支持字符串、哈希表、列表、集合、有序集合五種數(shù)據(jù)類型的存儲(chǔ)。

字符串(string)

  • string類型是二進(jìn)制安全的。意思是redis的string可以包含任何數(shù)據(jù)。比如jpg圖片或者序列化的對(duì)象 。
  • string類型是Redis最基本的數(shù)據(jù)類型,一個(gè)鍵最大能存儲(chǔ)512MB。

string 這種數(shù)據(jù)結(jié)構(gòu)應(yīng)該是我們最為常用的。在 Redis 中 string 表示的是一個(gè)可變的字節(jié)數(shù)組,我們初始化字符串的內(nèi)容、可以拿到字符串的長(zhǎng)度,可以獲取 string 的子串,可以覆蓋 string 的子串內(nèi)容,可以追加子串。

Redis 的 string 類型數(shù)據(jù)結(jié)構(gòu)

如上圖所示,在 Redis 中我們初始化一個(gè)字符串時(shí),會(huì)采用預(yù)分配冗余空間的方式來(lái)減少內(nèi)存的頻繁分配,如圖 1 所示,實(shí)際分配的空間 capacity 一般要高于實(shí)際字符串長(zhǎng)度 len。如果您看過(guò) Java 的 ArrayList 的源碼相信會(huì)對(duì)此種模式很熟悉。

列表(list)

  • redis列表是簡(jiǎn)單的字符串列表,排序?yàn)椴迦氲捻樞颉A斜淼淖畲箝L(zhǎng)度為2^32-1。
  • redis的列表是使用鏈表實(shí)現(xiàn)的,這意味著,即使列表中有上百萬(wàn)個(gè)元素,增加一個(gè)元素到列表的頭部或尾部的操作都是在常量的時(shí)間完成。
  • 可以用列表獲取最新的內(nèi)容(像帖子,微博等),用ltrim很容易就會(huì)獲取最新的內(nèi)容,并移除舊的內(nèi)容。
  • 用列表可以實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式,生產(chǎn)者調(diào)用lpush添加項(xiàng)到列表中,消費(fèi)者調(diào)用rpop從列表中提取,如果沒(méi)有元素,則輪詢?nèi)カ@取,或者使用brpop等待生產(chǎn)者添加項(xiàng)到列表中。

Redis 中列表 list 采用的存儲(chǔ)結(jié)構(gòu)是雙向鏈表,由此可見(jiàn)其隨機(jī)定位性能較差,比較適合首位插入刪除。像 Java 中的數(shù)組一樣,Redis 中的列表支持通過(guò)下標(biāo)訪問(wèn),不同的是 Redis 還為列表提供了一種負(fù)下標(biāo),-1 表示倒數(shù)一個(gè)元素,-2 表示倒數(shù)第二個(gè)數(shù),依此類推。綜合列表首尾增刪性能優(yōu)異的特點(diǎn),通常我們使用 rpush/rpop/lpush/lpop 四條指令將列表作為隊(duì)列來(lái)使用。

List 類型數(shù)據(jù)結(jié)構(gòu)

如上圖所示,在列表元素較少的情況下會(huì)使用一塊連續(xù)的內(nèi)存存儲(chǔ),這個(gè)結(jié)構(gòu)是 ziplist,也即是壓縮列表。它將所有的元素緊挨著一起存儲(chǔ),分配的是一塊連續(xù)的內(nèi)存。當(dāng)數(shù)據(jù)量比較多的時(shí)候才會(huì)改成 quicklist。因?yàn)?strong>普通的鏈表需要的附加指針空間太大,會(huì)比較浪費(fèi)空間。比如這個(gè)列表里存的只是 int 類型的數(shù)據(jù),結(jié)構(gòu)上還需要兩個(gè)額外的指針 prev 和 next。所以 Redis 將鏈表和 ziplist 結(jié)合起來(lái)組成了 quicklist。也就是將多個(gè) ziplist 使用雙向指針串起來(lái)使用。這樣既滿足了快速的插入刪除性能,又不會(huì)出現(xiàn)太大的空間冗余。

哈希表(hash)

  • redis的哈希值是字符串字段和字符串之間的映射,是表示對(duì)象的完美數(shù)據(jù)類型。
  • 哈希中的字段數(shù)量沒(méi)有限制,所以可以在你的應(yīng)用程序以不同的方式來(lái)使用哈希。

hash 與 Java 中的 HashMap 差不多,實(shí)現(xiàn)上采用二維結(jié)構(gòu),第一維是數(shù)組,第二維是鏈表。hash 的 key 與 value 都存儲(chǔ)在鏈表中,而數(shù)組中存儲(chǔ)的則是各個(gè)鏈表的表頭。在檢索時(shí),首先計(jì)算 key 的 hashcode,然后通過(guò) hashcode 定位到鏈表的表頭,再遍歷鏈表得到 value 值。可能您比較好奇為啥要用鏈表來(lái)存儲(chǔ) key 和 value,直接用 key 和 value 一對(duì)一存儲(chǔ)不就可以了嗎?其實(shí)是因?yàn)橛行r(shí)候我們無(wú)法保證 hashcode 值的唯一,若兩個(gè)不同的 key 產(chǎn)生了相同的 hashcode,我們需要一個(gè)鏈表在存儲(chǔ)兩對(duì)鍵值對(duì),這就是所謂的 hash 碰撞。

集合(set)

  • redis集合是無(wú)序的字符串集合,集合中的值是唯一的,無(wú)序的。可以對(duì)集合執(zhí)行很多操作,例如,測(cè)試元素是否存在,對(duì)多個(gè)集合執(zhí)行交集、并集和差集等等。
  • 我們通常可以用集合存儲(chǔ)一些無(wú)關(guān)順序的,表達(dá)對(duì)象間關(guān)系的數(shù)據(jù),例如用戶的角色,可以用sismember很容易就判斷用戶是否擁有某個(gè)角色。
  • 在一些用到隨機(jī)值的場(chǎng)合是非常適合的(抽獎(jiǎng)),可以用 srandmember/spop 獲取/彈出一個(gè)隨機(jī)元素。
    同時(shí),使用@EnableCaching開(kāi)啟聲明式緩存支持,這樣就可以使用基于注解的緩存技術(shù)。注解緩存是一個(gè)對(duì)緩存使用的抽象,通過(guò)在代碼中添加下面的一些注解,達(dá)到緩存的效果。
    • @Cacheable:在方法執(zhí)行前Spring先查看緩存中是否有數(shù)據(jù),如果有數(shù)據(jù),則直接返回緩存數(shù)據(jù);沒(méi)有則調(diào)用方法并將方法返回值放進(jìn)緩存。(查)
    • @CachePut:將方法的返回值放到緩存中。(增/改)
    • @CacheEvict:刪除緩存中的數(shù)據(jù)。(刪)

熟悉 Java 的同學(xué)應(yīng)該知道 HashSet 的內(nèi)部實(shí)現(xiàn)使用的是 HashMap,只不過(guò)所有的 value 都指向同一個(gè)對(duì)象。Redis 的 Set 結(jié)構(gòu)也是一樣,它的內(nèi)部也使用 Hash 結(jié)構(gòu),所有的 value 都指向同一個(gè)內(nèi)部值。

有序集合(sorted set)

  • 有序集合由唯一的,不重復(fù)的字符串元素組成。有序集合中的每個(gè)元素都關(guān)聯(lián)了一個(gè)浮點(diǎn)值,稱為分?jǐn)?shù)。可以把有序集合zset看成hash和集合的混合體,分?jǐn)?shù)即為hash的key。
  • 有序集合中的元素是按序存儲(chǔ)的,不是請(qǐng)求時(shí)才排序的。

有時(shí)也被稱作 ZSet,是 Redis 中一個(gè)比較特別的數(shù)據(jù)結(jié)構(gòu),在有序集合中我們會(huì)給每個(gè)元素賦予一個(gè)權(quán)重,其內(nèi)部元素會(huì)按照權(quán)重進(jìn)行排序,我們可以通過(guò)命令查詢某個(gè)范圍權(quán)重內(nèi)的元素,這個(gè)特性在我們做一個(gè)排行榜的功能時(shí)可以說(shuō)非常實(shí)用了。其底層的實(shí)現(xiàn)使用了兩個(gè)數(shù)據(jù)結(jié)構(gòu), hash 和跳躍列表,hash 的作用就是關(guān)聯(lián)元素 value 和權(quán)重 score,保障元素 value 的唯一性,可以通過(guò)元素 value 找到相應(yīng)的 score 值。跳躍列表的目的在于給元素 value 排序,根據(jù) score 的范圍獲取元素列表。

在 Spring Boot 項(xiàng)目中使用 Redis

spring-data-redis針對(duì)jedis提供了如下功能:

1. 連接池自動(dòng)管理,提供了一個(gè)高度封裝的“RedisTemplate”類

2. 針對(duì)jedis客戶端中大量api進(jìn)行了歸類封裝,將同一類型操作封裝為operation接口

ValueOperations:簡(jiǎn)單K-V操作
SetOperations:set類型數(shù)據(jù)操作
ZSetOperations:zset類型數(shù)據(jù)操作
HashOperations:針對(duì)map類型的數(shù)據(jù)操作
ListOperations:針對(duì)list類型的數(shù)據(jù)操作

3. 提供了對(duì)key的“bound”(綁定)便捷化操作API,可以通過(guò)bound封裝指定的key,然后進(jìn)行一系列的操作而無(wú)須“顯式”的再次指定Key,即BoundKeyOperations:

BoundValueOperations
BoundSetOperations
BoundListOperations
BoundSetOperations
BoundHashOperations

4. 將事務(wù)操作封裝,有容器控制。

5. 針對(duì)數(shù)據(jù)的“序列化/反序列化”,提供了多種可選擇策略(RedisSerializer)

JdkSerializationRedisSerializer:POJO對(duì)象的存取場(chǎng)景,使用JDK本身序列化機(jī)制,將pojo類通過(guò)ObjectInputStream/ObjectOutputStream進(jìn)行序列化操作,最終redis-server中將存儲(chǔ)字節(jié)序列。是目前最常用的序列化策略。

StringRedisSerializer:Key或者value為字符串的場(chǎng)景,根據(jù)指定的charset對(duì)數(shù)據(jù)的字節(jié)序列編碼成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封裝。是最輕量級(jí)和高效的策略。

JacksonJsonRedisSerializer:jackson-json工具提供了javabean與json之間的轉(zhuǎn)換能力,可以將pojo實(shí)例序列化成json格式存儲(chǔ)在redis中,也可以將json格式的數(shù)據(jù)轉(zhuǎn)換成pojo實(shí)例。因?yàn)閖ackson工具在序列化和反序列化時(shí),需要明確指定Class類型,因此此策略封裝起來(lái)稍微復(fù)雜。【需要jackson-mapper-asl工具支持】

OxmSerializer:提供了將javabean與xml之間的轉(zhuǎn)換能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存儲(chǔ)的數(shù)據(jù)將是xml工具。不過(guò)使用此策略,編程將會(huì)有些難度,而且效率最低;不建議使用?!拘枰猻pring-oxm模塊的支持】

如果你的數(shù)據(jù)需要被第三方工具解析,那么數(shù)據(jù)應(yīng)該使用StringRedisSerializer而不是JdkSerializationRedisSerializer。

如果使用的是默認(rèn)的JdkSerializationRedisSerializer,注意一定要讓緩存的對(duì)象實(shí)現(xiàn)序列化接口用于序列化 。

關(guān)于key的設(shè)計(jì)

key的存活時(shí)間:

無(wú)論什么時(shí)候,只要有可能就利用key超時(shí)的優(yōu)勢(shì)。

關(guān)系型數(shù)據(jù)庫(kù)的redis

1: 把表名轉(zhuǎn)換為key前綴 如, tag:
2: 第2段放置用于區(qū)分區(qū)key的字段--對(duì)應(yīng)mysql中的主鍵的列名,如userid
3: 第3段放置主鍵值,如2,3,4...., a , b ,c
4: 第4段,寫要存儲(chǔ)的列名
例:user:userid:9:username

使用前配置

添加 Redis 依賴

<!-- 配置使用 redis 啟動(dòng)器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--springboot 集成 junit 起步依賴-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.1.6.RELEASE</version>
    <scope>test</scope>
</dependency>

Spring Boot 中配置 Redis: application.properties

spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis 數(shù)據(jù)庫(kù)索引(默認(rèn)為 0)
spring.redis.database=0 
# Redis 服務(wù)器連接端口
# Redis 服務(wù)器連接密碼(默認(rèn)為空)
spring.redis.password=123456
#連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)
spring.redis.jedis.pool.max-active=8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
spring.redis.jedis.pool.max-wait=-1
# 連接池中的最大空閑連接
spring.redis.jedis.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.jedis.pool.min-idle=0
# 連接超時(shí)時(shí)間(毫秒) 如果連接超時(shí)時(shí)間不設(shè)置,這要注釋掉配置而不能=0,否則會(huì)報(bào)連接超時(shí)錯(cuò)誤:Command timed out after no timeout,,有超時(shí)時(shí)間最后設(shè)置為200以上
spring.redis.timeout=300

RedisTemplate 的配置

@Configuration
public class RedisConfig {

    /**
     * 默認(rèn)是JDK的序列化策略,這里配置redisTemplate采用的是Jackson2JsonRedisSerializer的序列化策略
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        //使用Jackson2JsonRedisSerializer來(lái)序列化和反序列化redis的value值(默認(rèn)使用JDK的序列化方式)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修飾符范圍,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
        // 指定序列化輸入的類型,類必須是非final修飾的,final修飾的類,比如String,Integer等會(huì)拋出異常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 配置連接工廠
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //使用StringRedisSerializer來(lái)序列化和反序列化redis的key值
        //redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        // 值采用json序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /***
     * stringRedisTemplate默認(rèn)采用的是String的序列化策略
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }
}

另外一直版本:key采用String序列化,value使用jackson。

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

RedisTemplate

Spring Boot 的 spring-boot-starter-data-redis 為 Redis 的相關(guān)操作提供了一個(gè)高度封裝的 RedisTemplate 類,而且對(duì)每種類型的數(shù)據(jù)結(jié)構(gòu)都進(jìn)行了歸類,將同一類型操作封裝為 operation 接口。RedisTemplate 對(duì)五種數(shù)據(jù)結(jié)構(gòu)分別定義了操作,如下所示:

  • 操作字符串:redisTemplate.opsForValue()
  • 操作 Hash:redisTemplate.opsForHash()
  • 操作 List:redisTemplate.opsForList()
  • 操作 Set:redisTemplate.opsForSet()
  • 操作 ZSet:redisTemplate.opsForZSet()

但是對(duì)于 string 類型的數(shù)據(jù),Spring Boot 還專門提供了 StringRedisTemplate 類,而且官方也建議使用該類來(lái)操作 String 類型的數(shù)據(jù)。那么它和 RedisTemplate 又有啥區(qū)別呢?

  1. RedisTemplate 是一個(gè)泛型類,而 StringRedisTemplate 不是,后者只能對(duì)鍵和值都為 String 類型的數(shù)據(jù)進(jìn)行操作,而前者則可以操作任何類型。
  2. 兩者的數(shù)據(jù)是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的數(shù)據(jù),RedisTemplate 只能管理 RedisTemplate 中 的數(shù)據(jù)。

實(shí)踐

//用于指定junit運(yùn)行環(huán)境,是junit提供給其他框架測(cè)試環(huán)境接口擴(kuò)展,為了便于使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用于加載ApplicationContext,啟動(dòng)spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {

    @Autowired
    private RedisTemplate redisTemplate;
    //因?yàn)閟pring自動(dòng)注入管理了bean容器,直接用@Autowired取即可
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 操作字符串
     */
    @Test
    public void testString() {
        //設(shè)置值
        stringRedisTemplate.opsForValue().set("String", "Mao");
        //獲取值
        String string = stringRedisTemplate.opsForValue().get("String");

        //設(shè)置值且設(shè)置超時(shí)時(shí)間
        stringRedisTemplate.opsForValue().set("Middle", "Yu", 3, TimeUnit.MINUTES);
        String middle = stringRedisTemplate.opsForValue().get("Middle");
        System.out.println(middle);

        //刪除數(shù)據(jù)
        Boolean isDelete = stringRedisTemplate.delete("String");
        System.out.println(isDelete ? "Yes" : "No");
    }

    /**
     * 操作列表
     */
    @Test
    public void testList() {
        ListOperations listOp = redisTemplate.opsForList();
        //往 List 左側(cè)插入一個(gè)元素
        listOp.leftPush("namelist", "mike");
        listOp.leftPush("namelist", "kim");
        //往 List 右側(cè)插入一個(gè)元素
        listOp.rightPush("namelist", "jimmy");
        listOp.rightPush("namelist", "chuck");
        //List 大小
        Long size = listOp.size("namelist");
        //遍歷整個(gè)List
        List namelist1 = listOp.range("namelist", 0, size);
        //遍歷整個(gè)List,-1表示倒數(shù)第一個(gè)即最后一個(gè)
        List namelist = listOp.range("namelist", 0, -1);
        System.out.println(JSON.toJSONString(namelist));
        //從 List 左側(cè)取出第一個(gè)元素,并移除
        Object name1 = listOp.leftPop("namelist", 200, TimeUnit.MILLISECONDS);
        System.out.println("is kim:" + name1.equals("kim"));
        //從 List 右側(cè)取出第一個(gè)元素,并移除
        Object name2 = listOp.rightPop("namelist");
        System.out.println("is chuck:" + name2.equals("chuck"));
    }

    /**
     * 操作 Hash
     */
    @Test
    public void testHash() {
        //添加泛型方便操作和返回想要的具體類型
        HashOperations<String, String, Integer> hashOp = redisTemplate.opsForHash();
        //Hash 中新增元素。
        hashOp.put("score", "Mike", 10);
        hashOp.put("score", "Jimmy", 9);
        hashOp.put("score", "Kim", 8);
        //判斷指定 key 對(duì)應(yīng)的 Hash 中是否存在指定的 map 鍵
        Assert.assertTrue(hashOp.hasKey("score", "Kim"));
        //獲取指定 key 對(duì)應(yīng)的 Hash 中指定鍵的值
        Integer kim = hashOp.get("score", "Kim");
        System.out.println("kim score:" + kim);
        //獲取hash表所有的key集合
        Set<String> name = hashOp.keys("score");
        System.out.println(JSON.toJSONString(name));
        //獲取hash表所有的values集合
        List<Integer> score = hashOp.values("score");
        System.out.println(JSON.toJSONString(score));
        //獲取"score"對(duì)應(yīng)的hash表Map
        Map<String, Integer> map = hashOp.entries("score");
        System.out.println(JSON.toJSONString(map));
        //刪除指定 key 對(duì)應(yīng) Hash 中指定鍵的鍵值對(duì)
        hashOp.delete("score", "Mike");
        //如果要?jiǎng)h除整個(gè)hash表,要用redisTemplate.delete("score")方法,否則報(bào)錯(cuò):Fields must not be empty
        //hashOp.delete("score");
        //刪除整個(gè)hash表
        redisTemplate.delete("score");
        Map<String, Integer> map1 = hashOp.entries("score");
        System.out.println(JSON.toJSONString(map1));
    }

    /**
     * 操作集合
     */
    @Test
    public void testSet() {
        SetOperations<String, String> setOp = redisTemplate.opsForSet();
        //向集合中添加元素,set元素具有唯一性
        setOp.add("city", "quanzhou", "newyork", "paris", "hongkong", "hongkong");
        Long size = setOp.size("city");
        System.out.println("city size:" + size);
        //獲取集合中的元素
        Set city = setOp.members("city");
        System.out.println(JSON.toJSONString(city));
        //移除集合中的元素,可以一個(gè)或多個(gè)
        setOp.remove("city", "paris");
        //判斷是否是集合中的元素
        Boolean isMember = setOp.isMember("city", "paris");
        System.out.println("paris is in city:" + isMember);
        //移除并返回集合中的一個(gè)隨機(jī)元素
        String city1 = setOp.pop("city");
        System.out.println(city1);
    }

    /**
     * 操作有序集合
     */
    @Test
    public void testZSet() {
        ZSetOperations<String, String> zSetOp = redisTemplate.opsForZSet();
        zSetOp.add("zcity", "beijing", 100);
        zSetOp.add("zcity", "shanghai", 95);
        zSetOp.add("zcity", "guangzhou", 75);
        zSetOp.add("zcity", "shenzhen", 85);
        //獲取變量指定區(qū)間的元素。0, -1表示全部
        Set<String> zcity = zSetOp.range("zcity", 0, -1);
        System.out.println(JSON.toJSONString(zcity));
        //通過(guò)分?jǐn)?shù)返回有序集合指定區(qū)間內(nèi)的成員,其中有序集成員按分?jǐn)?shù)值遞增(從小到大)順序排列
        Set<String> byScore = zSetOp.rangeByScore("zcity", 85, 100);
        System.out.println(JSON.toJSONString(byScore));
        //獲取有序集合的成員數(shù)
        Long aLong = zSetOp.zCard("zcity");
        System.out.println("zcity size: " + aLong);

        ZSetOperations<String, Integer> zSetOp1 = redisTemplate.opsForZSet();
        zSetOp1.add("board", 1, 100);
        zSetOp1.add("board", 2, 100);
        zSetOp1.add("board", 3, 100);
        zSetOp1.add("board", 4, 100);
        Set<Integer> board = zSetOp1.range("board", 0, -1);
        System.out.println(JSON.toJSONString(board));
        RedisZSetCommands.Range range = new RedisZSetCommands.Range();
        //less than
        range.lt("3");
        RedisZSetCommands.Limit limit = new RedisZSetCommands.Limit();
        limit.count(1);
        limit.offset(1);
        //用于獲取滿足非 score 的排序取值。這個(gè)排序只有在有相同分?jǐn)?shù)的情況下才能使用,如果有不同的分?jǐn)?shù)則返回值不確定。
        //rangeByLex應(yīng)用在數(shù)值上比較
        Set<Integer> set = zSetOp1.rangeByLex("board", range);
        System.out.println(JSON.toJSONString(set));
        //用于獲取滿足非 score 的設(shè)置下標(biāo)開(kāi)始的長(zhǎng)度排序取值。
        Set<Integer> setlmt = zSetOp1.rangeByLex("board", range, limit);
        System.out.println(JSON.toJSONString(setlmt));
    }

    /**
     * 分布式鎖
     */
    @Test
    public void testLock() {
        String value = UUID.randomUUID().toString();
        Boolean lock = lock("buy", value,120L, TimeUnit.SECONDS);
        if (!lock) {
            System.out.println("can't lock buy");
        }
        try {
            Thread.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Boolean unLock = unLock("buy",value);
        if (!unLock) {
            System.out.println("can't unlock buy");
        }
    }

    public Boolean lock(String key,String value, Long timeout, TimeUnit timeUnit) {
        Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.set(key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")),
                        Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
        return lockStat;
    }

    public Boolean unLock(String key,String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        boolean unLockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
                        key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8"))));
        return unLockStat;
    }
    
    @Test
    public void boundTest(){
        BoundListOperations bound = redisTemplate.boundListOps("bound");
        bound.leftPush("haha");
        bound.rightPush("hehe");
        List list = bound.range(0, -1);
        System.out.println(JSON.toJSONString(list));
    }
}
報(bào)錯(cuò)1:
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR Client sent AUTH, but no password is set
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135)
    
A:這是因?yàn)榉?wù)器沒(méi)有設(shè)置密碼,客戶端設(shè)置了密碼去連接,導(dǎo)致連接失敗。我們?cè)趩?dòng)服務(wù)器時(shí)通過(guò)命令(config set requirepass "123456")手動(dòng)設(shè)置了密碼,一旦Redis服務(wù)器關(guān)閉后,重新啟動(dòng)加載了Redis啟動(dòng)的配置,因?yàn)閱?dòng)配置菜單(redis-server.exe redis.windows.conf)中沒(méi)有配置密碼,所以重新啟動(dòng)的redis是沒(méi)有設(shè)置密碼的,這樣就會(huì)報(bào)錯(cuò)。
解決方法:如果要設(shè)置密碼則要在Redis啟動(dòng)的配置文件中修改添加;或者如果不設(shè)置密碼則客戶端連接時(shí)application.properties不要設(shè)置密碼連接
# Redis 服務(wù)器連接密碼(默認(rèn)為空)
#spring.redis.password=123456
報(bào)錯(cuò)2:
org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after no timeout at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70)
A:這是因?yàn)榭蛻舳诉B接時(shí)application.properties中設(shè)置了連接超時(shí)時(shí)間為0導(dǎo)致的.
解決方法:
# 連接超時(shí)時(shí)間(毫秒) 如果連接超時(shí)時(shí)間不設(shè)置,這要注釋掉配置而不能=0,否則會(huì)報(bào)連接超時(shí)錯(cuò)誤:Command timed out after no timeout,有超時(shí)時(shí)間最后設(shè)置為200以上
spring.redis.timeout=300
org.springframework.data.redis.RedisSystemException: Unknown redis exception; nested exception is java.lang.IllegalArgumentException: Fields must not be empty at org.springframework.data.redis.FallbackExceptionTranslationStrategy.getFallback(FallbackExceptionTranslationStrategy.java:53)

A:如果要?jiǎng)h除整個(gè)hash表,要用redisTemplate.delete("score")方法,否則報(bào)錯(cuò):Fields must not be empty
解決方法:使用redisTemplate方法,HashOperations的delete是用于刪除hash表字段用
        HashOperations<String,String,Integer> hashOp = redisTemplate.opsForHash();
        //hashOp.delete("score");
        redisTemplate.delete("score");

關(guān)于 Redis 的幾個(gè)經(jīng)典問(wèn)題

緩存與數(shù)據(jù)庫(kù)一致性問(wèn)題

對(duì)于既有數(shù)據(jù)庫(kù)操作又有緩存操作的接口,一般分為兩種執(zhí)行順序。

  1. 先操作數(shù)據(jù)庫(kù),再操作緩存。這種情況下如果數(shù)據(jù)庫(kù)操作成功,緩存操作失敗就會(huì)導(dǎo)致緩存和數(shù)據(jù)庫(kù)不一致。
  2. 第二種情況就是先操作緩存再操作數(shù)據(jù)庫(kù),這種情況下如果緩存操作成功,數(shù)據(jù)庫(kù)操作失敗也會(huì)導(dǎo)致數(shù)據(jù)庫(kù)和緩存不一致。

大部分情況下,我們的緩存理論上都是需要可以從數(shù)據(jù)庫(kù)恢復(fù)出來(lái)的,所以基本上采取第一種順序都是不會(huì)有問(wèn)題的。針對(duì)那些必須保證數(shù)據(jù)庫(kù)和緩存一致的情況,通常是不建議使用緩存的。

緩存穿透問(wèn)題

緩存擊穿表示惡意用戶頻繁的模擬請(qǐng)求緩存中不存在的數(shù)據(jù),以致這些請(qǐng)求短時(shí)間內(nèi)直接落在了數(shù)據(jù)庫(kù)上,導(dǎo)致數(shù)據(jù)庫(kù)性能急劇下降,最終影響服務(wù)整體的性能。這個(gè)在實(shí)際項(xiàng)目很容易遇到,如搶購(gòu)活動(dòng)、秒殺活動(dòng)的接口 API 被大量的惡意用戶刷,導(dǎo)致短時(shí)間內(nèi)數(shù)據(jù)庫(kù)宕機(jī)。對(duì)于緩存擊穿的問(wèn)題,有以下幾種解決方案,這里只做簡(jiǎn)要說(shuō)明。

  1. 使用互斥鎖排隊(duì)。當(dāng)從緩存中獲取數(shù)據(jù)失敗時(shí),給當(dāng)前接口加上鎖,從數(shù)據(jù)庫(kù)中加載完數(shù)據(jù)并寫入后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試。(數(shù)據(jù)庫(kù)取數(shù)據(jù)時(shí)加鎖)
  2. 使用布隆過(guò)濾器。將所有可能存在的數(shù)據(jù)緩存放到布隆過(guò)濾器中,當(dāng)黑客訪問(wèn)不存在的緩存時(shí)迅速返回避免緩存及 DB 掛掉。

緩存雪崩問(wèn)題

在短時(shí)間內(nèi)有大量緩存失效,如果這期間有大量的請(qǐng)求發(fā)生同樣也有可能導(dǎo)致數(shù)據(jù)庫(kù)發(fā)生宕機(jī)。在 Redis 機(jī)群的數(shù)據(jù)分布算法上如果使用的是傳統(tǒng)的 hash 取模算法,在增加或者移除 Redis 節(jié)點(diǎn)的時(shí)候就會(huì)出現(xiàn)大量的緩存臨時(shí)失效的情形。

  1. 像解決緩存穿透一樣加鎖排隊(duì)。
  2. 建立備份緩存,緩存 A 和緩存 B,A 設(shè)置超時(shí)時(shí)間,B 不設(shè)值超時(shí)時(shí)間,先從 A 讀緩存,A 沒(méi)有讀 B,并且更新 A 緩存和 B 緩存。
  3. 計(jì)算數(shù)據(jù)緩存節(jié)點(diǎn)的時(shí)候采用一致性 hash 算法,這樣在節(jié)點(diǎn)數(shù)量發(fā)生改變時(shí)不會(huì)存在大量的緩存數(shù)據(jù)需要遷移的情況發(fā)生。
描述
brpop : block right pop 
BRPOP key1 [key2 ] timeout 
移出并獲取列表的最后一個(gè)元素, 如果列表沒(méi)有元素會(huì)阻塞列表直到等待超時(shí)或發(fā)現(xiàn)可彈出元素為止。

問(wèn)題

Q:布隆過(guò)濾器是什么?有什么用?怎么使用?

大白話布隆過(guò)濾器

Q:我們?cè)诰帉慠edisConfig配置類時(shí)是否繼承于CachingConfigurerSupport類有什么區(qū)別?為什么有的繼承了有的不選擇繼承,是繼承了的話可以結(jié)合Springboot的@EnableCaching的注解開(kāi)啟緩存么?以便使用如下注解自動(dòng)開(kāi)啟緩存么?那在項(xiàng)目中是使用好還是不使用好?

開(kāi)啟緩存后注解使用參考:

注解緩存的使用
@Cacheable:在方法執(zhí)行前Spring先查看緩存中是否有數(shù)據(jù),如果有數(shù)據(jù),則直接返回緩存數(shù)據(jù);沒(méi)有則調(diào)用方法并將方法返回值放進(jìn)緩存。
@CachePut:將方法的返回值放到緩存中。
@CacheEvict:刪除緩存中的數(shù)據(jù)。

Q:Spring Boot中混合使用StringRedisTemplate和RedisTemplate的坑——存儲(chǔ)到Redis的數(shù)據(jù)取不到值。

A:因?yàn)樗瑫r(shí)使用了StringRedisTemplate和RedisTemplate在Redis中存儲(chǔ)和讀取數(shù)據(jù)。它們最重要的一個(gè)區(qū)別就是默認(rèn)采用的序列化方式不同。StringRedisTemplate采用的是RedisSerializer.string()來(lái)序列化Redis中存儲(chǔ)數(shù)據(jù)的Key ;RedisTemplate使用的序列化類為defaultSerializer,默認(rèn)情況下為JdkSerializationRedisSerializer。如果未指定Key的序列化類,keySerializer與defaultSerializer采用相同的序列化類。

解決方法:需要指定統(tǒng)一的Key的序列化處理類,比如在RedisTemplate序列化時(shí)指定與StringRedisTemplate相同的類。 也就是對(duì)他們的序列化都采用StringRedisSerializer。

  StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

Q:Redis的brpop命令對(duì)應(yīng)RedisTemplate中的什么方法?

其實(shí)可以寫個(gè)死循環(huán)調(diào)用rightPop(K key)方法,當(dāng)獲取到數(shù)據(jù)時(shí)才跳出循環(huán)即可。當(dāng)然要注意接口超時(shí)的情況。所以直接使用超時(shí)方法就是阻塞調(diào)用bRPop。

@Override
    public V rightPop(K key, long timeout, TimeUnit unit) {

        int tm = (int) TimeoutUtils.toSeconds(timeout, unit);

        return execute(new ValueDeserializingRedisCallback(key) {

            @Override
            protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
                List<byte[]> bRPop = connection.bRPop(tm, rawKey);
                return (CollectionUtils.isEmpty(bRPop) ? null : bRPop.get(1));
            }
        }, true);
    }

Q:單元測(cè)試時(shí)@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class)兩個(gè)注解的作用?

A:如下。

1)@RunWith:用于指定junit運(yùn)行環(huán)境,是junit提供給其他框架測(cè)試環(huán)境接口擴(kuò)展,為了便于使用spring的依賴注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作為Junit測(cè)試環(huán)境。

2)@ContextConfiguration({"classes=Congfig.clsss",classpath:applicationContext.xml"}) 這里可以用classes來(lái)直接導(dǎo)入同包下寫的配置類。或者導(dǎo)入配置文件。

3)@SpringBootTest替代了spring-test中的@ContextConfiguration注解,目的是加載ApplicationContext,啟動(dòng)spring容器。因?yàn)锧SpringBootTest包含了讀取配置文件的@ContextConfiguration注解。

springboot使用單元測(cè)試步驟:

1、引入依賴

<!--springboot 集成 junit 起步依賴-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.1.6.RELEASE</version>
    <scope>test</scope>
</dependency>

2、編寫測(cè)試類,注意加注解@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class),模板如下:

//用于指定junit運(yùn)行環(huán)境,是junit提供給其他框架測(cè)試環(huán)境接口擴(kuò)展,為了便于使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用于加載ApplicationContext,啟動(dòng)spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {
    @Autowired
    private RedisTemplate redisTemplate;
    //因?yàn)閟pring自動(dòng)注入管理了bean容器,直接用@Autowired取即可
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testString(){
        //設(shè)置值
        stringRedisTemplate.opsForValue().set("String","Mao");
    }
}

Q:緩存并發(fā)問(wèn)題,這里的并發(fā)指的是多個(gè) Redis 的客戶端同時(shí) set 值引起的并發(fā)問(wèn)題。比較有效的解決方案就是把 set 操作放在隊(duì)列中使其串行化,必須得一個(gè)一個(gè)執(zhí)行。我的疑問(wèn)是,redis本身就是單線程串行執(zhí)行的,怎么會(huì)有并發(fā)問(wèn)題呢?

A:我們說(shuō)的緩存并發(fā)指的是多個(gè)Redis客戶端同時(shí)SET Key時(shí)會(huì)引起并發(fā)問(wèn)題。我們知道,Redis是單線程的,在多個(gè)Client并發(fā)操作時(shí),秉承“先發(fā)起先執(zhí)行”的原則,其它的處于阻塞狀態(tài)。

緩存并發(fā)問(wèn)題其實(shí)主要指的是讀取數(shù)據(jù)庫(kù)數(shù)據(jù)的并發(fā)操作問(wèn)題。

常見(jiàn)緩存并發(fā)有兩種場(chǎng)景:

  • 緩存過(guò)期后會(huì)從后端數(shù)據(jù)庫(kù)查詢數(shù)據(jù)然后存入Redis緩存,但在高并發(fā)情況下可能在還沒(méi)來(lái)得及將庫(kù)中查出來(lái)的數(shù)據(jù)存入Redis時(shí),其它Client又從數(shù)據(jù)庫(kù)里查詢數(shù)據(jù)再存入Redis了。這樣一來(lái)會(huì)造成多個(gè)請(qǐng)求并發(fā)的從數(shù)據(jù)庫(kù)獲取數(shù)據(jù),對(duì)后端數(shù)據(jù)庫(kù)會(huì)造成壓力。
  • 在高并發(fā)場(chǎng)景下,某個(gè)Key正在更新時(shí),可能有很多Client在獲取此Key的數(shù)據(jù),這樣會(huì)導(dǎo)致“臟數(shù)據(jù)”。

如何解決緩存并發(fā)問(wèn)題呢?

1、加鎖。我們常借助“鎖”來(lái)實(shí)現(xiàn),具體實(shí)現(xiàn)邏輯為:

在更新緩存或者讀取過(guò)期緩存的情況下,我們先獲取“鎖”,當(dāng)完成了緩存數(shù)據(jù)的更新后再釋放鎖,這樣一來(lái),其它的請(qǐng)求需要在釋放鎖之后執(zhí)行,會(huì)犧牲一點(diǎn)時(shí)間。

2、異步隊(duì)列串行執(zhí)行。把 set 操作放在隊(duì)列中使其串行化,必須得一個(gè)一個(gè)執(zhí)行。如通過(guò)消息中間件異步執(zhí)行。

3、使用類似SQL的樂(lè)觀鎖機(jī)制 。解決途徑是在并發(fā)寫入Redis緩存時(shí),用要寫入數(shù)據(jù)的版本號(hào)和時(shí)間戳與Redis中的數(shù)據(jù)進(jìn)行對(duì)比,如果寫入的數(shù)據(jù)時(shí)間戳或者版本號(hào) 比Redis 高,則寫入Redis中;否則就不寫入。

參考

了解 Redis 并在 Spring Boot 項(xiàng)目中使用 Redis——IBM

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

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