最全的Redis解析,趕忙收藏

在這篇文章中,咱們將完全瞭解 Redis 的使用場景、Redis 的五種數據結構,以及如何在
Spring Boot 中使用 Redis,文章的最後還會列舉面試過程當中常常被問到的關於 Redis
的問題以及其解決方案。html

Redis 簡介

Redis 是一個開源(BSD
許可)、內存存儲的數據結構服務器,可用做數據庫,高速緩存和消息隊列代理。它支持字符串、哈希表、列表、集合、有序集合等數據類型。內置複製、Lua
腳本、LRU 收回、事務以及不一樣級別磁盤持久化功能,同時經過 Redis Sentinel
提供高可用,經過 Redis Cluster
提供自動分區。在實際的開發過程當中,多多少少都會涉及到緩存,而 Redis
一般來講是咱們分佈式緩存的最佳選擇。Redis 也是咱們熟知的
NoSQL(非關係性數據庫)之一,雖然其不能徹底的替代關係性數據庫,但它可做爲其良好的補充。web

Redis 使用場景

微服務以及分佈式被普遍使用後,Redis
的使用場景就愈來愈多了,這裏我羅列了主要的幾種場景。面試

  1. 分佈式緩存:在分佈式的系統架構中,將緩存存儲在內存中顯然不當,由於緩存須要與其餘機器共享,這時
    Redis 便自告奮勇了,緩存也是 Redis 使用最多的場景。redis

  2. 分佈式鎖:在高併發的狀況下,咱們須要一個鎖來防止併發帶來的髒數據,Java
    自帶的鎖機制顯然對進程間的併發並很差使,此時能夠利用 Redis
    單線程的特性來實現咱們的分佈式。算法

  3. Session 存儲/共享:Redis 能夠將 Session
    持久化到存儲中,這樣能夠避免因爲機器宕機而丟失用戶會話信息。spring

  4. 發佈/訂閱:Redis 還有一個發佈/訂閱的功能,您能夠設定對某一個 key
    值進行消息發佈及消息訂閱,當一個 key
    值上進行了消息發佈後,全部訂閱它的客戶端都會收到相應的消息。這一功能最明顯的用法就是用做實時消息系統。數據庫

  5. 任務隊列:Redis 的 lpush+brpop
    命令組合便可實現阻塞隊列,生產者客戶端使用 lrpush
    從列表左側插入元素,多個消費者客戶端使用 brpop
    命令阻塞式的"搶"列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。數組

  6. 限速,接口訪問頻率限制:好比發送短信驗證碼的接口,一般爲了防止別人惡意頻刷,會限制用戶每分鐘獲取驗證碼的頻率,例如一分鐘不能超過
    5 次。緩存

固然 Redis
的使用場景並不只僅只有這麼多,還有不少未列出的場景,如計數、排行榜等,可見 Redis
的強大。不過 Redis
說到底仍是一個數據庫(非關係型),那麼咱們仍是有必要了解一下它支持存儲的數據結構。服務器

Redis 數據類型

前面也提到過,Redis
支持字符串、哈希表、列表、集合、有序集合五種數據類型的存儲。瞭解這五種數據結構很是重要,能夠說若是吃透了這五種數據結構,你就掌握了
Redis 應用知識的三分之一,下面咱們就來逐一解析。

字符串(string)

string 這種數據結構應該是咱們最爲經常使用的。在 Redis 中 string
表示的是一個可變的字節數組,咱們初始化字符串的內容、能夠拿到字符串的長度,能夠獲取
string 的子串,能夠覆蓋 string 的子串內容,能夠追加子串。

圖 1. Redis 的 string 類型數據結構

在這裏插入圖片描述

如上圖所示,在 Redis
中咱們初始化一個字符串時,會採用預分配冗餘空間的方式來減小內存的頻繁分配,如圖 1
所示,實際分配的空間 capacity 通常要高於實際字符串長度 len。若是您看過 Java 的
ArrayList 的源碼相信會對此種模式很熟悉。

列表(list)

在 Redis 中列表 list
採用的存儲結構是雙向鏈表,因而可知其隨機定位性能較差,比較適合首位插入刪除。像
Java 中的數組同樣,Redis 中的列表支持經過下標訪問,不一樣的是 Redis
還爲列表提供了一種負下標,-1 表示倒數一個元素,-2
表示倒數第二個數,依此類推。綜合列表首尾增刪性能優異的特色,一般咱們使用
rpush/rpop/lpush/lpop 四條指令將列表做爲隊列來使用。

圖 2. List 類型數據結構

在這裏插入圖片描述

如上圖所示,在列表元素較少的狀況下會使用一塊連續的內存存儲,這個結構是
ziplist,也便是壓縮列表。它將全部的元素緊挨着一塊兒存儲,分配的是一塊連續的內存。當數據量比較多的時候纔會改爲
quicklist。由於普通的鏈表須要的附加指針空間太大,會比較浪費空間。好比這個列表裏存的只是
int 類型的數據,結構上還須要兩個額外的指針 prev 和 next。因此 Redis 將鏈表和
ziplist 結合起來組成了 quicklist。也就是將多個 ziplist
使用雙向指針串起來使用。這樣既知足了快速的插入刪除性能,又不會出現太大的空間冗餘。

哈希表(hash)

hash 與 Java 中的 HashMap
差很少,實現上採用二維結構,第一維是數組,第二維是鏈表。hash 的 key 與 value
都存儲在鏈表中,而數組中存儲的則是各個鏈表的表頭。在檢索時,首先計算 key 的
hashcode,而後經過 hashcode 定位到鏈表的表頭,再遍歷鏈表獲得 value
值。可能您比較好奇爲啥要用鏈表來存儲 key 和 value,直接用 key 和 value
一對一存儲不就能夠了嗎?實際上是由於有些時候咱們沒法保證 hashcode
值的惟一,若兩個不一樣的 key 產生了相同的
hashcode,咱們須要一個鏈表在存儲兩對鍵值對,這就是所謂的 hash 碰撞。

集合(set)

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

有序集合(sorted set)

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

在 Spring Boot 項目中使用 Redis

準備工做

開始在 Spring Boot 項目中使用 Redis 以前,咱們還須要一些準備工做。

  1. 一臺安裝了 Redis 的機器或者虛擬機。

  2. 一個建立好的 Spring Boot 項目。

添加 Redis 依賴

Spring Boot 官方已經爲咱們提供好了集成 Redis 的 Starter,咱們只須要簡單地在
pom.xml 文件中添加以下代碼便可。Spring Boot 的 Starter
給咱們在項目依賴管理上提供了諸多便利。

清單 1. 添加 Redis 依賴

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

添加完依賴以後,咱們還須要配置 Redis
的地址等信息才能使用,在 application.properties 中添加以下配置便可。

清單 2. Spring Boot 中配置 Redis

spring.redis.host=192.168.142.132
spring.redis.port=6379
# Redis 數據庫索引(默認爲 0)
spring.redis.database=0 
# Redis 服務器鏈接端口
# Redis 服務器鏈接密碼(默認爲空)
spring.redis.password=
#鏈接池最大鏈接數(使用負值表示沒有限制)
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
# 鏈接超時時間(毫秒)
spring.redis.timeout=0

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 中
    的數據。

RedisTemplate 的配置

一個 Spring Boot
項目中,咱們只須要維護一個 RedisTemplate 對象和一個 StringRedisTemplate 對象就能夠了。因此咱們須要經過一個 Configuration 類來初始化這兩個對象而且交由的 BeanFactory 管理。咱們在 cn.itweknow.sbredis.config包下面新建了一個 RedisConfig 類,其內容以下所示:

清單 3. RedisTemplate 和 StringRedisTemplate 的配置

@Configuration
public class RedisConfig {
 
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
 
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
 
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
 
    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

操做字符串

StringRedisTempalte 在上面已經初始化好了,咱們只須要在須要用到的地方經過 @AutoWired 註解注入就行。

  1. 設置值,對於設置值,咱們可使用 opsForValue().void set(K var1, V var2);
@Test
public void testSet() {
        stringRedisTemplate.opsForValue().set("test-string-value", "Hello Redis");
}
  1. 獲取值,與 set 方法相對於 StringRedisTemplate 還提供了.opsForValue().get(Object
    var1) 方法來獲取指定 key 對應的 value 值。
@Test
public void testGet() {
    String value = stringRedisTemplate.opsForValue().get("test-string-value");
    System.out.println(value);
}
  1. 設置值的時候設置過時時間。在設置緩存的時候,咱們一般都會給他設置一個過時時間,讓其可以達到定時刷新的效果。StringRedisTemplate 提供了 void
    set(K var1, V var2, long var3, TimeUnit
    var5) 方法來達到設置過時時間的目的,其中 var3 這個參數就是過時時間的數值,而 TimeUnit 是個枚舉類型,咱們用它來設置過時時間的單位,是小時或是秒等等。
@Test
public void testSetTimeOut() {
        stringRedisTemplate.opsForValue().set("test-string-key-time-out", "Hello Redis", 3, TimeUnit.HOURS);
}
  1. 刪除數據,咱們一樣能夠經過 StringRedisTmeplate 來刪除數據, Boolean delete(K
    key)方法提供了這個功能。
@Test
public void testDeleted() {
        stringRedisTemplate.delete("test-string-value");
}

操做數組

在 Redis 數據類型小節中,咱們提到過咱們常用 Redis
的 lpush/rpush/lpop/rpop 四條指令來實現一個隊列。那麼這四條指令在 RedisTemplate 中也有相應的實現。

  1. leftPush(K key, V value),往 List 左側插入一個元素,如 從左邊往數組中 push
    元素:
@Test
 public void testLeftPush() {
        redisTemplate.opsForList().leftPush("TestList", "TestLeftPush");
 }
  1. rightPush(K key, V value),往 List 右側插入一個元素, 如從右邊往數組中 push
    元素:
@Test
public void testRightPush() {
       redisTemplate.opsForList().rightPush("TestList", "TestRightPush");
}
  1. 執行完上面兩個 Test 以後,咱們可使用 Redis 客戶端工具 RedisDesktopManager
    來查看 TestList 中的內容,以下圖 (Push 以後 TestList 中的內容)所示:

在這裏插入圖片描述

此時咱們再一次執行 leftPush 方法,TestList 的內容就會變成下圖(第二次執行leftPush 以後的內容)所示:

在這裏插入圖片描述

能夠看到 leftPush 其實是往數組的頭部新增一個元素,那麼 rightPush就是往數組尾部插入一個元素。

  1. leftPop(K key),從 List 左側取出第一個元素,並移除,
    如從數組頭部獲取並移除值:
@Test
public void testLeftPop() {
        Object leftFirstElement = redisTemplate.opsForList().leftPop("TestList");
        System.out.println(leftFirstElement);
}

執行上面的代碼以後,您會看到控制檯會打印出 TestLeftPush,而後再去RedisDesktopManager 中查看 TestList 的內容,以下圖(同數組頂端移除一個元素後)所示。您會發現數組中的第一個元素已經被移除了。

在這裏插入圖片描述

  1. rightPop(K key),從 List 右側取出第一個元素,並移除,如從數組尾部獲取並移除值:
@Test
 public void testRightPop() {
        Object rightFirstElement = redisTemplate.opsForList().rightPop("TestList");
        System.out.println(rightFirstElement);
 }

操做 Hash

Redis 中的 Hash 數據結構實際上與 Java 中的 HashMap 是很是相似的,提供的 API
也很相似。下面咱們就一塊兒來看下 RedisTemplate 爲 Hash 提供了哪些 API。

  1. Hash 中新增元素。
@Test
public void testPut() {
        redisTemplate.opsForHash().put("TestHash", "FirstElement", "Hello,Redis hash.");
        Assert.assertTrue(redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"));
}
  1. 判斷指定 key 對應的 Hash 中是否存在指定的 map
    鍵,使用用法能夠見上方代碼所示。

  2. 獲取指定 key 對應的 Hash 中指定鍵的值。

@Test
public void testGet() {
        Object element = redisTemplate.opsForHash().get("TestHash", "FirstElement");
        Assert.assertEquals("Hello,Redis hash.", element);
}
  1. 刪除指定 key 對應 Hash 中指定鍵的鍵值對。
@Test
public void testDel() {
        redisTemplate.opsForHash().delete("TestHash", "FirstElement");
        Assert.assertFalse(redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"));
}

操做集合

集合很相似於 Java 中的 Set,RedisTemplate 也爲其提供了豐富的 API。

  1. 向集合中添加元素。
@Test
public void testAdd() {
        redisTemplate.opsForSet().add("TestSet", "e1", "e2", "e3");
        long size = redisTemplate.opsForSet().size("TestSet");
        Assert.assertEquals(3L, size);
}
  1. 獲取集合中的元素。
@Test
public void testGet() {
        Set<String> testSet = redisTemplate.opsForSet().members("TestSet");
        System.out.println(testSet);
}

執行上面的代碼後,控制檯輸出的是 [e1, e3,e2],固然您可能會看到其餘結果,由於 Set是無序的,並非按照咱們添加的順序來排序的。

  1. 獲取集合的長度,在像集合中添加元素的示例代碼中展現瞭如何獲取集合長度。

  2. 移除集合中的元素。’

@Test
public void testRemove() {
        redisTemplate.opsForSet().remove("TestSet", "e1", "e2");
        Set testSet = redisTemplate.opsForSet().members("TestSet");
        Assert.assertEquals("e3", testSet.toArray()[0]);
}

操做有序集合

與 Set 不同的地方是,ZSet 對於集合中的每一個元素都維護了一個權重值,那麼
RedisTemplate 提供了很多與這個權重值相關的 API。

API 描述
add(K key, V value, double score) 添加元素到變量中同時指定元素的分值。
range(K key, long start, long end) 獲取變量指定區間的元素。
rangeByLex(K key, RedisZSetCommands.Range range) 用於獲取知足非 score 的排序取值。這個排序只有在有相同分數的狀況下才能使用,若是有不一樣的分數則返回值不肯定。
angeByLex(K key, RedisZSetCommands.Range range, RedisZSetCommands.Limit limit) 用於獲取知足非 score 的設置下標開始的長度排序取值。
add(K key, Set<ZSetOperations.TypedTuple<V>> tuples) 經過 TypedTuple 方式新增數據。
rangeByScore(K key, double min, double max) 根據設置的 score 獲取區間值。
rangeByScore(K key, double min, double max,long offset, long count) 根據設置的 score 獲取區間值從給定下標和給定長度獲取最終值。
rangeWithScores(K key, long start, long end) 獲取 RedisZSetCommands.Tuples 的區間值。

實現分佈式鎖

上面基本列出了 RedisTemplate 和 StringRedisTemplate 兩個類所提供的對 Redis
操做的相關 API,可是有些時候這些 API
並不能完成咱們全部的需求,這個時候咱們其實還能夠在 Spring Boot 項目中直接與
Redis
交互來完成操做。好比,咱們在實現分佈式鎖的時候其實就是使用了 RedisTemplate 的 execute 方法來執行
lua 腳原本獲取和釋放鎖的。

清單 4. 獲取鎖

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));

清單 5. 釋放鎖

1 2 3 4 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」))));

關於 Redis 的幾個經典問題

最近幾年 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
操做放在隊列中使其串行化,必須得一個一個執行。

參考資源