springboot中RedisTemplate的使用

卡斯特梅的雨傘發表於2020-08-13

springboot中RedisTemplate的使用

參考

瞭解 Redis 並在 Spring Boot 專案中使用 Redis——以IBM為學習模板

springboot之使用redistemplate優雅地操作redis——@EnableCaching開啟快取

springboot整合redis——redisTemplate的使用——@EnableCaching開啟快取

Spring Boot 中 Redis 的使用——共享 Session

使用 Spring Boot AOP 實現 Web 日誌處理和分散式鎖

Redis分散式鎖正確的實現方法

API功能介紹:

RedisTemplate提供的API列表

RedisTemplate 用法

Redis 命令參考

Redis 學習路線


問題

Q:springboot中RedisTemplate的使用?

Q:RedisTemplate的是一定要配置麼?不配置也能使用,配置RedisTemplate是為了我們能自定義其序列化方式麼?不配置的話表示預設採用其預設的配置序列化方式?

Q:布隆過濾器是什麼?有什麼用?怎麼使用?

大白話布隆過濾器

Q:我們在編寫RedisConfig配置類時是否繼承於CachingConfigurerSupport類有什麼區別?為什麼有的繼承了有的不選擇繼承,是繼承了的話可以結合Springboot的@EnableCaching的註解開啟快取麼?以便使用如下註解自動開啟快取麼?那在專案中是使用好還是不使用好?

開啟快取後註解使用參考:

註解快取的使用
@Cacheable:在方法執行前Spring先檢視快取中是否有資料,如果有資料,則直接返回快取資料;沒有則呼叫方法並將方法返回值放進快取。
@CachePut:將方法的返回值放到快取中。
@CacheEvict:刪除快取中的資料。

Q:Spring Boot中混合使用StringRedisTemplate和RedisTemplate的坑——儲存到Redis的資料取不到值。

A:因為他同時使用了StringRedisTemplate和RedisTemplate在Redis中儲存和讀取資料。它們最重要的一個區別就是預設採用的序列化方式不同。StringRedisTemplate採用的是RedisSerializer.string()來序列化Redis中儲存資料的Key ;RedisTemplate使用的序列化類為defaultSerializer,預設情況下為JdkSerializationRedisSerializer。如果未指定Key的序列化類,keySerializer與defaultSerializer採用相同的序列化類。

解決方法:需要指定統一的Key的序列化處理類,比如在RedisTemplate序列化時指定與StringRedisTemplate相同的類。

Q:Redis的brpop命令對應RedisTemplate中的什麼方法?

Q:雙向連結串列資料結構的瞭解?

Q:單元測試時@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class)兩個註解的作用?

A:如下。

1)@RunWith:用於指定junit執行環境,是junit提供給其他框架測試環境介面擴充套件,為了便於使用spring的依賴注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作為Junit測試環境。

2)@ContextConfiguration({"classes=Congfig.clsss",classpath:applicationContext.xml"}) 這裡可以用classes來直接匯入同包下寫的配置類。或者匯入配置檔案。

3)@SpringBootTest替代了spring-test中的@ContextConfiguration註解,目的是載入ApplicationContext,啟動spring容器。

springboot使用單元測試步驟:

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、編寫測試類,注意加註解@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class),模板如下:

//用於指定junit執行環境,是junit提供給其他框架測試環境介面擴充套件,為了便於使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用於載入ApplicationContext,啟動spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {
    @Autowired
    private RedisTemplate redisTemplate;
	//因為spring自動注入管理了bean容器,直接用@Autowired取即可
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

Q:快取併發問題,這裡的併發指的是多個 Redis 的客戶端同時 set 值引起的併發問題。比較有效的解決方案就是把 set 操作放在佇列中使其序列化,必須得一個一個執行。我的疑問是,redis本身就是單執行緒序列執行的,怎麼會有併發問題呢?

A:我們說的快取併發指的是多個Redis客戶端同時SET Key時會引起併發問題。我們知道,Redis是單執行緒的,在多個Client併發操作時,秉承“先發起先執行”的原則,其它的處於阻塞狀態。

常見快取併發有兩種場景:

  • 快取過期後會從後端資料庫查詢資料然後存入Redis快取,但在高併發情況下可能在還沒來得及將庫中查出來的資料存入Redis時,其它Client又從資料庫裡查詢資料再存入Redis了。這樣一來會造成多個請求併發的從資料庫獲取資料,對後端資料庫會造成壓力。
  • 在高併發場景下,某個Key正在更新時,可能有很多Client在獲取此Key的資料,這樣會導致“髒資料”。

如何解決快取併發問題呢?

1、加鎖。我們常藉助“鎖”來實現,具體實現邏輯為:

在更新快取或者讀取過期快取的情況下,我們先獲取“鎖”,當完成了快取資料的更新後再釋放鎖,這樣一來,其它的請求需要在釋放鎖之後執行,會犧牲一點時間。

2、非同步佇列序列執行。把 set 操作放在佇列中使其序列化,必須得一個一個執行。如通過訊息中介軟體非同步執行。

3、使用類似SQL的樂觀鎖機制  。解決途徑是在併發寫入Redis快取時,用要寫入資料的版本號和時間戳 與 Redis中的資料進行對比,如果寫入的資料時間戳或者版本號 比Redis 高,則寫入Redis中;否則就不寫入。


Redis 簡介

Redis 是一個開源(BSD 許可)、記憶體儲存的資料結構伺服器,可用作資料庫,快取記憶體和訊息佇列代理。它支援字串、雜湊表、列表、集合、有序集合等資料型別。內建複製、Lua 指令碼、LRU 收回、事務以及不同級別磁碟持久化功能,同時通過 Redis Sentinel 提供高可用,通過 Redis Cluster 提供自動分割槽。

Redis 使用場景

微服務以及分散式被廣泛使用後,Redis 的使用場景就越來越多了,這裡我羅列了主要的幾種場景。

  1. 分散式快取:在分散式的系統架構中,將快取儲存在記憶體中顯然不當,因為快取需要與其他機器共享,這時 Redis 便挺身而出了,快取也是 Redis 使用最多的場景。
  2. 分散式鎖:在高併發的情況下,我們需要一個鎖來防止併發帶來的髒資料,Java 自帶的鎖機制顯然對程式間的併發並不好使,此時可以利用 Redis 單執行緒的特性來實現我們的分散式鎖,如何實現,可以參考這篇文章。
  3. Session 儲存/共享:Redis 可以將 Session 持久化到儲存中,這樣可以避免由於機器當機而丟失使用者會話資訊。(個人認為更重要的是因為服務叢集的出現,需要一個分散式Session來作為統一的會話)
  4. 釋出/訂閱:Redis 還有一個釋出/訂閱的功能,您可以設定對某一個 key 值進行訊息釋出及訊息訂閱,當一個 key 值上進行了訊息釋出後,所有訂閱它的客戶端都會收到相應的訊息。這一功能最明顯的用法就是用作實時訊息系統。
  5. 任務佇列:Redis 的 lpush+brpop 命令組合即可實現阻塞佇列,生產者客戶端使用 lrpush 從列表左側插入元素,多個消費者客戶端使用 brpop 命令阻塞式的”搶”列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。
  6. 限速,介面訪問頻率限制:比如傳送簡訊驗證碼的介面,通常為了防止別人惡意頻刷,會限制使用者每分鐘獲取驗證碼的頻率,例如一分鐘不能超過 5 次。

當然 Redis 的使用場景並不僅僅只有這麼多,還有很多未列出的場景,如計數、排行榜等。

Redis 資料型別

前面也提到過,Redis 支援字串、雜湊表、列表、集合、有序集合五種資料型別的儲存。

字串(string)

  • string型別是二進位制安全的。意思是redis的string可以包含任何資料。比如jpg圖片或者序列化的物件 。
  • string型別是Redis最基本的資料型別,一個鍵最大能儲存512MB。

string 這種資料結構應該是我們最為常用的。在 Redis 中 string 表示的是一個可變的位元組陣列,我們初始化字串的內容、可以拿到字串的長度,可以獲取 string 的子串,可以覆蓋 string 的子串內容,可以追加子串。

圖 1. Redis 的 string 型別資料結構

如上圖所示,在 Redis 中我們初始化一個字串時,會採用預分配冗餘空間的方式來減少記憶體的頻繁分配,如圖 1 所示,實際分配的空間 capacity 一般要高於實際字串長度 len。如果您看過 Java 的 ArrayList 的原始碼相信會對此種模式很熟悉。

列表(list)

  • redis列表是簡單的字串列表,排序為插入的順序。列表的最大長度為2^32-1。
  • redis的列表是使用連結串列實現的,這意味著,即使列表中有上百萬個元素,增加一個元素到列表的頭部或尾部的操作都是在常量的時間完成。
  • 可以用列表獲取最新的內容(像帖子,微博等),用ltrim很容易就會獲取最新的內容,並移除舊的內容。
  • 用列表可以實現生產者消費者模式,生產者呼叫lpush新增項到列表中,消費者呼叫rpop從列表中提取,如果沒有元素,則輪詢去獲取,或者使用brpop等待生產者新增項到列表中。

在 Redis 中列表 list 採用的儲存結構是雙向連結串列,由此可見其隨機定位效能較差,比較適合首位插入刪除。像 Java 中的陣列一樣,Redis 中的列表支援通過下標訪問,不同的是 Redis 還為列表提供了一種負下標,-1 表示倒數一個元素,-2 表示倒數第二個數,依此類推。綜合列表首尾增刪效能優異的特點,通常我們使用 rpush/rpop/lpush/lpop 四條指令將列表作為佇列來使用。

圖 2. List 型別資料結構

如上圖所示,在列表元素較少的情況下會使用一塊連續的記憶體儲存,這個結構是 ziplist,也即是壓縮列表。它將所有的元素緊挨著一起儲存,分配的是一塊連續的記憶體。當資料量比較多的時候才會改成 quicklist。因為普通的連結串列需要的附加指標空間太大,會比較浪費空間。比如這個列表裡存的只是 int 型別的資料,結構上還需要兩個額外的指標 prev 和 next。所以 Redis 將連結串列和 ziplist 結合起來組成了 quicklist。也就是將多個 ziplist 使用雙向指標串起來使用。這樣既滿足了快速的插入刪除效能,又不會出現太大的空間冗餘。

雜湊表(hash)

  • redis的雜湊值是字串欄位和字串之間的對映,是表示物件的完美資料型別。
  • 雜湊中的欄位數量沒有限制,所以可以在你的應用程式以不同的方式來使用雜湊。

hash 與 Java 中的 HashMap 差不多,實現上採用二維結構,第一維是陣列,第二維是連結串列。hash 的 key 與 value 都儲存在連結串列中,而陣列中儲存的則是各個連結串列的表頭。在檢索時,首先計算 key 的 hashcode,然後通過 hashcode 定位到連結串列的表頭,再遍歷連結串列得到 value 值。可能您比較好奇為啥要用連結串列來儲存 key 和 value,直接用 key 和 value 一對一儲存不就可以了嗎?其實是因為有些時候我們無法保證 hashcode 值的唯一,若兩個不同的 key 產生了相同的 hashcode,我們需要一個連結串列在儲存兩對鍵值對,這就是所謂的 hash 碰撞。

集合(set)

  • redis集合是無序的字串集合,集合中的值是唯一的,無序的。可以對集合執行很多操作,例如,測試元素是否存在,對多個集合執行交集、並集和差集等等。
  • 我們通常可以用集合儲存一些無關順序的,表達物件間關係的資料,例如使用者的角色,可以用sismember很容易就判斷使用者是否擁有某個角色。
  • 在一些用到隨機值的場合是非常適合的,可以用 srandmember/spop 獲取/彈出一個隨機元素。
    同時,使用@EnableCaching開啟宣告式快取支援,這樣就可以使用基於註解的快取技術。註解快取是一個對快取使用的抽象,通過在程式碼中新增下面的一些註解,達到快取的效果。
    • @Cacheable:在方法執行前Spring先檢視快取中是否有資料,如果有資料,則直接返回快取資料;沒有則呼叫方法並將方法返回值放進快取。
    • @CachePut:將方法的返回值放到快取中。
    • @CacheEvict:刪除快取中的資料。

熟悉 Java 的同學應該知道 HashSet 的內部實現使用的是 HashMap,只不過所有的 value 都指向同一個物件。Redis 的 Set 結構也是一樣,它的內部也使用 Hash 結構,所有的 value 都指向同一個內部值。

有序集合(sorted set)

  • 有序集合由唯一的,不重複的字串元素組成。有序集合中的每個元素都關聯了一個浮點值,稱為分數。可以把有序看成hash和集合的混合體,分數即為hash的key。
  • 有序集合中的元素是按序儲存的,不是請求時才排序的。

有時也被稱作 ZSet,是 Redis 中一個比較特別的資料結構,在有序集合中我們會給每個元素賦予一個權重,其內部元素會按照權重進行排序,我們可以通過命令查詢某個範圍權重內的元素,這個特性在我們做一個排行榜的功能時可以說非常實用了。其底層的實現使用了兩個資料結構, hash 和跳躍列表,hash 的作用就是關聯元素 value 和權重 score,保障元素 value 的唯一性,可以通過元素 value 找到相應的 score 值。跳躍列表的目的在於給元素 value 排序,根據 score 的範圍獲取元素列表。

在 Spring Boot 專案中使用 Redis

spring-data-redis針對jedis提供瞭如下功能:

1. 連線池自動管理,提供了一個高度封裝的“RedisTemplate”類

2. 針對jedis客戶端中大量api進行了歸類封裝,將同一型別操作封裝為operation介面

ValueOperations:簡單K-V操作
SetOperations:set型別資料操作
ZSetOperations:zset型別資料操作
HashOperations:針對map型別的資料操作
ListOperations:針對list型別的資料操作

3. 提供了對key的“bound”(繫結)便捷化操作API,可以通過bound封裝指定的key,然後進行一系列的操作而無須“顯式”的再次指定Key,即BoundKeyOperations:

BoundValueOperations
BoundSetOperations
BoundListOperations
BoundSetOperations
BoundHashOperations

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

5. 針對資料的“序列化/反序列化”,提供了多種可選擇策略(RedisSerializer)

JdkSerializationRedisSerializer:POJO物件的存取場景,使用JDK本身序列化機制,將pojo類通過ObjectInputStream/ObjectOutputStream進行序列化操作,最終redis-server中將儲存位元組序列。是目前最常用的序列化策略。

StringRedisSerializer:Key或者value為字串的場景,根據指定的charset對資料的位元組序列編碼成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封裝。是最輕量級和高效的策略。

JacksonJsonRedisSerializer:jackson-json工具提供了javabean與json之間的轉換能力,可以將pojo例項序列化成json格式儲存在redis中,也可以將json格式的資料轉換成pojo例項。因為jackson工具在序列化和反序列化時,需要明確指定Class型別,因此此策略封裝起來稍微複雜。【需要jackson-mapper-asl工具支援】

OxmSerializer:提供了將javabean與xml之間的轉換能力,目前可用的三方支援包括jaxb,apache-xmlbeans;redis儲存的資料將是xml工具。不過使用此策略,程式設計將會有些難度,而且效率最低;不建議使用。【需要spring-oxm模組的支援】

如果你的資料需要被第三方工具解析,那麼資料應該使用StringRedisSerializer而不是JdkSerializationRedisSerializer。

如果使用的是預設的JdkSerializationRedisSerializer,注意一定要讓快取的物件實現序列化介面用於序列化 。

關於key的設計

key的存活時間:

無論什麼時候,只要有可能就利用key超時的優勢。

關係型資料庫的redis

1: 把表名轉換為key字首 如, tag:

2: 第2段放置用於區分割槽key的欄位--對應mysql中的主鍵的列名,如userid

3: 第3段放置主鍵值,如2,3,4...., a , b ,c

4: 第4段,寫要儲存的列名

例:user:userid:9:username

使用前配置

新增 Redis 依賴

<!-- 配置使用 redis 啟動器 -->
<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 資料庫索引(預設為 0)
spring.redis.database=0 
# Redis 伺服器連線埠
# Redis 伺服器連線密碼(預設為空)
spring.redis.password=123456
#連線池最大連線數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=8
# 連線池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1
# 連線池中的最大空閒連線
spring.redis.jedis.pool.max-idle=8
# 連線池中的最小空閒連線
spring.redis.jedis.pool.min-idle=0
# 連線超時時間(毫秒) 如果連線超時時間不設定,這要註釋掉配置而不能=0,否則會報連線超時錯誤:Command timed out after no timeout,,有超時時間最後設定為200以上
spring.redis.timeout=300

RedisTemplate 的配置

@Configuration
public class RedisConfig {

    /**
     * 預設是JDK的序列化策略,這裡配置redisTemplate採用的是Jackson2JsonRedisSerializer的序列化策略
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(預設使用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等會丟擲異常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 配置連線工廠
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //使用StringRedisSerializer來序列化和反序列化redis的key值
        //redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        // 值採用json序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /***
     * stringRedisTemplate預設採用的是String的序列化策略
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }
}

RedisTemplate

Spring Boot 的 spring-boot-starter-data-redis 為 Redis 的相關操作提供了一個高度封裝的 RedisTemplate 類,而且對每種型別的資料結構都進行了歸類,將同一型別操作封裝為 operation 介面。RedisTemplate 對五種資料結構分別定義了操作,如下所示:

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

但是對於 string 型別的資料,Spring Boot 還專門提供了 StringRedisTemplate 類,而且官方也建議使用該類來操作 String 型別的資料。那麼它和 RedisTemplate 又有啥區別呢?

  1. RedisTemplate 是一個泛型類,而 StringRedisTemplate 不是,後者只能對鍵和值都為 String 型別的資料進行操作,而前者則可以操作任何型別。
  2. 兩者的資料是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 裡面的資料,RedisTemplate 只能管理 RedisTemplate 中 的資料。

實踐

//用於指定junit執行環境,是junit提供給其他框架測試環境介面擴充套件,為了便於使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用於載入ApplicationContext,啟動spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {

    @Autowired
    private RedisTemplate redisTemplate;
    //因為spring自動注入管理了bean容器,直接用@Autowired取即可
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

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

        //刪除資料
        Boolean isDelete = stringRedisTemplate.delete("String");
        System.out.println(isDelete ? "Yes" : "No");
    }

    /**
     * 操作列表
     */
    @Test
    public void testList() {
        ListOperations listOp = redisTemplate.opsForList();
        //往 List 左側插入一個元素
        listOp.leftPush("namelist", "mike");
        listOp.leftPush("namelist", "kim");
        //往 List 右側插入一個元素
        listOp.rightPush("namelist", "jimmy");
        listOp.rightPush("namelist", "chuck");
        //List 大小
        Long size = listOp.size("namelist");
        //遍歷整個List
        List namelist1 = listOp.range("namelist", 0, size);
        //遍歷整個List,-1表示倒數第一個即最後一個
        List namelist = listOp.range("namelist", 0, -1);
        System.out.println(JSON.toJSONString(namelist));
        //從 List 左側取出第一個元素,並移除
        Object name1 = listOp.leftPop("namelist", 200, TimeUnit.MILLISECONDS);
        System.out.println("is kim:" + name1.equals("kim"));
        //從 List 右側取出第一個元素,並移除
        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 對應的 Hash 中是否存在指定的 map 鍵
        Assert.assertTrue(hashOp.hasKey("score", "Kim"));
        //獲取指定 key 對應的 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"對應的hash表Map
        Map<String, Integer> map = hashOp.entries("score");
        System.out.println(JSON.toJSONString(map));
        //刪除指定 key 對應 Hash 中指定鍵的鍵值對
        hashOp.delete("score", "Mike");
        //如果要刪除整個hash表,要用redisTemplate.delete("score")方法,否則報錯:Fields must not be empty
        //hashOp.delete("score");
        //刪除整個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));
        //移除集合中的元素,可以一個或多個
        setOp.remove("city", "paris");
        //判斷是否是集合中的元素
        Boolean isMember = setOp.isMember("city", "paris");
        System.out.println("paris is in city:" + isMember);
        //移除並返回集合中的一個隨機元素
        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);
        //獲取變數指定區間的元素。0, -1表示全部
        Set<String> zcity = zSetOp.range("zcity", 0, -1);
        System.out.println(JSON.toJSONString(zcity));
        //通過分數返回有序集合指定區間內的成員,其中有序整合員按分數值遞增(從小到大)順序排列
        Set<String> byScore = zSetOp.rangeByScore("zcity", 85, 100);
        System.out.println(JSON.toJSONString(byScore));
        //獲取有序集合的成員數
        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 的排序取值。這個排序只有在有相同分數的情況下才能使用,如果有不同的分數則返回值不確定。
        //rangeByLex應用在數值上比較
        Set<Integer> set = zSetOp1.rangeByLex("board", range);
        System.out.println(JSON.toJSONString(set));
        //用於獲取滿足非 score 的設定下標開始的長度排序取值。
        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));
    }
}

報錯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:這是因為伺服器沒有設定密碼,客戶端設定了密碼去連線,導致連線失敗。我們在啟動伺服器時通過命令(config set requirepass "123456")手動設定了密碼,一旦Redis伺服器關閉後,重新啟動載入了Redis啟動的配置,因為啟動配置選單(redis-server.exe redis.windows.conf)中沒有配置密碼,所以重新啟動的redis是沒有設定密碼的,這樣就會報錯。
解決方法:如果要設定密碼則要在Redis啟動的配置檔案中修改新增;或者如果不設定密碼則客戶端連線時application.properties不要設定密碼連線
# Redis 伺服器連線密碼(預設為空)
#spring.redis.password=123456

報錯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:這是因為客戶端連線時application.properties中設定了連線超時時間為0導致的.
解決方法:
# 連線超時時間(毫秒) 如果連線超時時間不設定,這要註釋掉配置而不能=0,否則會報連線超時錯誤:Command timed out after no timeout,有超時時間最後設定為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:如果要刪除整個hash表,要用redisTemplate.delete("score")方法,否則報錯:Fields must not be empty
解決方法:使用redisTemplate方法,HashOperations的delete是用於刪除hash表欄位用
		HashOperations<String,String,Integer> hashOp = redisTemplate.opsForHash();
        //hashOp.delete("score");
        redisTemplate.delete("score");

關於 Redis 的幾個經典問題

快取與資料庫一致性問題

對於既有資料庫操作又有快取操作的介面,一般分為兩種執行順序。

  1. 先運算元據庫,再操作快取。這種情況下如果資料庫操作成功,快取操作失敗就會導致快取和資料庫不一致。
  2. 第二種情況就是先操作快取再運算元據庫,這種情況下如果快取操作成功,資料庫操作失敗也會導致資料庫和快取不一致。

大部分情況下,我們的快取理論上都是需要可以從資料庫恢復出來的,所以基本上採取第一種順序都是不會有問題的。針對那些必須保證資料庫和快取一致的情況,通常是不建議使用快取的。

快取擊穿問題

快取擊穿表示惡意使用者頻繁的模擬請求快取中不存在的資料,以致這些請求短時間內直接落在了資料庫上,導致資料庫效能急劇下降,最終影響服務整體的效能。這個在實際專案很容易遇到,如搶購活動、秒殺活動的介面 API 被大量的惡意使用者刷,導致短時間內資料庫當機。對於快取擊穿的問題,有以下幾種解決方案,這裡只做簡要說明。

  1. 使用互斥鎖排隊。當從快取中獲取資料失敗時,給當前介面加上鎖,從資料庫中載入完資料並寫入後再釋放鎖。若其它執行緒獲取鎖失敗,則等待一段時間後重試。(資料庫取資料時加鎖)
  2. 使用布隆過濾器。將所有可能存在的資料快取放到布隆過濾器中,當黑客訪問不存在的快取時迅速返回避免快取及 DB 掛掉。

快取雪崩問題

在短時間內有大量快取失效,如果這期間有大量的請求發生同樣也有可能導致資料庫發生當機。在 Redis 機群的資料分佈演算法上如果使用的是傳統的 hash 取模演算法,在增加或者移除 Redis 節點的時候就會出現大量的快取臨時失效的情形。

  1. 像解決快取穿透一樣加鎖排隊。
  2. 建立備份快取,快取 A 和快取 B,A 設定超時時間,B 不設值超時時間,先從 A 讀快取,A 沒有讀 B,並且更新 A 快取和 B 快取。
  3. 計算資料快取節點的時候採用一致性 hash 演算法,這樣在節點數量發生改變時不會存在大量的快取資料需要遷移的情況發生。

快取併發問題

這裡的併發指的是多個 Redis 的客戶端同時 set 值引起的併發問題。比較有效的解決方案就是把 set 操作放在佇列中使其序列化,必須得一個一個執行。

描述

brpop : block right pop 
BRPOP key1 [key2 ] timeout 
移出並獲取列表的最後一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止。

相關文章