1. 程式人生 > 資料庫 >Java中高階核心知識全面解析——Redis([資料型別、編碼、底層資料結構]、5種資料型別的編碼和資料結構、記憶體回收和共享)6

Java中高階核心知識全面解析——Redis([資料型別、編碼、底層資料結構]、5種資料型別的編碼和資料結構、記憶體回收和共享)6

目錄

一、Redis資料型別、編碼、底層資料結構

1.Redis構建的型別系統

Redis構建了自己的型別系統,主要包括

  • redisObject物件
  • 基於redisObject物件的型別檢查
  • 基於redisObject物件的顯示多型函式
  • 對redisObject進行分配、共享和銷燬的機制

C語言不是面嚮物件語言,這裡將redisObject稱呼為物件是為了講述方便,讓裡面的內容更容易被理解,redisObject其實是一個結構體。

1)redisObject物件

Redis內部使用一個redisObject物件來表示所有的key和value,每次在Redis資料塊中建立一個鍵值對時,一個是鍵物件,一個是值物件,而Redis中的每個物件都是由redisObject結構來表示。

在Redis中,鍵總是一個字串物件,而值可以是字串、列表、集合等物件,所以我們通常說鍵為字串鍵,表示這個鍵對應的值為字串物件,我們說一個鍵為集合鍵時,表示這個鍵對應的值為集合物件

redisobject最主要的資訊:

redisobject原始碼 
typedef struct redisObject{ 
	//型別 
	unsigned type:4; 
	//編碼 
	unsigned encoding:4; 
	//指向底層資料結構的指標 
	void *ptr; 
	//引用計數 
	int refcount; 
	//記錄最後一次被程式訪問的時間 
	unsigned lru:22; 
}robj
  • type代表一個value物件具體是何種資料型別
    • type key :判斷物件的資料型別
  • encoding屬性和*prt指標
    • prt指標指向物件底層的資料結構,而資料結構由encoding屬性來決定
    • 每種型別的物件至少使用了兩種不同的編碼,而這些編碼對使用者是完全透明的。
    • object encoding key命令可以檢視值物件的編碼

2)命令的型別檢查和多型

①、Redis命令分類

  • 一種是隻能用於對應資料型別的命令,例如LPUSH和LLEN只能用於列表鍵, SADDSRANDMEMBER只能用於集合鍵。
  • 另一種是可以用於任何型別鍵的命令。比如TTL

當執行一個處理資料型別的命令時,Redis執行以下步驟:

  • 根據給定 key
    ,在資料庫字典中查詢和它相對應的 redisObject ,如果沒找到,就返回 NULL
  • 檢查 redisObjecttype 屬性和執行命令所需的型別是否相符,如果不相符,返回型別錯誤。
  • 根據 redisObjectencoding 屬性所指定的編碼,選擇合適的操作函式來處理底層的資料結構。
  • 返回資料結構的操作結果作為命令的返回值。

小插曲:
更多阿里、騰訊、美團、京東等一線網際網路大廠Java面試真題;包含:基礎、併發、鎖、JVM、設計模式、資料結構、反射/IO、資料庫、Redis、Spring、訊息佇列、分散式、Zookeeper、Dubbo、Mybatis、Maven、面經等。
更多Java程式設計師技術進階小技巧;例如高效學習(如何學習和閱讀程式碼、面對枯燥和量大的知識)高效溝通(溝通方式及技巧、溝通技術)
更多Java大牛分享的一些職業生涯分享文件


請點選社群,免費獲取


比你優秀的對手在學習,你的仇人在磨刀,你的閨蜜在減肥,隔壁老王在練腰, 我們必須不斷學習,否則我們將被學習者超越!
趁年輕,使勁拼,給未來的自己一個交代!

2.5種資料型別對應的編碼和資料結構

1)string

string 是最常用的一種資料型別,普通的key/value儲存都可以歸結為string型別,value不僅是string,也可以是數字。其他幾種資料型別的構成元素也都是字串,注意Redis規定字串的長度不能超過512M

編碼字串物件的編碼可以是intrawembstr

  • int編碼
    • 儲存的是可以用long型別表示的整數值
  • raw編碼
    • 儲存長度大於44位元組的字串
  • embstr編碼
    • 儲存長度小於44位元組的字串

int用來儲存整數值,raw用來儲存長字串,embstr用來儲存短字串。embstr編碼是用來專門儲存短字串的一種優化編碼。

Redis中對於浮點型也是作為字串儲存的,在需要時再將其轉換成浮點數型別

編碼的轉換

  • 當 int 編碼儲存的值不再是整數,或大小超過了long的範圍時,自動轉化為raw
  • 對於 embstr 編碼,由於 Redis 沒有對其編寫任何的修改程式(embstr 是隻讀的),在對embstr物件進行修改時,都會先轉化為raw再進行修改,因此,只要是修改embstr物件,修改後的物件一定是raw的,無論是否達到了44個位元組。

常用命令

  • set/get
    • set:設定key對應的值為string型別的value (多次set name會覆蓋)
    • get:獲取key對應的值
  • mset /mget
    • mset 批量設定多個key的值,如果成功表示所有值都被設定,否則返回0表示沒有任何值被設定
    • mget批量獲取多個key的值,如果不存在則返回null
127.0.0.1:6379> mset user1:name redis user1:age 22 
OK
127.0.0.1:6379> mget user1:name user1:age 
1) "redis" 
2) "22"
  • 應用場景
    • 類似於雜湊操作,儲存物件

incr && incrby<原子操作>

  • incr對key對應的值進行加加操作,並返回新的值,incrby加指定的值

decr && decrby<原子操作>

  • decr對key對應的值進行減減操做,並返回新的值,decrby減指定的值

setnx <小小體驗一把分散式鎖,真香>

  • 設定Key對應的值為string型別的值,如果已經存在則返回0

setex

  • 設定key對應的值為string型別的value,並設定有效期

setrange/getrange

  • setrange從指定位置替換字串
  • getrange獲取key對應value子字串

其他命令

  • msetnx 同mset,不存在就設定,不會覆蓋已有的key
  • getset 設定key的值,並返回key舊的值
  • append 給指定的key的value追加字串,並返回新字串的長度
  • strlen 返回key對應的value字串的長度

應用場景

  • 因為string型別是二進位制安全的,可以用來存放圖片,視訊等內容。
  • 由於redis的高效能的讀寫功能,而string型別的value也可以是數字,可以用做計數器(使用INCR,DECR指令)。比如分散式環境中統計系統的線上人數,秒殺等。
  • 除了上面提到的,還有用於SpringSession實現分散式session
  • 分散式系統全域性序列號

2)list列表,它是簡單的字串列表,你可以新增一個元素到列表的頭部,或者尾部。

編碼

  • 列表物件的編碼可以是ziplist(壓縮列表)和linkedlist(雙端連結串列)。
  • 編碼轉換
    • 同時滿足下面兩個條件時使用壓縮列表:
      • 列表儲存元素個數小於512個
      • 每個元素長度小於64位元組
    • 不能滿足上面兩個條件使用linkedlist(雙端列表)編碼
  • 常用命令
    • lpush: 從頭部加入元素
127.0.0.1:6379> lpush list1 hello 
(integer) 1 
127.0.0.1:637 9> lpush list1 world 
(integer) 2 
127.0.0.1:6379> lrange list1 0 -1 
1) "world" 
2) "hello"
  • rpush:從尾部加入元素
127.0.0.1:6379> rpush list2 world 
(integer) 1 
127.0.0.1:6379> rpush list2 hello 
(integer) 2 
127.0.0.1:6379> lrange list2 0 -1 
1) "world" 
2) "hello"
  • lpop: 從list的頭部刪除元素,並返回刪除的元素
127.0.0.1:6379> lrange list1 0 -1 
1) "world" 
2) "hello" 
127.0.0.1:6379> lpop list1 
"world" 
127.0.0.1:6379> lrange list1 0 -1 
1) "hello"
  • rpop:從list的尾部刪除元素,並返回刪除的元素
127.0.0.1:6379> lrange list2 0 -1 
1) "hello" 
2) "world" 
127.0.0.1:6379> rpop list2 
"world" 
127.0.0.1:6379> lrange list2 0 -1 
1) "hello"
  • rpoplpush: 第一步從尾部刪除元素,第二步從首部插入元素 結合著使用
  • linsert:插入方法 linsert listname before [集合的元素] [插入的元素]
127.0.0.1:6379> lpush list3 hello 
(integer) 1 
127.0.0.1:6379> lpush list3 world 
(integer) 2 
127.0.0.1:6379> linsert list3 before hello start 
(integer) 3 
127.0.0.1:6379> lrange list3 0 -1 
1) "world" 
2) "start" 
3) "hello"
  • lset :替換指定下標的元素
127.0.0.1:6379> lrange list1 0 -1 
1) "a" 
2) "b" 
127.0.0.1:6379> lset list1 0 v 
OK
127.0.0.1:6379> lrange list1 0 -1 
1) "v" 
2) "b"
  • lrm : 刪除元素,返回刪除的個數
127.0.0.1:6379> lrange list1 0 -1 
1) "b" 
2) "b" 
3) "a" 
4) "b" 
127.0.0.1:6379> lrange list1 0 -1 
1) "a" 
2) "b"
  • lindex: 返回list中指定位置的元素
  • llen: 返回list中的元素的個數

實現資料結構

  • Stack(棧)
    • LPUSH+LPOP
  • Queue(佇列)
    • LPUSH + RPOP
  • Blocking MQ(阻塞佇列)
    • LPUSH+BRPOP

應用場景

  • 實現簡單的訊息佇列
  • 利用LRANGE命令,實現基於Redis的分頁功能

3)set

集合物件set是string型別(整數也會轉成string型別進行儲存)的無序集合。注意集合和列表的區別:集合中的元素是無序的,因此不能通過索引來操作元素;集合中的元素不能有重複。

編碼

  • 集合物件的編碼可以是intset或者hashtable
    • intset編碼的集合物件使用整數集合作為底層實現,集合物件包含的所有元素都被儲存在整數集合中。
    • hashtable編碼的集合物件使用字典作為底層實現,字典的每個鍵都是一個字串物件,這裡的每個字串物件就是一個集合中的元素,而字典的值全部設定為null。當使用HT編碼時,Redis中的集合SET相當於Java中的HashSet,內部的鍵值對是無序的,唯一的。內部實現相當於一個特殊的字典,字典中所有value都是NULL。
  • 編碼轉換
    • 當集合滿足下列兩個條件時,使用intset編碼:
      • 集合物件中的所有元素都是整數
      • 集合物件所有元素數量不超過512

常用命令

  • sadd: 向集合中新增元素 (set不允許元素重複)
  • smembers: 檢視集合中的元素
127.0.0.1:6379> sadd set1 aaa 
(integer) 1 
127.0.0.1:6379> sadd set1 bbb 
(integer) 1 
127.0.0.1:6379> sadd set1 ccc 
(integer) 1 
127.0.0.1:6379> smembers set1 
1) "aaa" 
2) "ccc" 
3) "bbb"
  • srem: 刪除集合元素
  • spop: 隨機返回刪除的key
  • sdiff :返回兩個集合的不同元素 (哪個集合在前就以哪個集合為標準)
127.0.0.1:6379> smembers set1 
1) "ccc" 
2) "bbb" 
127.0.0.1:6379> smembers set2 
1) "fff" 
2) "rrr" 
3) "bbb" 
127.0.0.1:6379> sdiff set1 set2 
1) "ccc" 
127.0.0.1:6379> sdiff set2 set1 
1) "fff" 
2) "rrr"
  • sinter: 返回兩個集合的交集
  • sinterstore: 返回交集結果,存入目標集合
127.0.0.1:6379> sinterstore set3 set1 set2 
(integer) 1 
127.0.0.1:6379> smembers set3 
1) "bbb"
  • sunion: 取兩個集合的並集
  • sunionstore: 取兩個集合的並集,並存入目標集合
  • smove: 將一個集合中的元素移動到另一個集合中
  • scard: 返回集合中的元素個數
  • sismember: 判斷某元素是否存在某集合中,0代表否 1代表是
  • srandmember: 隨機返回一個元素
127.0.0.1:6379> srandmember set1 1 
1) "bbb" 
127.0.0.1:6379> srandmember set1 2 
1) "ccc" 
2) "bbb"

應用場景

  • 對於 set 資料型別,由於底層是字典實現的,查詢元素特別快,另外set 資料型別不允許重複,利用這兩個特性我們可以進行全域性去重,比如在使用者註冊模組,判斷使用者名稱是否註冊;微信點贊,微信抽獎小程式
  • 另外就是利用交集、並集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好,可能認識的人等功能。

4)zset

和集合物件相比,有序集合物件是有序的。與列表使用索引下表作為排序依據不同,有序集合為每一個元素設定一個分數(score)作為排序依據。

編碼

  • 有序集合的編碼可以使ziplist或者skiplist
    • ziplist編碼的有序集合物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存,第一個節點儲存元素的成員,第二個節點儲存元素的分值。並且壓縮列表內的集合元素按分值從小到大的順序進行排列,小的放置在靠近表頭的位置,大的放置在靠近表尾的位置。
    • skiplist編碼的依序集合物件使用zset結構作為底層實現,一個zset結構同時包含一個字典和一個跳躍表
typedef struct zset{ 
	//跳躍表 
	zskiplist *zsl; 
	//字典 
	dict *dice; 
}
zset

字典的鍵儲存元素的值,字典的值儲存元素的分值,跳躍表節點的object屬性儲存元素的成員,跳躍表節點的score屬性儲存元素的分值。這兩種資料結構會通過指標來共享相同元素的成員和分值,所以不會產生重複成員和分值,造成記憶體的浪費。
  • 編碼轉換
    • 當有序結合物件同時滿足以下兩個條件時,物件使用ziplist編碼,否則使用skiplist編碼
      • 儲存的元素數量小於128
      • 儲存的所有元素長度都小於64位元組

常用命令

  • zrem: 刪除集合中名稱為key的元素member
  • zincrby: 以指定值去自動遞增
  • zcard: 檢視元素集合的個數
  • zcount: 返回score在給定區間中的數量
127.0.0.1:6379> zrange zset 0 -1 
1) "one" 
2) "three" 
3) "two" 
4) "four" 
5) "five" 
6) "six" 
127.0.0.1:6379> zcard zset 
(integer) 6 
127.0.0.1:6379> zcount zset 1 4 
(integer) 4
  • zrangebyscore: 找到指定區間範圍的資料進行返回
127.0.0.1:6379> zrangebyscore zset 0 4 withscores 
1) "one" 
2) "1" 
3) "three" 
4) "2" 
5) "two" 
6) "2" 
7) "four" 
8) "4"
  • zremrangebyrank zset from to: 刪除索引
127.0.0.1:6379> zrange zset 0 -1 
1) "one" 
2) "three" 
3) "two" 
4) "four" 
5) "five" 
6) "six" 
127.0.0.1:6379> zremrangebyrank zset 1 3 
(integer) 3 
127.0.0.1:6379> zrange zset 0 -1 
1) "one" 
2) "five" 
3) "six"
  • zremrangebyscore zset from to: 刪除指定序號
127.0.0.1:6379> zrange zset 0 -1 withscores 
1) "one" 
2) "1" 
3) "five" 
4) "5" 
5) "six" 
6) "6" 
127.0.0.1:6379> zremrangebyscore zset 3 6 
(integer) 2 
127.0.0.1:6379> zrange zset 0 -1 withscores 
1) "one" 
2) "1"
  • zrank: 返回排序索引 (升序之後再找索引)
  • zrevrank: 返回排序索引 (降序之後再找索引)

應用場景

  • 對於 zset 資料型別,有序的集合,可以做範圍查詢,排行榜應用,取TOP N操作等。

5)hash

hash物件的鍵是一個字串型別,值是一個鍵值對集合

編碼

  • hash物件的編碼可以是ziplist或者hashtable
    • 當使用ziplist,也就是壓縮列表作為底層實現時,新增的鍵值是儲存到壓縮列表的表尾。
    • hashtable 編碼的hash表物件底層使用字典資料結構,雜湊物件中的每個鍵值對都使用一個字典鍵值對。Redis中的字典相當於Java裡面HashMap,內部實現也差不多類似,都是通過“陣列+連結串列”的鏈地址法來解決雜湊衝突的,這樣的結構吸收了兩種不同資料結構的優點。
  • 編碼轉換
    • 當同時滿足下面兩個條件使用ziplist編碼,否則使用hashtable編碼
      • 列表儲存元素個數小於512個
      • 每個元素長度小於64位元組
  • hash是一個String型別的field和value之間的對映表
  • Hash特別適合儲存物件
  • 所儲存的成員較少時資料儲存為zipmap,當成員數量增大時會自動轉成真正的HashMap,此時encoding為ht
  • Hash命令詳解
    • hset/hget
      • hset hashname hashkey hashvalue
      • hget hashname hashkey
127.0.0.1:6379> hset user id 1 
(integer) 1 
127.0.0.1:6379> hset user name z3 
(integer) 1 
127.0.0.1:6379> hset user add shanxi 
(integer) 1 
127.0.0.1:6379> hget user id "1" 
127.0.0.1:6379> hget user name "z3" 
127.0.0.1:6379> hget user add "shanxi"
  • hmset/hmget
    • hmset hashname hashkey1hashvalue1 hashkey2 hashvalue2 hashkey3 hashvalue3
    • hget hashname hashkey1 hashkey2 hashkey3
127.0.0.1:6379> hmset user id 1 name z3 add shanxi 
OK
127.0.0.1:6379> hmget user id name add 
1) "1" 
2) "z3" 
3) "shanxi"
  • hsetnx/hgetnx
  • hincrby/hdecrby
127.0.0.1:6379> hincrby user2 id 3 
(integer) 6 
127.0.0.1:6379> hget user2 id 
"6"
  • hexist 判斷是否存在key,不存在返回0
127.0.0.1:6379> hget user2 id 
"6"
  • hlen 返回hash集合裡所有的鍵值數
127.0.0.1:6379> hmset user3 id 3 name w5 
OK
127.0.0.1:6379> hlen user3 
(integer) 2
  • hdel :刪除指定的hash的key
  • hkeys 返回hash裡所有的欄位
  • hvals 返回hash裡所有的value
  • hgetall:返回hash集合裡所有的key和value
127.0.0.1:6379> hgetall user3 
1) "id" 
2) "3" 
3) "name" 
4) "w3" 
5) "add" 
6) "beijing"

優點

  • 同類資料歸類整合儲存,方便資料管理,比如單個使用者的所有商品都放在一個hash表裡面。
  • 相比string操作消耗記憶體cpu更小

缺點

  • hash結構的儲存消耗要高於單個字串
  • 過期功能不能使用在field上,只能用在key上
  • redis叢集架構不適合大規模使用

應用場景

  1. 對於 hash 資料型別,value 存放的是鍵值對,比如可以做單點登入存放使用者資訊。
  2. 存放商品資訊,實現購物車

小插曲:
更多阿里、騰訊、美團、京東等一線網際網路大廠Java面試真題;包含:基礎、併發、鎖、JVM、設計模式、資料結構、反射/IO、資料庫、Redis、Spring、訊息佇列、分散式、Zookeeper、Dubbo、Mybatis、Maven、面經等。
更多Java程式設計師技術進階小技巧;例如高效學習(如何學習和閱讀程式碼、面對枯燥和量大的知識)高效溝通(溝通方式及技巧、溝通技術)
更多Java大牛分享的一些職業生涯分享文件


請點選社群,免費獲取


比你優秀的對手在學習,你的仇人在磨刀,你的閨蜜在減肥,隔壁老王在練腰, 我們必須不斷學習,否則我們將被學習者超越!
趁年輕,使勁拼,給未來的自己一個交代!

3. 記憶體回收和記憶體共享

typedef struct redisObject{ 
	//型別 
	unsigned type:4; 
	//編碼 
	unsigned encoding:4; 
	//指向底層資料結構的指標 
	void *ptr; 
	//引用計數 
	int refcount; 
	//記錄最後一次被程式訪問的時間 
	unsigned lru:22; 
}robj

記憶體回收:因為c語言不具備自動記憶體回收功能,當將redisObject物件作為資料庫的鍵或值而不是作為引數儲存時其生命週期是非常長的,為了解決這個問題,Redis自己構建了一個記憶體回收機制,通過redisobject結構中的refcount實現.這個屬性會隨著物件的使用狀態而不斷變化。

  1. 建立一個新物件,屬性初始化為1
  2. 物件被一個新程式使用,屬性refcount加1
  3. 物件不再被一個程式使用,屬性refcount減1
  4. 當物件的引用計數值變為0時,物件所佔用的記憶體就會被釋放

記憶體共享:refcount屬性除了能實現記憶體回收以外,還能實現記憶體共享

  1. 將資料塊的鍵的值指標指向一個現有值的物件
  2. 將被共享的值物件引用refcount加1Redis的共享物件目前只支援整數值的字串物件。之所以如此,實際上是對記憶體和CPU(時間)的平衡:共享物件雖然會降低記憶體消耗,但是判斷兩個物件是否相等卻需要消耗額外的時間。對於整數值,判斷操作複雜度為o(1),對於普通字串,判斷複雜度為o(n);而對於雜湊,列表,集合和有序集合,判斷的複雜度為o(n^2).雖然共享的物件只能是整數值的字串物件,但是5種類型都可能使用共享物件。

參考資料:《Java中高階核心知識全面解析》限量100份,有一些人已經通過我之前的文章獲取了哦!
名額有限先到先得!!!
有想要獲取這份學習資料的同學可以