簡介#
什麼是 Redis#
Redis 是內存高速緩存數據庫。全稱為:Remote Dictionary Server(遠程數據服務)。
Redis 由 c 語言編寫,key-value 存儲系統,支持 string、list、set、zset、hash 物種數據類型。
Redis 可用於緩存、事件發布或訂閱、高速隊列等場景。基於內存,可持久化。
Redis 的優勢#
- 讀寫性能優異
- 豐富的數據類型
- 原子性
- 豐富的特性(發布訂閱、通知、過期)
- 支持持久化(RDB 和 AOF 方式持久化)
- 分佈式
Redis 可以做什麼#
- 數據緩存
- 限時業務
- 計數器(incrby)
- 分佈式鎖(基於 Redisson 實現分佈式鎖)
- 限流(使用 Redis 中的 List 作為消息隊列)
- 消息隊列
安裝 / 啟動#
安裝#
啟動#
# 在前台啟動
redis-server
# 在後台啟動
brew services start redis
# 關閉
brew services stop redis
連接#
redis-cli
默認 6379 端口
redis 默認有 16 個數據庫,0-15
使用
select 1
來切換
基礎命令#
keys *
查看當前數據庫中有哪些 key(有 16 個數據庫)exists key
判斷這個 key 是否存在type key
查看這個 key 的類型del key
或unlink key
刪除某個 keyunlink
異步刪除
expire key 10
設置 key 過期時間 秒ttl key
查看 key 多少秒過期 -1 永遠不會過期 -2 已經過期select
切換數據庫dbsize
查看當前數據庫中 key 數量flushdb
清除當前數據庫所有 key
基礎類型(5 種)#
string(字符串)#
- string 類型是二進制安全的,可以包含任何數據
- string 是 redis 的基本數據類型,一個 value 最大 512Mb
命令#
set <key> <value>
get <key>
查看append <key> <value>
在 key 後追加 value- 如果這個 key 不存在,則會創建
strlen <key>
獲取這個 key 對應 value 長度setnx <key> <value>
只有當 key 不存在是創建incr <key>
這個 key 的 value+1decr <key>
value-1incrby/decrby <key> <num>
key 的 value 加 num 或者減 num
這些操作是原子性操作,在執行操作期間不會被其他線程打斷,因為 redis 是單線程
mset <key> <value> <key> <value> <key> <value>
一次設置多個 keymget <key> <key> <key>
一次獲取多個 keymsetnx <key> <value> <key> <value>
一次設置多個 當 key 不存在時- 原子性操作 一個失敗全部失敗
getrange <key> <開始> <結束>
獲取 key 中第 <開始>< 結束 > 中間的所有 valuesetrange <key> <offset> <value>
在 key 中第 offset 之後覆蓋設置 valuesetex <key> <過期時間> <value>
在設置 key 時設置過期時間get set <key> <value>
取之前的,取完之後再設置新的
數據結構#
Arraylist
使用場景#
- 緩存:使用 Redis 作為 MySQL 緩存,降低 MySQL 讀寫壓力
- 計數器:Redis 是單線程的優勢
- session:spring session + redis 實現 session 共享
list(列表)#
單鍵多值,底層雙向鏈表
命令#
lpush/rpush <key> <v1> <v2> <v3>
從雙向鏈表左右兩邊 push 值Large <key> 0 -1
編列鏈表,獲取所有rpop/lpop <key>
從左右獲取並刪除值rpoplpush <k1> <k2>
從 k1 右邊 pop 一個值,在 push 到 k2 左邊lindex <key> idnex
獲取第 index 個值llen <k>
獲取長度linsert before/after <v1> <v2>
在 v1 前 / 後插入 v2lrem <key> <n> <v>
從做百年刪除 n 個 valuelset <k> <idnex> <v>
將 index 設置成 v
數據結構#
元素較少的情況下是 ziplist,元素多了之後會將多個 ziplist 組成一個 quicklist
使用場景#
- 消息隊列
set(集合)#
set 集合中的元素不能重複,底層 hash 表,增刪改查都是 O (1) 複雜度
命令#
sadd <k> <v1> <v2>
添加smamber <k>
取出該集合中所有值sismamber <k> <v>
判斷是否存在這個 valuescard <k>
返回這個集合長度srem <k> <v> <v>
刪除spop <k>
隨機取出並刪除一個srandmameber <k> <n>
隨機取出 n 個,不刪除smove <k1> <k2> <v>
把 k1 中的 value 取出來,放到 k2 中sinter <k1> <k2>
返回 交集sunion <k1> <k2>
返回 並集sdiff <k1> <k2>
返回差集
使用場景#
- 點讚、轉發、收藏等
hash(散列)#
類似於 java 中的 map<string,object>,key 是對象名,value 裡邊是 field 和 value
命令#
hset <k> <field> <value>
設置 key 和 key 中的 field 和 valuehget <k> <field>
獲取 k 中 field 中的 valuehmset <k> <field1> <v1> <field2> <v2>
一次設置多個hexists <k1> <field>
查看 key 中的 field 是否存在hkeys <key>
獲取該 key 的所有 fieldhavals <key>
獲取該 key 的所有 value,沒有 fieldhincrby <k> <f> <n>
給 f 的 value 加 nhsetnx <k> <f> <v>
field 不存在則設置 value
數據結構#
當 field-value 長度短且個數少時使用 ziplist,否則使用 hashtable
使用場景#
- 緩存
Zset(有序集合)#
有序集合
每個元素都有評分(score),使用這個 score 來給集合排序,score 可以重複
加權排序 set,類似於 Java 中優先隊列,只不過 redis 中將隊列更換為 set
命令#
zadd <k> <score> <k> <score> <k> <score>
添加 key 和 score,會根據 score 排名arange <k> <start> <stop> withscores
獲取 scores 在開始和結束之間的元素zrangebyscore <k> <mim> <max>
返回並且排名 最小 scores 和最大 scoreszincrbg <k> <n> <v>
為元素 v 添加 n 的 scoreszrem <k> <v>
刪除zcount <k> <min> <max>
統計 min scores 和 max scores 之間元素個數zrank <k> <v>
獲取該元素的排名,從 0 開始
數據結構#
hash 跳躍表
使用場景#
- 排行榜
特殊類型(3 種)#
以下內容的學習均來自於此,使用了代碼片段
HyperLogLogs(基數統計)#
什麼是基數#
兩個集合的全集
例如:A = {1,2,3,4,5,6} ; B = {5,6,7,8,9} ; 基數 = {1,2,3,4,7,8,9} ,即不重複元素。
使用場景#
統計和計數,用於統計註冊 IP 數,每日訪問 IP 數,頁面實時 UV,線上用戶數等。
優勢#
基於基數估算的算法,能比較準確的估算出基數,可以使用少量固定的內存存儲並識別集合中唯一的原色。
這個算法並不一定準確,有 0.81% 的錯誤率,基數統計適合接受一定容錯的場景,比如統計訪客數量等。
命令#
127.0.0.1:6379> pfadd key1 a b c d e f g h i # 創建第一組元素
(integer) 1
127.0.0.1:6379> pfcount key1 # 統計元素的基數數量
(integer) 9
127.0.0.1:6379> pfadd key2 c j k l m e g a # 創建第二組元素
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 8
127.0.0.1:6379> pfmerge key3 key1 key2 # 合併兩組:key1 key2 -> key3 並集
OK
127.0.0.1:6379> pfcount key3
(integer) 13
Bitmap(位存儲)#
Bitmap 是通過二進制記錄信息,只有 0 和 1 兩個狀態
使用場景#
- 統計用戶登錄信息,登錄和未登錄
- 統計用戶點讚信息,點了和沒點
- 統計員工打卡信息,打了和沒打
命令#
-
使用
setbit <key> <offset> <value>
寫入數據,其中 key 可以任意,offset 只能為 Integer,value 只能為 0 或 1如果要統計員工一周的打卡情況,key 可以設置為 sign,offset 設置為 0-6,打卡為 0,未打卡為 1
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
- 使用
getbit <key> <offset>
來查看某一個 key 的狀態,返回 0 或 1
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 5
(integer) 0
- 使用
bitcount <key>
統計某一項數據的總數
127.0.0.1:6379> bitcount sign
(integer) 3
geospatial(地理位置)#
可以用於推算地理位置信息,如兩地之間距離等
命令#
geoadd
添加位置
127.0.0.1:6379> geoadd china:city 118.76 32.04 nanjing 112.55 37.86 taiyuan 123.43 41.80 shenyang
(integer) 3
127.0.0.1:6379> geoadd china:city 144.05 22.52 shengzhen 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 3
geopop
獲取指定成員的地理位置
127.0.0.1:6379> geopos china:city taiyuan nanjing
1) 1) "112.54999905824661255"
1) "37.86000073876942196"
2) 1) "118.75999957323074341"
1) "32.03999960287850968"
geodist
獲取兩地之間的距離,可以指定單位
127.0.0.1:6379> geodist china:city taiyuan shenyang m
"1026439.1070"
127.0.0.1:6379> geodist china:city taiyuan shenyang km
"1026.4391"
georadius
附近的人,獲得所有附近的人的地址,定位,通過半徑來查詢
georadiusbymember
顯示與指定成員一定半徑範圍內的其他成員
Redis 中的數據結構#
- 5 種基礎數據結構:string(字符串)、list(列表)、set(集合)、hash(散列)、zset(有序集合)
- 3 種特殊數據結構:HyperLogLogs(基數統計)、Bitmap(位存儲)、Geospatial(地理位置)
對應關係#
動態字符串 SDS#
Redis 並沒有使用 C 中的字符串,而是自己定義了一種數據結構,即簡單動態字符串(Simple Dynamic String SDS)。Redis 中 String 的底層數據結構使用的就是 SDS。
SDS 內存結構#
len
保存 SDS 保存字符串的長度buf[]
數組保存字符串的每個元素alloc
分別以 unint8、unint16、unint32、unint64 表示整個 SDSflags
始終為一字符,以低三位標示著頭部的類型
SDS 的優勢#
- O (1) 複雜度獲取字符串長度:在 Redis 中如果要獲取字符串長度只需讀取 len 屬性即可,在 C 中如果要獲取字符串長度需要遍歷整個字符串,是 O (n) 複雜度。使用
strlen <K>
獲取字符串長度。 - 防止緩衝區溢出:在 C 中,使用
strcat
中拼接兩字符串,如果沒有分配足夠的內存空間,會造成緩衝區溢出。SDS 會先根據 len 先推算拼接後的長度是否滿足內存空間要求。不滿足會先擴展,再拼接,從而防止緩衝區溢出。 - 減少修改字符串內存重新分配次數:SDS 使用
len
和alloc
實現空間預分配和惰性空間釋放。- 空間預分配:對字符串空間擴展時,擴展大於需求,從而減少字符串長度增加所需的內存重新分配次數。當字符串小於 1M 時,redis 會分配字符串大小一倍的空間,當字符串大小大於 1M 時,額外多分配 1M 空間。
- 惰性空間釋放:字符串長度減少時,不立即歸還內存,而是使用
alloc
屬性將這些空間記錄下來,等待後續使用。如果覺得這樣會浪費空間,可以自行設置定時釋放未使用空間。
- 二進制安全:C 中使用空字符來表示一個字符串的結束。但是如果 Redis 中保存的二進制文件(如圖片)中可能包含空字符,C 無法正確讀取。SDS 使用
len
屬性表示的長度來判斷字符串是否結束。
壓縮列表 ZipList#
ziplist 是一種為提高存儲效率設計的特殊編碼的雙向鏈表。可用於存儲字符串或整數。
ziplist 內存結構#
zlbytes
存儲整個 ziplist 所占用內存字節數zltail
存儲 ziplist 中最後一個 entry 的偏移量,用於快速定位最後一個 entry,方便快速 pop 操作zllen
存儲 ziplist 中 entry 的數量。zlend
是終止字節
Entry 結構#
prevlen
前一個 entry 的大小encoding
當前 entry 的類型和長度entry-data
entry 數據
PS:如果 entry-data 中保存的是整形數據,encoding 會和 entrydata 合為一,此時 entry 結構為
ziplist 優點#
節省內存
- 普通 List 保存是,每個元素占用空間大小一致,均為當前 List 中最大元素所占的空間大小,造成了空間浪費。
- ziplist 通過特殊的數據結構,盡量使每個元素占用與自身大小相同的空間,減少空間浪費。
- 傳統 List 在內存中地址連續,ziplist 所占大小不一致,所以地址不連續,需要通過
prelen
字段保存上個元素的 length 來解決遍歷問題。
ziplist 缺點#
- 不預留內存空間,list 中元素被移除之後 list 無用空間立即歸還,這導致每一次修改 list 都需要重新分配空間。
快表 QuickList#
以 ziplist 作為結點的雙端鏈表,如何理解 QuickList:List<List<ziplist>> quickList
quicklist 在內存中結構#
quicklistNode
鏈表中的節點,在 quicklist 中,這個節點是 ziplistquickListLZF
ziplist 經過 LZ4 算法壓縮後,可以包裝成一個quickListLZF
結構quicklistBookmark
位於 quicklist 尾部quicklist
- head、tail 指向頭尾指針
- len 表示鏈表中的節點
- count 表示 quicklist 中 ziplist 中 entry 數目
- fill 控制每個 quicklist 中 ziplist 最大所占空間
- compress 控制是否要對每個 ziplist 以 LZ4 算法進行壓縮以節省空間
quicklistIter
迭代器quicklistEntry
對 ziplist 中 entry 封裝
哈希表 - Dict-HashTable#
哈希表於大多數語言中的 HashTable 無異,差別主要存在於解決哈希衝突、擴容。
哈希衝突#
使用鏈地址法
擴容#
擴容步驟:
- 根據原哈希表所占空間大小創建一個新哈希表,大小為原哈希表一倍,(收縮也是減小為原來一倍大小)
- 使用哈希算法重新計算索引值
- 新表創建並賦值完畢之後刪除原表
擴容發生條件
- redis 沒有執行
bgsave
或bgrewriteaof
命令,負載因子大於等於 1 - redis 沒有執行
bgsave
或bgrewriteaof
命令,負載因子大於等於 5
負載因子 = 哈希表中節點數量 / 哈希表大小
整數集 IntSet#
intset 是 redis 中集合類型底層實現之一,當一個集合只包含整數數值元素,並且元素數量較少,redis 使用 intset 作為集合鍵的底層實現
內存結構#
encoding
編碼方式length
存儲整數個數contents
指向實際存儲數值的連續內存區域
擴容#
intset 底層為數組,當新插入的元素大小大於原數組中最大元素的大小時,就需要對整個數組進行擴容,每個元素所占空間大小修改為最大元素的大小
會擴容但不會縮容!
跳表 ZSkipList#
跳躍表的目的類似於紅黑樹,為了性能。跳躍表可以在對數時間內完成插入、查找、刪除。但是跳躍表所占空間較大,屬於空間換時間的數據結構。
什麼是跳躍表#
傳統鏈表如果需要查詢元素,需要從頭遍歷整個鏈表知道找到查詢元素,時間複雜度 O (n)。跳躍表通過增加多級索引的方式減少遍歷長度,從而加快查找速度。跳躍表實現了平衡樹、哈希表類似的效果。
為什麼不使用平衡樹,而是跳躍表