久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

Redis中都有哪些數據結構

141次閱讀
沒有評論

共計 11424 個字符,預計需要花費 29 分鐘才能閱讀完成。

今天就跟大家聊聊有關 Redis 中都有哪些數據結構,可能很多人都不太了解,為了讓大家更加了解,丸趣 TV 小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。

Redis 核心對象

在 Redis 中有一個「核心的對象」叫做 redisObject,是用來表示所有的 key 和 value 的,用 redisObject 結構體來表示 String、Hash、List、Set、ZSet 五種數據類型。

redisObject 的源代碼在 redis.h 中,使用 c 語言寫的,感興趣的可以自行查看,關于 redisObject 我這里畫了一張圖,表示 redisObject 的結構如下所示:

閃瞎人的五顏六色圖

在 redisObject 中「type 表示屬于哪種數據類型,encoding 表示該數據的存儲方式」,也就是底層的實現的該數據類型的數據結構。因此這篇文章具體介紹的也是 encoding 對應的部分。

那么 encoding 中的存儲類型又分別表示什么意思呢?具體數據類型所表示的含義,如下圖所示:

圖片截圖出自《Redis 設計與實現第二版》

可能看完這圖,還是覺得一臉懵。不慌,會進行五種數據結構的詳細介紹,這張圖只是讓你找到每種中數據結構對應的儲存類型有哪些,大概腦子里有個印象。

舉一個簡單的例子,你在 Redis 中設置一個字符串 key 234,然后查看這個字符串的存儲類型就會看到為 int 類型,非整數型的使用的是 embstr 儲存類型,具體操作如下圖所示:

String 類型

String 是 Redis 最基本的數據類型,上面的簡介中也說到 Redis 是用 c 語言開發的。但是 Redis 中的字符串和 c 語言中的字符串類型卻是有明顯的區別。

String 類型的數據結構存儲方式有三種 int、raw、embstr。那么這三種存儲方式有什么區別呢?

int

Redis 中規定假如存儲的是「整數型值」,比如 set num 123 這樣的類型,就會使用 int 的存儲方式進行存儲,在 redisObject 的「ptr 屬性」中就會保存該值。

SDS

假如存儲的「字符串是一個字符串值并且長度大于 32 個字節」就會使用 SDS(simple dynamic string)方式進行存儲,并且 encoding 設置為 raw;若是「字符串長度小于等于 32 個字節」就會將 encoding 改為 embstr 來保存字符串。

SDS 稱為「簡單動態字符串」,對于 SDS 中的定義在 Redis 的源碼中有的三個屬性 int len、int free、char buf[]。

len 保存了字符串的長度,free 表示 buf 數組中未使用的字節數量,buf 數組則是保存字符串的每一個字符元素。

因此當你在 Redsi 中存儲一個字符串 Hello 時,根據 Redis 的源代碼的描述可以畫出 SDS 的形式的 redisObject 結構圖如下圖所示:

SDS 與 c 語言字符串對比

Redis 使用 SDS 作為存儲字符串的類型肯定是有自己的優勢,SDS 與 c 語言的字符串相比,SDS 對 c 語言的字符串做了自己的設計和優化,具體優勢有以下幾點:

(1)c 語言中的字符串并不會記錄自己的長度,因此「每次獲取字符串的長度都會遍歷得到,時間的復雜度是 O(n)」,而 Redis 中獲取字符串只要讀取 len 的值就可,時間復雜度變為 O(1)。

(2)「c 語言」中兩個字符串拼接,若是沒有分配足夠長度的內存空間就「會出現緩沖區溢出的情況」;而「SDS」會先根據 len 屬性判斷空間是否滿足要求,若是空間不夠,就會進行相應的空間擴展,所以「不會出現緩沖區溢出的情況」。

(3)SDS 還提供「空間預分配」和「惰性空間釋放」兩種策略。在為字符串分配空間時,分配的空間比實際要多,這樣就能「減少連續的執行字符串增長帶來內存重新分配的次數」。

當字符串被縮短的時候,SDS 也不會立即回收不適用的空間,而是通過 free 屬性將不使用的空間記錄下來,等后面使用的時候再釋放。

具體的空間預分配原則是:「當修改字符串后的長度 len 小于 1MB,就會預分配和 len 一樣長度的空間,即 len=free;若是 len 大于 1MB,free 分配的空間大小就為 1MB」。

(4)SDS 是二進制安全的,除了可以儲存字符串以外還可以儲存二進制文件(如圖片、音頻,視頻等文件的二進制數據);而 c 語言中的字符串是以空字符串作為結束符,一些圖片中含有結束符,因此不是二進制安全的。

為了方便易懂,做了一個 c 語言的字符串和 SDS 進行對比的表格,如下所示:

c 語言字符串 SDS 獲取長度的時間復雜度為 O(n) 獲取長度的時間復雜度為 O(1) 不是二進制安全的是二進制安全的只能保存字符串還可以保存二進制數據 n 次增長字符串必然會帶來 n 次的內存分配 n 次增長字符串內存分配的次數 =n

String 類型應用

說到這里我相信很多人可以說已經精通 Redis 的 String 類型了,但是純理論的精通,理論還是得應用實踐,上面說到 String 可以用來存儲圖片,現在就以圖片存儲作為案例實現。

(1)首先要把上傳得圖片進行編碼,這里寫了一個工具類把圖片處理成了 Base64 得編碼形式,具體得實現代碼如下:

/** *  將圖片內容處理成 Base64 編碼格式  * @param file * @return */ public static String encodeImg(MultipartFile file) { byte[] imgBytes = null; try { imgBytes = file.getBytes(); } catch (IOException e) { e.printStackTrace(); } BASE64Encoder encoder = new BASE64Encoder(); return imgBytes==null?null:encoder.encode(imgBytes ); }

(2)第二步就是把處理后的圖片字符串格式存儲進 Redis 中,實現的代碼如下所示:

/** * Redis 存儲圖片  * @param file * @return */ public void uploadImageServiceImpl(MultipartFile image) { String imgId = UUID.randomUUID().toString(); String imgStr= ImageUtils.encodeImg(image); redisUtils.set(imgId , imgStr); //  后續操作可以把 imgId 存進數據庫對應的字段,如果需要從 redis 中取出,只要獲取到這個字段后從 redis 中取出即可。 }

這樣就是實現了圖片得二進制存儲,當然 String 類型得數據結構得應用也還有常規計數:「統計微博數、統計粉絲數」等。

Hash 類型

Hash 對象的實現方式有兩種分別是 ziplist、hashtable,其中 hashtable 的存儲方式 key 是 String 類型的,value 也是以 key value 的形式進行存儲。

字典類型的底層就是 hashtable 實現的,明白了字典的底層實現原理也就是明白了 hashtable 的實現原理,hashtable 的實現原理可以與 HashMap 的是底層原理相類比。

字典

兩者在新增時都會通過 key 計算出數組下標,不同的是計算法方式不同,HashMap 中是以 hash 函數的方式,而 hashtable 中計算出 hash 值后,還要通過 sizemask 屬性和哈希值再次得到數組下標。

我們知道 hash 表最大的問題就是 hash 沖突,為了解決 hash 沖突,假如 hashtable 中不同的 key 通過計算得到同一個 index,就會形成單向鏈表(「鏈地址法」),如下圖所示:

rehash

在字典的底層實現中,value 對象以每一個 dictEntry 的對象進行存儲,當 hash 表中的存放的鍵值對不斷的增加或者減少時,需要對 hash 表進行一個擴展或者收縮。

這里就會和 HashMap 一樣也會就進行 rehash 操作,進行重新散列排布。從上圖中可以看到有 ht[0] 和 ht[1] 兩個對象,先來看看對象中的屬性是干嘛用的。

在 hash 表結構定義中有四個屬性分別是 dictEntry **table、unsigned long size、unsigned long sizemask、unsigned long used,分別表示的含義就是「哈希表數組、hash 表大小、用于計算索引值,總是等于 size-1、hash 表中已有的節點數」。

ht[0] 是用來最開始存儲數據的,當要進行擴展或者收縮時,ht[0] 的大小就決定了 ht[1] 的大小,ht[0] 中的所有的鍵值對就會重新散列到 ht[1] 中。

擴展操作:ht[1] 擴展的大小是比當前 ht[0].used 值的二倍大的第一個 2 的整數冪;收縮操作:ht[0].used 的第一個大于等于的 2 的整數冪。

當 ht[0] 上的所有的鍵值對都 rehash 到 ht[1] 中,會重新計算所有的數組下標值,當數據遷移完后 ht[0] 就會被釋放,然后將 ht[1] 改為 ht[0],并新創建 ht[1],為下一次的擴展和收縮做準備。

漸進式 rehash

假如在 rehash 的過程中數據量非常大,Redis 不是一次性把全部數據 rehash 成功,這樣會導致 Redis 對外服務停止,Redis 內部為了處理這種情況采用「漸進式的 rehash」。

Redis 將所有的 rehash 的操作分成多步進行,直到都 rehash 完成,具體的實現與對象中的 rehashindex 屬性相關,「若是 rehashindex 表示為 - 1 表示沒有 rehash 操作」。

當 rehash 操作開始時會將該值改成 0,在漸進式 rehash 的過程「更新、刪除、查詢會在 ht[0] 和 ht[1] 中都進行」,比如更新一個值先更新 ht[0],然后再更新 ht[1]。

而新增操作直接就新增到 ht[1] 表中,ht[0] 不會新增任何的數據,這樣保證「ht[0] 只減不增,直到最后的某一個時刻變成空表」,這樣 rehash 操作完成。

上面就是字典的底層 hashtable 的實現原理,說完了 hashtable 的實現原理,我們再來看看 Hash 數據結構的兩一種存儲方式「ziplist(壓縮列表)」

ziplist

壓縮列表(ziplist)是一組連續內存塊組成的順序的數據結構,壓縮列表能夠節省空間,壓縮列表中使用多個節點來存儲數據。

壓縮列表是列表鍵和哈希鍵底層實現的原理之一,「壓縮列表并不是以某種壓縮算法進行壓縮存儲數據,而是它表示一組連續的內存空間的使用,節省空間」,壓縮列表的內存結構圖如下:

壓縮列表中每一個節點表示的含義如下所示:

鴻蒙官方戰略合作共建——HarmonyOS 技術社區

 zlbytes:4 個字節的大小,記錄壓縮列表占用內存的字節數。

 zltail:4 個字節大小,記錄表尾節點距離起始地址的偏移量,用于快速定位到尾節點的地址。

 zllen:2 個字節的大小,記錄壓縮列表中的節點數。

 entry:表示列表中的每一個節點。

 zlend:表示壓縮列表的特殊結束符號 0xFF。

再壓縮列表中每一個 entry 節點又有三部分組成,包括 previous_entry_ength、encoding、content。

鴻蒙官方戰略合作共建——HarmonyOS 技術社區

 previous_entry_ength 表示前一個節點 entry 的長度,可用于計算前一個節點的其實地址,因為他們的地址是連續的。

 encoding:這里保存的是 content 的內容類型和長度。

 content:content 保存的是每一個節點的內容。

說到這里相信大家已經都 hash 這種數據結構已經非常了解,若是第一次接觸 Redis 五種基本數據結構的底層實現的話,建議多看幾遍,下面來說一說 hash 的應用場景。

應用場景

哈希表相對于 String 類型存儲信息更加直觀,存儲更加方便,經常會用來做用戶數據的管理,存儲用戶的信息。

hash 也可以用作高并發場景下使用 Redis 生成唯一的 id。下面我們就以這兩種場景用作案例編碼實現。

存儲用戶數據

第一個場景比如我們要儲存用戶信息,一般使用用戶的 ID 作為 key 值,保持唯一性,用戶的其他信息(地址、年齡、生日、電話號碼等)作為 value 值存儲。

若是傳統的實現就是將用戶的信息封裝成為一個對象,通過序列化存儲數據,當需要獲取用戶信息的時候,就會通過反序列化得到用戶信息。

但是這樣必然會造成序列化和反序列化的性能的開銷,并且若是只修改其中的一個屬性值,就需要把整個對象序列化出來,操作的動作太大,造成不必要的性能開銷。

若是使用 Redis 的 hash 來存儲用戶數據,就會將原來的 value 值又看成了一個 k v 形式的存儲容器,這樣就不會帶來序列化的性能開銷的問題。

分布式生成唯一 ID

第二個場景就是生成分布式的唯一 ID,這個場景下就是把 redis 封裝成了一個工具類進行實現,實現的代碼如下:

// offset 表示的是 id 的遞增梯度值  public Long getId(String key,String hashKey,Long offset) throws BusinessException{ try { if (null == offset) { offset=1L; } //  生成唯一 id return redisUtil.increment(key, hashKey, offset); } catch (Exception e) { // 若是出現異常就是用 uuid 來生成唯一的 id 值  int randNo=UUID.randomUUID().toString().hashCode(); if (randNo   0) { randNo=-randNo; } return Long.valueOf(String.format( %16d , randNo)); } }

List 類型

Redis 中的列表在 3.2 之前的版本是使用 ziplist 和 linkedlist 進行實現的。在 3.2 之后的版本就是引入了 quicklist。

ziplist 壓縮列表上面已經講過了,我們來看看 linkedlist 和 quicklist 的結構是怎么樣的。

linkedlist 是一個雙向鏈表,他和普通的鏈表一樣都是由指向前后節點的指針。插入、修改、更新的時間復雜度尾 O(1),但是查詢的時間復雜度確實 O(n)。

linkedlist 和 quicklist 的底層實現是采用鏈表進行實現,在 c 語言中并沒有內置的鏈表這種數據結構,Redis 實現了自己的鏈表結構。

Redis 中都有哪些數據結構

Redis 中鏈表的特性:

鴻蒙官方戰略合作共建——HarmonyOS 技術社區

  每一個節點都有指向前一個節點和后一個節點的指針。

  頭節點和尾節點的 prev 和 next 指針指向為 null,所以鏈表是無環的。

  鏈表有自己長度的信息,獲取長度的時間復雜度為 O(1)。

Redis 中 List 的實現比較簡單,下面我們就來看看它的應用場景。

應用場景

Redis 中的列表可以實現「阻塞隊列」,結合 lpush 和 brpop 命令就可以實現。生產者使用 lupsh 從列表的左側插入元素,消費者使用 brpop 命令從隊列的右側獲取元素進行消費。

(1)首先配置 redis 的配置,為了方便我就直接放在 application.yml 配置文件中,實際中可以把 redis 的配置文件放在一個 redis.properties 文件單獨放置,具體配置如下:

spring redis: host: 127.0.0.1 port: 6379 password: user timeout: 0 database: 2 pool: max-active: 100 max-idle: 10 min-idle: 0 max-wait: 100000

(2)第二步創建 redis 的配置類,叫做 RedisConfig,并標注上 @Configuration 注解,表明他是一個配置類。

@Configuration public class RedisConfiguration { @Value( ${spring.redis.host} ) private String host; @Value(${spring.redis.port} ) private int port; @Value(${spring.redis.password} ) private String password; @Value(${spring.redis.pool.max-active} ) private int maxActive; @Value(${spring.redis.pool.max-idle} ) private int maxIdle; @Value(${spring.redis.pool.min-idle} ) private int minIdle; @Value(${spring.redis.pool.max-wait} ) private int maxWait; @Value(${spring.redis.database} ) private int database; @Value(${spring.redis.timeout} ) private int timeout; @Bean public JedisPoolConfig getRedisConfiguration(){ JedisPoolConfig jedisPoolConfig= new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(maxActive); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMinIdle(minIdle); jedisPoolConfig.setMaxWaitMillis(maxWait); return jedisPoolConfig; } @Bean public JedisConnectionFactory getConnectionFactory() { JedisConnectionFactory factory = new JedisConnectionFactory(); factory.setHostName(host); factory.setPort(port); factory.setPassword(password); factory.setDatabase(database); JedisPoolConfig jedisPoolConfig= getRedisConfiguration(); factory.setPoolConfig(jedisPoolConfig); return factory; } @Bean public RedisTemplate ?, ?  getRedisTemplate() { JedisConnectionFactory factory = getConnectionFactory(); RedisTemplate ?, ?  redisTemplate = new StringRedisTemplate(factory); return redisTemplate; } }

(3)第三步就是創建 Redis 的工具類 RedisUtil,自從學了面向對象后,就喜歡把一些通用的東西拆成工具類,好像一個一個零件,需要的時候,就把它組裝起來。

@Component public class RedisUtil { @Autowired private RedisTemplate String, Object  redisTemplate; /** *  存消息到消息隊列中  * @param key  鍵  * @param value  值  * @return */ public boolean lPushMessage(String key, Object value) { try { redisTemplate.opsForList().leftPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** *  從消息隊列中彈出消息  * @param key  鍵  * @return */ public Object rPopMessage(String key) { try { return redisTemplate.opsForList().rightPop(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** *  查看消息  * @param key  鍵  * @param start  開始  * @param end  結束  0  到  - 1 代表所有值  * @return */ public List Object  getMessage(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } }

這樣就完成了 Redis 消息隊列工具類的創建,在后面的代碼中就可以直接使用。

Set 集合

Redis 中列表和集合都可以用來存儲字符串,但是「Set 是不可重復的集合,而 List 列表可以存儲相同的字符串」,Set 集合是無序的這個和后面講的 ZSet 有序集合相對。

Set 的底層實現是「ht 和 intset」,ht(哈希表)前面已經詳細了解過,下面我們來看看 inset 類型的存儲結構。

inset 也叫做整數集合,用于保存整數值的數據結構類型,它可以保存 int16_t、int32_t 或者 int64_t 的整數值。

在整數集合中,有三個屬性值 encoding、length、contents[],分別表示編碼方式、整數集合的長度、以及元素內容,length 就是記錄 contents 里面的大小。

在整數集合新增元素的時候,若是超出了原集合的長度大小,就會對集合進行升級,具體的升級過程如下:

鴻蒙官方戰略合作共建——HarmonyOS 技術社區

  首先擴展底層數組的大小,并且數組的類型為新元素的類型。

  然后將原來的數組中的元素轉為新元素的類型,并放到擴展后數組對應的位置。

  整數集合升級后就不會再降級,編碼會一直保持升級后的狀態。

應用場景

Set 集合的應用場景可以用來「去重、抽獎、共同好友、二度好友」等業務類型。接下來模擬一個添加好友的案例實現:

@RequestMapping(value =  /addFriend , method = RequestMethod.POST) public Long addFriend(User user, String friend) { String currentKey = null; //  判斷是否是當前用戶的好友  if (AppContext.getCurrentUser().getId().equals(user.getId)) { currentKey = user.getId.toString(); } // 若是返回 0 則表示不是該用戶好友  return currentKey==null?0l:setOperations.add(currentKey, friend); }

假如兩個用戶 A 和 B 都是用上上面的這個接口添加了很多的自己的好友,那么有一個需求就是要實現獲取 A 和 B 的共同好友,那么可以進行如下操作:

public Set intersectFriend(User userA, User userB) { return setOperations.intersect(userA.getId.toString(), userB.getId.toString()); }

舉一反三,還可以實現 A 用戶自己的好友,或者 B 用戶自己的好友等,都可以進行實現。

ZSet 集合

ZSet 是有序集合,從上面的圖中可以看到 ZSet 的底層實現是 ziplist 和 skiplist 實現的,ziplist 上面已經詳細講過,這里來講解 skiplist 的結構實現。

skiplist 也叫做「跳躍表」,跳躍表是一種有序的數據結構,它通過每一個節點維持多個指向其它節點的指針,從而達到快速訪問的目的。

skiplist 有如下幾個特點:

鴻蒙官方戰略合作共建——HarmonyOS 技術社區

  有很多層組成,由上到下節點數逐漸密集,最上層的節點最稀疏,跨度也最大。

  每一層都是一個有序鏈表,至少包含兩個節點,頭節點和尾節點。

  每一層的每一個每一個節點都含有指向同一層下一個節點和下一層同一個位置節點的指針。

  如果一個節點在某一層出現,那么該以下的所有鏈表同一個位置都會出現該節點。

具體實現的結構圖如下所示:

Redis 中都有哪些數據結構

在跳躍表的結構中有 head 和 tail 表示指向頭節點和尾節點的指針,能快速的實現定位。level 表示層數,len 表示跳躍表的長度,BW 表示后退指針,在從尾向前遍歷的時候使用。

BW 下面還有兩個值分別表示分值(score)和成員對象(各個節點保存的成員對象)。

跳躍表的實現中,除了最底層的一層保存的是原始鏈表的完整數據,上層的節點數會越來越少,并且跨度會越來越大。

跳躍表的上面層就相當于索引層,都是為了找到最后的數據而服務的,數據量越大,條表所體現的查詢的效率就越高,和平衡樹的查詢效率相差無幾。

應用場景

因為 ZSet 是有序的集合,因此 ZSet 在實現排序類型的業務是比較常見的,比如在首頁推薦 10 個最熱門的帖子,也就是閱讀量由高到低,排行榜的實現等業務。

下面就選用獲取排行榜前前 10 名的選手作為案例實現,實現的代碼如下所示:

@Autowired private RedisTemplate redisTemplate; /** *  獲取前 10 排名  * @return */ public static List levelVO   getZset(String key, long baseNum, LevelService levelService){ ZSetOperations Serializable, Object  operations = redisTemplate.opsForZSet(); //  根據 score 分數值獲取前 10 名的數據  Set ZSetOperations.TypedTuple Object  set = operations.reverseRangeWithScores(key,0,9); List LevelVO  list= new ArrayList LevelVO  int i=1; for (ZSetOperations.TypedTuple Object  o:set){ int uid = (int) o.getValue(); LevelCache levelCache = levelService.getLevelCache(uid); LevelVO levelVO = levelCache.getLevelVO(); long score = (o.getScore().longValue() - baseNum + levelVO .getCtime())/CommonUtil.multiplier; levelVO .setScore(score); levelVO .setRank(i); list.add( levelVO ); i++; } return list; }

看完上述內容,你們對 Redis 中都有哪些數據結構有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注丸趣 TV 行業資訊頻道,感謝大家的支持。

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-08-01發表,共計11424字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 丰顺县| 兴城市| 曲靖市| 娄烦县| 剑阁县| 榕江县| 梨树县| 扎赉特旗| 龙岩市| 宿迁市| 伊宁县| 龙川县| 行唐县| 格尔木市| 黎川县| 新乐市| 涟水县| 德昌县| 静海县| 松阳县| 纳雍县| 青铜峡市| 新建县| 新郑市| 老河口市| 天长市| 林口县| 二连浩特市| 宾川县| 汝城县| 花垣县| 辉县市| 弋阳县| 杨浦区| 丽水市| 密云县| 洱源县| 绥化市| 永靖县| 浏阳市| 隆林|