天天看點

springboot中RedisTemplate的使用

Redis 是一個開源(BSD 許可)、記憶體存儲的資料結構伺服器,可用作資料庫,高速緩存和消息隊列代理。它支援字元串、哈希表、清單、集合、有序集合等資料類型。内置複制、Lua 腳本、LRU 收回、事務以及不同級别磁盤持久化功能,同時通過 Redis Sentinel 提供高可用,通過 Redis Cluster 提供自動分區。

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

分布式緩存:在分布式的系統架構中,将緩存存儲在記憶體中顯然不當,因為緩存需要與其他機器共享,這時 Redis 便挺身而出了,緩存也是 Redis 使用最多的場景。

分布式鎖:在高并發的情況下,我們需要一個鎖來防止并發帶來的髒資料,Java 自帶的鎖機制顯然對程序間的并發并不好使,此時可以利用 Redis 單線程的特性來實作我們的分布式鎖,如何實作,可以參考這篇文章。

Session 存儲/共享:Redis 可以将 Session 持久化到存儲中,這樣可以避免由于機器當機而丢失使用者會話資訊。(個人認為更重要的是因為服務叢集的出現,需要一個分布式Session來作為統一的會話)

釋出/訂閱:Redis 還有一個釋出/訂閱的功能,您可以設定對某一個 key 值進行消息釋出及消息訂閱,當一個 key 值上進行了消息釋出後,所有訂閱它的用戶端都會收到相應的消息。這一功能最明顯的用法就是用作實時消息系統。

任務隊列:Redis 的 <code>lpush+brpop</code> 指令組合即可實作阻塞隊列,生産者用戶端使用 <code>lrpush</code> 從清單左側插入元素,多個消費者用戶端使用 <code>brpop</code> 指令阻塞式的”搶”清單尾部的元素,多個用戶端保證了消費的負載均衡和高可用性。

限速,接口通路頻率限制:比如發送短信驗證碼的接口,通常為了防止别人惡意頻刷,會限制使用者每分鐘擷取驗證碼的頻率,例如一分鐘不能超過 5 次。

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

前面也提到過,Redis 支援字元串、哈希表、清單、集合、有序集合五種資料類型的存儲。

string類型是二進制安全的。意思是redis的string可以包含任何資料。比如jpg圖檔或者序列化的對象 。

string類型是Redis最基本的資料類型,一個鍵最大能存儲512MB。

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

Redis 的 string 類型資料結構

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

redis清單是簡單的字元串清單,排序為插入的順序。清單的最大長度為2^32-1。

redis的清單是使用連結清單實作的,這意味着,即使清單中有上百萬個元素,增加一個元素到清單的頭部或尾部的操作都是在常量的時間完成。

可以用清單擷取最新的内容(像文章,微網誌等),用ltrim很容易就會擷取最新的内容,并移除舊的内容。

用清單可以實作生産者消費者模式,生産者調用lpush添加項到清單中,消費者調用rpop從清單中提取,如果沒有元素,則輪詢去擷取,或者使用brpop等待生産者添加項到清單中。

在 Redis 中清單 list 采用的存儲結構是雙向連結清單,由此可見其随機定位性能較差,比較适合首位插入删除。像 Java 中的數組一樣,Redis 中的清單支援通過下标通路,不同的是 Redis 還為清單提供了一種負下标,<code>-1</code> 表示倒數一個元素,<code>-2</code> 表示倒數第二個數,依此類推。綜合清單首尾增删性能優異的特點,通常我們使用 <code>rpush/rpop/lpush/lpop</code> 四條指令将清單作為隊列來使用。

List 類型資料結構

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

redis的哈希值是字元串字段和字元串之間的映射,是表示對象的完美資料類型。

哈希中的字段數量沒有限制,是以可以在你的應用程式以不同的方式來使用哈希。

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

redis集合是無序的字元串集合,集合中的值是唯一的,無序的。可以對集合執行很多操作,例如,測試元素是否存在,對多個集合執行交集、并集和差集等等。

我們通常可以用集合存儲一些無關順序的,表達對象間關系的資料,例如使用者的角色,可以用sismember很容易就判斷使用者是否擁有某個角色。

在一些用到随機值的場合是非常适合的(抽獎),可以用 srandmember/spop 擷取/彈出一個随機元素。

同時,使用@EnableCaching開啟聲明式緩存支援,這樣就可以使用基于注解的緩存技術。注解緩存是一個對緩存使用的抽象,通過在代碼中添加下面的一些注解,達到緩存的效果。

@Cacheable:在方法執行前Spring先檢視緩存中是否有資料,如果有資料,則直接傳回緩存資料;沒有則調用方法并将方法傳回值放進緩存。(查)

@CachePut:将方法的傳回值放到緩存中。(增/改)

@CacheEvict:删除緩存中的資料。(删)

熟悉 Java 的同學應該知道 HashSet 的内部實作使用的是 HashMap,隻不過所有的 value 都指向同一個對象。Redis 的 Set 結構也是一樣,它的内部也使用 Hash 結構,所有的 value 都指向同一個内部值。

有序集合由唯一的,不重複的字元串元素組成。有序集合中的每個元素都關聯了一個浮點值,稱為分數。可以把有序集合zset看成hash和集合的混合體,分數即為hash的key。

有序集合中的元素是按序存儲的,不是請求時才排序的。

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

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

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

如果使用的是預設的JdkSerializationRedisSerializer,注意一定要讓緩存的對象實作序列化接口用于序列化 。

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

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

2: 第2段放置用于區分區key的字段--對應mysql中的主鍵的列名,如userid

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

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

例:user:userid:9:username

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

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

操作字元串:<code>redisTemplate.opsForValue()</code>

操作 Hash:<code>redisTemplate.opsForHash()</code>

操作 List:<code>redisTemplate.opsForList()</code>

操作 Set:<code>redisTemplate.opsForSet()</code>

操作 ZSet:<code>redisTemplate.opsForZSet()</code>

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

<code>RedisTemplate</code> 是一個泛型類,而 <code>StringRedisTemplate</code> 不是,後者隻能對鍵和值都為 String 類型的資料進行操作,而前者則可以操作任何類型。

兩者的資料是不共通的,<code>StringRedisTemplate</code> 隻能管理 <code>StringRedisTemplate</code> 裡面的資料,<code>RedisTemplate</code> 隻能管理 <code>RedisTemplate</code> 中 的資料。

對于既有資料庫操作又有緩存操作的接口,一般分為兩種執行順序。

先操作資料庫,再操作緩存。這種情況下如果資料庫操作成功,緩存操作失敗就會導緻緩存和資料庫不一緻。

第二種情況就是先操作緩存再操作資料庫,這種情況下如果緩存操作成功,資料庫操作失敗也會導緻資料庫和緩存不一緻。

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

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

使用互斥鎖排隊。當從緩存中擷取資料失敗時,給目前接口加上鎖,從資料庫中加載完資料并寫入後再釋放鎖。若其它線程擷取鎖失敗,則等待一段時間後重試。(資料庫取資料時加鎖)

使用布隆過濾器。将所有可能存在的資料緩存放到布隆過濾器中,當黑客通路不存在的緩存時迅速傳回避免緩存及 DB 挂掉。

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

像解決緩存穿透一樣加鎖排隊。

建立備份緩存,緩存 A 和緩存 B,A 設定逾時時間,B 不設值逾時時間,先從 A 讀緩存,A 沒有讀 B,并且更新 A 緩存和 B 緩存。

計算資料緩存節點的時候采用一緻性 hash 算法,這樣在節點數量發生改變時不會存在大量的緩存資料需要遷移的情況發生。

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

大白話布隆過濾器

Q:我們在編寫RedisConfig配置類時是否繼承于CachingConfigurerSupport類有什麼差別?為什麼有的繼承了有的不選擇繼承,是繼承了的話可以結合Springboot的@EnableCaching的注解開啟緩存麼?以便使用如下注解自動開啟緩存麼?那在項目中是使用好還是不使用好?

開啟緩存後注解使用參考:

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

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

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

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

其實可以寫個死循環調用<code>rightPop(K key)</code>方法,當擷取到資料時才跳出循環即可。當然要注意接口逾時的情況。是以直接使用逾時方法就是阻塞調用bRPop。

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

A:如下。

springboot使用單元測試步驟:

1、引入依賴

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

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 并在 Spring Boot 項目中使用 Redis——IBM