本篇博文目標(biāo):
1胰柑、了解 Redis 的定位與基本特性
2、掌握 Redis 基本數(shù)據(jù)類型的操作命令爬泥、底層存儲(chǔ)結(jié)構(gòu)、應(yīng)用場(chǎng)景
內(nèi)容定位:
1崩瓤、沒用過 Redis 的同學(xué)
2袍啡、項(xiàng)目里面在用 Redis,但是只知道可以做緩存却桶,只知道 get set 方法
3境输、不知道 Redis 其他數(shù)據(jù)類型、各種數(shù)據(jù)類型的底層存儲(chǔ)結(jié)構(gòu)的同學(xué)
基于最新版本:5.0.5
1. Redis 入門
1.1Redis 誕生歷程
1.1.1.從一個(gè)故事開始
08 年的時(shí)候有一個(gè)意大利西西里島的小伙子颖系,筆名 antirez嗅剖,創(chuàng)建了一個(gè)訪客信息網(wǎng)站 LLOOGG.COM。有的時(shí)候我們需要知道網(wǎng)站的訪問情況嘁扼,比如訪客的 IP信粮、操作系統(tǒng)、瀏覽器趁啸、使用的搜索關(guān)鍵詞强缘、所在地區(qū)、訪問的網(wǎng)頁地址等等不傅。在國(guó)內(nèi)旅掂,有很多網(wǎng)站提供了這個(gè)功能,比如 CNZZ访娶,百度統(tǒng)計(jì)商虐,國(guó)外也有谷歌的 Google Analytics。我們不用自己寫代碼去實(shí)現(xiàn)這個(gè)功能崖疤,只需要在全局的 footer 里面嵌入一段JS 代碼就行了秘车,當(dāng)頁面被訪問的時(shí)候,就會(huì)自動(dòng)把訪客的信息發(fā)送到這些網(wǎng)站統(tǒng)計(jì)的服務(wù)器劫哼,然后我們登錄后臺(tái)就可以查看數(shù)據(jù)了鲫尊。
LLOOGG.COM 提供的就是這種功能,它可以查看最多 10000 條的最新瀏覽記錄沦偎。這樣的話疫向,它需要為每一個(gè)網(wǎng)站創(chuàng)建一個(gè)列表(List)咳蔚,不同網(wǎng)站的訪問記錄進(jìn)入到不同的列表。如果列表的長(zhǎng)度超過了用戶指定的長(zhǎng)度搔驼,它需要把最早的記錄刪除(先進(jìn)先出)谈火。
當(dāng) LLOOGG.COM 的用戶越來越多的時(shí)候,它需要維護(hù)的列表數(shù)量也越來越多舌涨,這種記錄最新的請(qǐng)求和刪除最早的請(qǐng)求的操作也越來越多糯耍。LLOOGG.COM 最初使用的數(shù)據(jù)庫是 MySQL,可想而知囊嘉,因?yàn)槊恳淮斡涗浐蛣h除都要讀寫磁盤温技,因?yàn)閿?shù)據(jù)量和并發(fā)量太大,在這種情況下無論怎么去優(yōu)化數(shù)據(jù)庫都不管用了扭粱。
考慮到最終限制數(shù)據(jù)庫性能的瓶頸在于磁盤舵鳞,所以 antirez 打算放棄磁盤,自己去實(shí)現(xiàn)一個(gè)具有列表結(jié)構(gòu)的數(shù)據(jù)庫的原型琢蛤,把數(shù)據(jù)放在內(nèi)存而不是磁盤蜓堕,這樣可以大大地提升列表的 push 和 pop 的效率。antirez 發(fā)現(xiàn)這種思路確實(shí)能解決這個(gè)問題博其,所以用 C 語言重寫了這個(gè)內(nèi)存數(shù)據(jù)庫套才,并且加上了持久化的功能,09 年慕淡,Redis 橫空出世了背伴。從最開始只支持列表的數(shù)據(jù)庫,到現(xiàn)在支持多種數(shù)據(jù)類型峰髓,并且提供了一系列的高級(jí)特性挂据,Redis 已經(jīng)成為一個(gè)在全世界被廣泛使用的開源項(xiàng)目。
為什么叫 REDIS 呢儿普?它的全稱是 REmote DIctionary Service崎逃,直接翻譯過來是遠(yuǎn)程字典服務(wù)。
從 Redis 的誕生歷史我們看到了眉孩,在某些場(chǎng)景中个绍,關(guān)系型數(shù)據(jù)庫并不適合用來存儲(chǔ)我們的 Web 應(yīng)用的數(shù)據(jù)。那么浪汪,關(guān)系型數(shù)據(jù)庫和非關(guān)系型數(shù)據(jù)庫巴柿,或者說 SQL 和 NoSQL,到底有什么不一樣呢死遭?
1.2. Redis 定位與特性
1.2.1.SQL 與 NoSQL
在絕大部分時(shí)候广恢,我們都會(huì)首先考慮用關(guān)系型數(shù)據(jù)庫來存儲(chǔ)我們的數(shù)據(jù),比如SQLServer呀潭,Oracle钉迷,MySQL 等等至非。
關(guān)系型數(shù)據(jù)庫的特點(diǎn):
- 它以表格的形式,基于行存儲(chǔ)數(shù)據(jù)糠聪,是一個(gè)二維的模式荒椭。
- 它存儲(chǔ)的是結(jié)構(gòu)化的數(shù)據(jù),數(shù)據(jù)存儲(chǔ)有固定的模式(schema)舰蟆,數(shù)據(jù)需要適應(yīng)表結(jié)構(gòu)趣惠。
- 表與表之間存在關(guān)聯(lián)(Relationship)。
- 大部分關(guān)系型數(shù)據(jù)庫都支持 SQL(結(jié)構(gòu)化查詢語言)的操作身害,支持復(fù)雜的關(guān)聯(lián)查詢味悄。
- 通過支持事務(wù)(ACID 酸)來提供嚴(yán)格或者實(shí)時(shí)的數(shù)據(jù)一致性。
但是使用關(guān)系型數(shù)據(jù)庫也存在一些限制塌鸯,比如:
- 要實(shí)現(xiàn)擴(kuò)容的話侍瑟,只能向上(垂直)擴(kuò)展,比如磁盤限制了數(shù)據(jù)的存儲(chǔ)界赔,就要擴(kuò)大磁盤容量,通過堆硬件的方式牵触,不支持動(dòng)態(tài)的擴(kuò)縮容淮悼。水平擴(kuò)容需要復(fù)雜的技術(shù)來實(shí)現(xiàn),比如分庫分表揽思。
- 表結(jié)構(gòu)修改困難袜腥,因此存儲(chǔ)的數(shù)據(jù)格式也受到限制。
- 在高并發(fā)和高數(shù)據(jù)量的情況下钉汗,我們的關(guān)系型數(shù)據(jù)庫通常會(huì)把數(shù)據(jù)持久化到磁盤羹令,基于磁盤的讀寫壓力比較大。
為了規(guī)避關(guān)系型數(shù)據(jù)庫的一系列問題损痰,我們就有了非關(guān)系型的數(shù)據(jù)庫福侈,我們一般把它叫做“non-relational”或者“Not Only SQL”。NoSQL 最開始是不提供 SQL 的數(shù)據(jù)庫的意思卢未,但是后來意思慢慢地發(fā)生了變化肪凛。
非關(guān)系型數(shù)據(jù)庫的特點(diǎn):
- 存儲(chǔ)非結(jié)構(gòu)化的數(shù)據(jù),比如文本辽社、圖片伟墙、音頻、視頻滴铅。
- 表與表之間沒有關(guān)聯(lián)戳葵,可擴(kuò)展性強(qiáng)。
- 保證數(shù)據(jù)的最終一致性汉匙。遵循 BASE(堿)理論拱烁。 Basically Available(基本可用)生蚁; Soft-state(軟狀態(tài)); Eventually Consistent(最終一致性)邻梆。
- 支持海量數(shù)據(jù)的存儲(chǔ)和高并發(fā)的高效讀寫守伸。
- 支持分布式,能夠?qū)?shù)據(jù)進(jìn)行分片存儲(chǔ)浦妄,擴(kuò)縮容簡(jiǎn)單尼摹。
對(duì)于不同的存儲(chǔ)類型,我們又有各種各樣的非關(guān)系型數(shù)據(jù)庫剂娄,比如有幾種常見的類型:
- KV 存 儲(chǔ) 蠢涝, 用 Key Value 的 形 式 來 存 儲(chǔ) 數(shù) 據(jù) 。 比 較 常 見 的 有 Redis 和
MemcacheDB阅懦。 - 文檔存儲(chǔ)和二,MongoDB。
- 列存儲(chǔ)耳胎,HBase惯吕。
- 圖存儲(chǔ),這個(gè)圖(Graph)是數(shù)據(jù)結(jié)構(gòu)怕午,不是文件格式废登。Neo4j。
- 對(duì)象存儲(chǔ)郁惜。
- XML 存儲(chǔ)等等等等堡距。
這個(gè)網(wǎng)頁列舉了各種各樣的 NoSQL 數(shù)據(jù)庫 http://nosql-database.org/ 。
NewSQL 結(jié)合了 SQL 和 NoSQL 的特性(例如 PingCAP 的 TiDB)兆蕉。
1.2.2.Redis 特性
官網(wǎng)介紹:https://redis.io/topics/introduction
中文網(wǎng)站: http://www.redis.cn
硬件層面有 CPU 的緩存羽戒;瀏覽器也有緩存;手機(jī)的應(yīng)用也有緩存虎韵。我們把數(shù)據(jù)緩存起來的原因就是從原始位置取數(shù)據(jù)的代價(jià)太大了易稠,放在一個(gè)臨時(shí)位置存儲(chǔ)起來,取回就可以快一些包蓝。
Redis 的特性:
- 更豐富的數(shù)據(jù)類型
- 進(jìn)程內(nèi)與跨進(jìn)程缩多;單機(jī)與分布式
- 功能豐富:持久化機(jī)制、過期策略
- 支持多種編程語言
- 高可用养晋,集群
1.3. Redis 安裝啟動(dòng)
1衬吆、Linux 安裝
參考:
CentOS7 安裝 Redis 單實(shí)例 https://gper.club/articles/7e7e7f7ff7g5egc4g6b
Docker 安裝 RabbitMQ 集群 https://gper.club/articles/7e7e7f7ff7g5egc5g6c
主要是注意配置文件幾處關(guān)鍵內(nèi)容( 后臺(tái)啟動(dòng)、 綁定 IP绳泉、 密碼) 的修改逊抡, 配置別名
2、Windows 服務(wù)端安裝
Redis 作者沒有為 Windows 編寫 Redis 服務(wù)端,微軟自行編寫了一個(gè) Redis 服務(wù)端冒嫡,可用于基本的測(cè)試和學(xué)習(xí)拇勃。https://github.com/MicrosoftArchive/redis/tags
安裝啟動(dòng)不詳解了,百度搜一大堆孝凌。
1.3.1.基本操作
默認(rèn)有 16 個(gè)庫(0-15)方咆,可以在配置文件中修改,默認(rèn)使用第一個(gè) db0蟀架。
databases 16
因?yàn)闆]有完全隔離瓣赂,不像數(shù)據(jù)庫的database,不適合把不同的庫分配給不同的業(yè)務(wù)使用片拍。
切換數(shù)據(jù)庫 select 0
清空當(dāng)前數(shù)據(jù)庫 flushdb
清空所有數(shù)據(jù)庫 flushall
Redis 是字典結(jié)構(gòu)的存儲(chǔ)方式煌集,采用 key-value 存儲(chǔ)。key 和 value 的最大長(zhǎng)度限制是 512M(來自官網(wǎng) https://redis.io/topics/data-types-intro/)捌省。
鍵的基本操作命令參考:http://redisdoc.com/index.html
存值 set cong 2673
取值 get cong
查看所有鍵 keys *
獲取所有鍵 dbsize
查看鍵是否存在 exists cong
刪除鍵 del cong
重命名鍵 rename cong congnew
查看類型 type cong
Redis 一共有幾種數(shù)據(jù)類型苫纤?(注意是數(shù)據(jù)類型不是數(shù)據(jù)結(jié)構(gòu))
官網(wǎng): https://redis.io/topics/data-types-intro
String、Hash纲缓、Set卷拘、List、Zset祝高、Hyperloglog栗弟、Geo、Streams
1.4. Redis 基本數(shù)據(jù)類型
最基本也是最常用的數(shù)據(jù)類型就是 String褂策。set 和 get 命令就是 String 的操作命令横腿。它是二進(jìn)制安全的(Binary-safe strings)颓屑,為啥斤寂?后面講
1.4.1String 字符串
存儲(chǔ)類型:
可以用來存儲(chǔ)字符串、整數(shù)揪惦、浮點(diǎn)數(shù)遍搞。
操作命令:
- 設(shè)置多個(gè)值(批量操作, 原子性)
mset cong 2673 jack 666
- 設(shè)置值器腋, 如果 key 存在溪猿, 則不成功
setnx cong value
基于此可實(shí)現(xiàn)分布式鎖。 用 del key 釋放鎖纫塌。但如果釋放鎖的操作失敗了诊县, 導(dǎo)致其他節(jié)點(diǎn)永遠(yuǎn)獲取不到鎖, 怎么辦措左?加過期時(shí)間依痊。 單獨(dú)用 expire 加過期, 也失敗了怎披, 無法保證原子性胸嘁, 怎么辦瓶摆? 多參數(shù)
set key value [expiration EX seconds|PX milliseconds][NX|XX]
使用參數(shù)的方式:set lock1 1 EX 10 NX,設(shè)置鍵lock的值為1,10秒過期時(shí)間性宏,前提是lock鍵不存在群井。 - (整數(shù)) 值遞增
incr cong
incrby cong100
- (整數(shù)) 值遞減
decr cong
decrby cong100
- 浮點(diǎn)數(shù)增量
set f 2.6
incrbyfloat f 7.3
- 獲取多個(gè)值
mget cong jack
- 獲取值長(zhǎng)度
strlen cong
- 字符串追加內(nèi)容
append cong good
- 獲取指定范圍的字符
getrange cong 0 8
1.5存儲(chǔ)( 實(shí)現(xiàn)) 原理
1.5.1數(shù)據(jù)模型
set hello word 為例,因?yàn)?Redis 是 KV 的數(shù)據(jù)庫毫胜,它是通過 hashtable 實(shí)現(xiàn)的(我們把這個(gè)叫做外層的哈希)书斜。所以每個(gè)鍵值對(duì)都會(huì)有一個(gè) dictEntry(源碼位置:dict.h),里面指向了 key 和 value 的指針指蚁。next 指向下一個(gè) dictEntry菩佑。
typedef struct dictEntry {
void *key; /* key 關(guān)鍵字定義 */
union {
void *val; uint64_t u64; /* value 定義 */
int64_t s64; double d;
} v;
struct dictEntry *next; /* 指向下一個(gè)鍵值對(duì)節(jié)點(diǎn) */
} dictEntry;
key 是字符串,但是 Redis 沒有直接使用 C 的字符數(shù)組凝化,而是存儲(chǔ)在自定義的 SDS中稍坯。
value 既不是直接作為字符串存儲(chǔ),也不是直接存儲(chǔ)在 SDS 中搓劫,而是存儲(chǔ)在redisObject 中瞧哟。實(shí)際上五種常用的數(shù)據(jù)類型的任何一種,都是通過 redisObject 來存儲(chǔ)的枪向。
redisObject
redisObject 定義在 src/server.h 文件中
typedef struct redisObject {
unsigned type:4; /* 對(duì)象的類型勤揩, 包括: OBJ_STRING、 OBJ_LIST秘蛔、 OBJ_HASH陨亡、 OBJ_SET、 OBJ_ZSET */
unsigned encoding:4; /* 具體的數(shù)據(jù)結(jié)構(gòu) */
unsigned lru:LRU_BITS; /* 24 位深员, 對(duì)象最后一次被命令程序訪問的時(shí)間负蠕, 與內(nèi)存回收有關(guān) */
int refcount; /* 引用計(jì)數(shù)。 當(dāng) refcount 為 0 的時(shí)候倦畅, 表示該對(duì)象已經(jīng)不被任何對(duì)象引用遮糖, 則可以進(jìn)行垃圾回收了*/
void *ptr; /* 指向?qū)ο髮?shí)際的數(shù)據(jù)結(jié)構(gòu) */
} robj;
可以使用 type 命令來查看對(duì)外的類型。
127.0.0.1:6379> type qs
string
1.5.2內(nèi)部編碼
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> set qs "is a good teacher in gupao, have crossed mountains and sea "
OK
127.0.0.1:6379> set jack bighead
OK
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> object encoding jack
"embstr"
127.0.0.1:6379> object encoding qs
"raw"
字符串類型的內(nèi)部編碼有三種:
- int叠赐,存儲(chǔ) 8 個(gè)字節(jié)的長(zhǎng)整型(long欲账,2^63-1)。
- embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 簡(jiǎn)單動(dòng)態(tài)字符串)芭概,
存儲(chǔ)小于 44 個(gè)字節(jié)的字符串赛不。 - raw,存儲(chǔ)大于 44 個(gè)字節(jié)的字符串(3.2 版本之前是 39 字節(jié))罢洲。為什么是 39踢故?
/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
問題 1、什么是 SDS?
Redis 中字符串的實(shí)現(xiàn)畴椰。
在 3.2 以后的版本中臊诊,SDS 又有多種結(jié)構(gòu)(sds.h):sdshdr5、sdshdr8斜脂、sdshdr16抓艳、sdshdr32、sdshdr64帚戳,用于存儲(chǔ)不同的長(zhǎng)度的字符串玷或,分別代表 2^5 =32byte,2^8 =256byte片任,2^16= 65536byte=64KB偏友,2^32byte=4GB。
/* sds.h */
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 當(dāng)前字符數(shù)組的長(zhǎng)度 */
uint8_t alloc; /*當(dāng)前字符數(shù)組總共分配的內(nèi)存大小 */
unsigned char flags; /* 當(dāng)前字符數(shù)組的屬性对供、 用來標(biāo)識(shí)到底是 sdshdr8 還是 sdshdr16 等 */
char buf[]; /* 字符串真正的值 */
};
問題 2位他、為什么 Redis 要用 SDS 實(shí)現(xiàn)字符串?
我們知道产场,C 語言本身沒有字符串類型(只能用字符數(shù)組 char[]實(shí)現(xiàn))鹅髓。
- 使用字符數(shù)組必須先給目標(biāo)變量分配足夠的空間皆愉,否則可能會(huì)溢出鳞贷。
- 如果要獲取字符長(zhǎng)度,必須遍歷字符數(shù)組剧蹂,時(shí)間復(fù)雜度是 O(n)确徙。
- C 字符串長(zhǎng)度的變更會(huì)對(duì)字符數(shù)組做內(nèi)存重分配醒串。
- 通過從字符串開始到結(jié)尾碰到的第一個(gè)'\0'來標(biāo)記字符串的結(jié)束,因此不能保存圖片鄙皇、音頻芜赌、視頻、壓縮文件等二進(jìn)制(bytes)保存的內(nèi)容育苟,二進(jìn)制不安全较鼓。
SDS 的特點(diǎn):
- 不用擔(dān)心內(nèi)存溢出問題椎木,如果需要會(huì)對(duì) SDS 進(jìn)行擴(kuò)容违柏。
- 獲取字符串長(zhǎng)度時(shí)間復(fù)雜度為 O(1),因?yàn)槎x了 len 屬性香椎。
- 通過“空間預(yù)分配”( sdsMakeRoomFor)和“惰性空間釋放”漱竖,防止多次重分配內(nèi)存。
- 判斷是否結(jié)束的標(biāo)志是 len 屬性(它同樣以'\0'結(jié)尾是因?yàn)檫@樣就可以使用 C語言中函數(shù)庫操作字符串的函數(shù)了)畜伐,可以包含'\0'馍惹。
存儲(chǔ)二進(jìn)制: BytesTest.java
C 字符串 | SDS |
---|---|
獲取字符串長(zhǎng)度的復(fù)雜度為 O(N) | 獲取字符串長(zhǎng)度的復(fù)雜度為 O(1) |
API 是不安全的,可能會(huì)造成緩沖區(qū)溢出 | API 是安全的, 不會(huì)早晨個(gè)緩沖區(qū)溢出 |
修改字符串長(zhǎng)度 N 次必然需要執(zhí)行 N 次內(nèi)存重分配 | 修改字符串長(zhǎng)度 N 次最多需要執(zhí)行 N 次內(nèi)存重分配 |
只能保存文本數(shù)據(jù) | 可以保存文本或者二進(jìn)制數(shù)據(jù) |
可以使用所有<string.h>庫中的函數(shù) | 可以使用一部分<string.h>庫中的函數(shù) |
問題 3万矾、embstr 和 raw 的區(qū)別悼吱?
embstr 的使用只分配一次內(nèi)存空間(因?yàn)?RedisObject 和 SDS 是連續(xù)的),而 raw需要分配兩次內(nèi)存空間(分別為 RedisObject 和 SDS 分配空間)良狈。
因此與 raw 相比后添,embstr 的好處在于創(chuàng)建時(shí)少分配一次空間,刪除時(shí)少釋放一次空間薪丁,以及對(duì)象的所有數(shù)據(jù)連在一起遇西,尋找方便。
而 embstr 的壞處也很明顯严嗜,如果字符串的長(zhǎng)度增加需要重新分配內(nèi)存時(shí)粱檀,整個(gè)RedisObject 和 SDS 都需要重新分配空間,因此 Redis 中的 embstr 實(shí)現(xiàn)為只讀漫玄。
問題 4:int 和 embstr 什么時(shí)候轉(zhuǎn)化為 raw茄蚯?
當(dāng)int數(shù)據(jù)不再是整數(shù) ,或大小超過了long的范圍(2^63-1=9223372036854775807)時(shí)睦优,自動(dòng)轉(zhuǎn)化為 embstr第队。
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> append k1 a
(integer) 2
127.0.0.1:6379> object encoding k1
"raw"
問題 5:明明沒有超過閾值,為什么變成 raw 了刨秆?
127.0.0.1:6379> set k2 a
OK
127.0.0.1:6379> object encoding k2
"embstr"
127.0.0.1:6379> append k2 b
(integer) 2
127.0.0.1:6379> object encoding k2
"raw"
對(duì)于 embstr凳谦,由于其實(shí)現(xiàn)是只讀的,因此在對(duì) embstr 對(duì)象進(jìn)行修改時(shí)衡未,都會(huì)先轉(zhuǎn)化為 raw 再進(jìn)行修改尸执。因此,只要是修改 embstr 對(duì)象缓醋,修改后的對(duì)象一定是 raw 的如失,無論是否達(dá)到了 44個(gè)字節(jié)。
問題 6:當(dāng)長(zhǎng)度小于閾值時(shí)送粱,會(huì)還原嗎褪贵?
關(guān)于 Redis 內(nèi)部編碼的轉(zhuǎn)換,都符合以下規(guī)律:編碼轉(zhuǎn)換在 Redis 寫入數(shù)據(jù)時(shí)完成抗俄,且轉(zhuǎn)換過程不可逆脆丁,只能從小內(nèi)存編碼向大內(nèi)存編碼轉(zhuǎn)換(但是不包括重新 set)。
問題 7:為什么要對(duì)底層的數(shù)據(jù)結(jié)構(gòu)進(jìn)行一層包裝呢动雹?
通過封裝槽卫,可以根據(jù)對(duì)象的類型動(dòng)態(tài)地選擇存儲(chǔ)結(jié)構(gòu)和可以使用的命令,實(shí)現(xiàn)節(jié)省空間和優(yōu)化查詢速度胰蝠。
1.5.3應(yīng)用場(chǎng)景
-
緩存
String 類型
例如:熱點(diǎn)數(shù)據(jù)緩存(例如報(bào)表歼培,明星出軌)震蒋,對(duì)象緩存,全頁緩存躲庄〔槠剩可以提升熱點(diǎn)數(shù)據(jù)的訪問速度。 -
數(shù)據(jù)共享分布式
STRING 類型噪窘,因?yàn)?Redis 是分布式的獨(dú)立服務(wù)梗搅,可以在多個(gè)應(yīng)用之間共享。
例如:分布式 Session
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
-
分布式鎖
STRING 類型 setnx 方法效览,只有不存在時(shí)才能添加成功无切,返回 true。
http://redisdoc.com/string/set.html 建議用參數(shù)的形式丐枉,上文講過
public Boolean getLock(Object lockObject){
jedisUtil = getJedisConnetion();
boolean flag = jedisUtil.setNX(lockObj, 1);
if(flag){
expire(locakObj,10);
}r
return flag;
}
?public void releaseLock(Object lockObject){
del(lockObj);
}
-
全局 ID
INT 類型哆键,INCRBY,利用原子性
incrby userid 1000
(分庫分表的場(chǎng)景瘦锹,一次性拿一段)
計(jì)數(shù)器
INT 類型籍嘹,INCR 方法
例如:文章的閱讀量,微博點(diǎn)贊數(shù)弯院,允許一定的延遲辱士,先寫入 Redis 再定時(shí)同步到數(shù)據(jù)庫。限流
INT 類型听绳,INCR 方法
以訪問者的 IP 和其他信息作為 key颂碘,訪問一次增加一次計(jì)數(shù),超過次數(shù)則返回 false椅挣。位統(tǒng)計(jì)
String 類型的 BITCOUNT(1.6.6 的 bitmap 數(shù)據(jù)結(jié)構(gòu)介紹)头岔。
字符是以 8 位二進(jìn)制存儲(chǔ)的。
set k1 a
setbit k1 6 1
setbit k1 7 0
get k1
a 對(duì)應(yīng)的 ASCII 碼是 97鼠证,轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)是 01100001
b 對(duì)應(yīng)的 ASCII 碼是 98峡竣,轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)是 01100010
因?yàn)?bit 非常節(jié)省空間(1 MB=8388608 bit),可以用來做大數(shù)據(jù)量的統(tǒng)計(jì)量九。
例如:在線用戶統(tǒng)計(jì)适掰,留存用戶統(tǒng)計(jì)
setbit onlineusers 0 1
setbit onlineusers 1 1
setbit onlineusers 2 0
支持按位與、按位或等等操作荠列。
BITOP AND destkey key [key ...] 类浪, 對(duì)一個(gè)或多個(gè) key 求邏輯并, 并將結(jié)果保存到 destkey 弯予。
BITOP OR destkey key [key ...] 戚宦, 對(duì)一個(gè)或多個(gè) key 求邏輯或个曙, 并將結(jié)果保存到 destkey 锈嫩。
BITOP XOR destkey key [key ...] 受楼, 對(duì)一個(gè)或多個(gè) key 求邏輯異或, 并將結(jié)果保存到 destkey 呼寸。
BITOP NOT destkey key 艳汽, 對(duì)給定 key 求邏輯非, 并將結(jié)果保存到 destkey 对雪。
計(jì)算出 7 天都在線的用戶
BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
如果一個(gè)對(duì)象的 value 有多個(gè)值的時(shí)候河狐,怎么存儲(chǔ)?
例如用一個(gè) key 存儲(chǔ)一張表的數(shù)據(jù)瑟捣。
序列化馋艺?例如 JSON/Protobuf/XML,會(huì)增加序列化和反序列化的開銷迈套,并且不能單獨(dú)獲取捐祠、修改一個(gè)值。
可以通過 key 分層的方式來實(shí)現(xiàn)桑李,例如:
mset student:1:sno GP16666 student:1:sname 沐風(fēng) student:1:company 騰訊
獲取值的時(shí)候一次獲取多個(gè)值:
mget student:1:sno student:1:sname student:1:company
缺點(diǎn):key 太長(zhǎng)踱蛀,占用的空間太多。有沒有更好的方式贵白?HASH!!!
1.6 Hash哈希
存儲(chǔ)類型
包含鍵值對(duì)的無序散列表率拒。value 只能是字符串,不能嵌套其他類型禁荒。
同樣是存儲(chǔ)字符串猬膨,Hash 與 String 的主要區(qū)別?
- 把所有相關(guān)的值聚集到一個(gè) key 中呛伴,節(jié)省內(nèi)存空間
- 只使用一個(gè) key寥掐,減少 key 沖突
- 當(dāng)需要批量獲取值的時(shí)候,只需要使用一個(gè)命令磷蜀,減少內(nèi)存/IO/CPU 的消耗
Hash 不適合的場(chǎng)景:
- Field 不能單獨(dú)設(shè)置過期時(shí)間
- 沒有 bit 操作
- 需要考慮數(shù)據(jù)量分布的問題(value 值非常大的時(shí)候召耘,無法分布到多個(gè)節(jié)點(diǎn))
操作命令
hset h1 f 6
hset h1 e 5
hmset h1 a 1 b 2 c 3 d 4
hget h1 a
hmget h1 a b c d
hkeys h1
hvals h1
hgetall h1
key 操作
hget exists h1
hdel h1
hlen h1
存儲(chǔ)(實(shí)現(xiàn))原理
Redis 的 Hash 本身也是一個(gè) KV 的結(jié)構(gòu),類似于 Java 中的 HashMap褐隆。外層的哈希(Redis KV 的實(shí)現(xiàn))只用到了 hashtable污它。當(dāng)存儲(chǔ) hash 數(shù)據(jù)類型時(shí),我們把它叫做內(nèi)層的哈希庶弃。內(nèi)層的哈希底層可以使用兩種數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn):
ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)
hashtable:OBJ_ENCODING_HT(哈希表)
ziplist 壓縮列表
ziplist 壓縮列表是什么衫贬?
/* ziplist.c 源碼頭部注釋 */
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and
integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop
operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory
used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.
ziplist 是一個(gè)經(jīng)過特殊編碼的雙向鏈表,它不存儲(chǔ)指向上一個(gè)鏈表節(jié)點(diǎn)和指向下一個(gè)鏈表節(jié)點(diǎn)的指針歇攻,而是存儲(chǔ)上一個(gè)節(jié)點(diǎn)長(zhǎng)度和當(dāng)前節(jié)點(diǎn)長(zhǎng)度固惯,通過犧牲部分讀寫性能,來換取高效的內(nèi)存空間利用率缴守,是一種時(shí)間換空間的思想葬毫。只用在字段個(gè)數(shù)少镇辉,字段值小的場(chǎng)景里面。
ziplist 的內(nèi)部結(jié)構(gòu)贴捡?
ziplist.c 源碼第 16 行的注釋:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
typedef struct zlentry {
unsigned int prevrawlensize; /* 上一個(gè)鏈表節(jié)點(diǎn)占用的長(zhǎng)度 */
unsigned int prevrawlen; /* 存儲(chǔ)上一個(gè)鏈表節(jié)點(diǎn)的長(zhǎng)度數(shù)值所需要的字節(jié)數(shù) */
unsigned int lensize; /* 存儲(chǔ)當(dāng)前鏈表節(jié)點(diǎn)長(zhǎng)度數(shù)值所需要的字節(jié)數(shù) */
unsigned int len; /* 當(dāng)前鏈表節(jié)點(diǎn)占用的長(zhǎng)度 */
unsigned int headersize; /* 當(dāng)前鏈表節(jié)點(diǎn)的頭部大泻龈亍(prevrawlensize + lensize) , 即非數(shù)據(jù)域的大小 */
unsigned char encoding; /* 編碼方式 */
unsigned char *p; /* 壓縮鏈表以字符串的形式保存烂斋, 該指針指向當(dāng)前節(jié)點(diǎn)起始位置 */
} zlentry;
編碼 encoding(ziplist.c 源碼第 204 行)
#define ZIP_STR_06B (0 << 6) //長(zhǎng)度小于等于 63 字節(jié)
#define ZIP_STR_14B (1 << 6) //長(zhǎng)度小于等于 16383 字節(jié)
#define ZIP_STR_32B (2 << 6) //長(zhǎng)度小于等于 4294967295 字節(jié)
問題:什么時(shí)候使用 ziplist 存儲(chǔ)屹逛?
當(dāng) hash 對(duì)象同時(shí)滿足以下兩個(gè)條件的時(shí)候,使用 ziplist 編碼:
1)所有的鍵值對(duì)的健和值的字符串長(zhǎng)度都小于等于 64byte(一個(gè)英文字母
一個(gè)字節(jié))汛骂;
2)哈希對(duì)象保存的鍵值對(duì)數(shù)量小于 512 個(gè)罕模。
/* src/redis.conf 配置 */
hash-max-ziplist-value 64 // ziplist 中最大能存放的值長(zhǎng)度
hash-max-ziplist-entries 512 // ziplist 中最多能存放的 entry 節(jié)點(diǎn)數(shù)量
/* 源碼位置: t_hash.c , 當(dāng)達(dá)字段個(gè)數(shù)超過閾值帘瞭, 使用 HT 作為編碼 */
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
/*源碼位置: t_hash.c手销, 當(dāng)字段值長(zhǎng)度過大, 轉(zhuǎn)為 HT */
for (i = start; i <= end; i++) {
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
}
}
一個(gè)哈希對(duì)象超過配置的閾值(鍵和值的長(zhǎng)度有>64byte图张,鍵值對(duì)個(gè)數(shù)>512 個(gè))時(shí)锋拖,會(huì)轉(zhuǎn)換成哈希表(hashtable)。
hashtable(dict)
在Redis中祸轮,hashtable被稱為字典(dictionary)兽埃,它是一個(gè)數(shù)組+鏈表的結(jié)構(gòu)。
源碼位置: dict.h
前面我們知道了适袜,Redis 的 KV 結(jié)構(gòu)是通過一個(gè) dictEntry 來實(shí)現(xiàn)的柄错。Redis 又對(duì) dictEntry 進(jìn)行了多層的封裝。
typedef struct dictEntry {
void *key; /* key 關(guān)鍵字定義 */
union {
void *val; uint64_t u64; /* value 定義 */
int64_t s64; double d;
} v;
struct dictEntry *next; /* 指向下一個(gè)鍵值對(duì)節(jié)點(diǎn) */
} dictEntry;
dictEntry 放到了 dictht(hashtable 里面):
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table; /* 哈希表數(shù)組 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 掩碼大小苦酱, 用于計(jì)算索引值售貌。 總是等于 size-1 */
unsigned long used; /* 已有節(jié)點(diǎn)數(shù) */
} dictht;
ht 放到了 dict 里面:
typedef struct dict {
dictType *type; /* 字典類型 */
void *privdata; /* 私有數(shù)據(jù) */
dictht ht[2]; /* 一個(gè)字典有兩個(gè)哈希表 */
long rehashidx; /* rehash 索引 */
unsigned long iterators; /* 當(dāng)前正在使用的迭代器數(shù)量 */
} dict;
從最底層到最高層 dictEntry——dictht——dict——OBJ_ENCODING_HT
總結(jié):哈希的存儲(chǔ)結(jié)構(gòu)
注意: dictht 后面是 NULL 說明第二個(gè) ht 還沒用到。 dictEntry*后面是 NULL 說明沒有 hash 到這個(gè)地址疫萤。 dictEntry 后面是 NULL 說明沒有發(fā)生哈希沖突颂跨。
問題:為什么要定義兩個(gè)哈希表呢?ht[2]
redis 的 hash 默認(rèn)使用的是 ht[0]扯饶,ht[1]不會(huì)初始化和分配空間恒削。哈希表 dictht 是用鏈地址法來解決碰撞問題的。在這種情況下尾序,哈希表的性能取決于它的大械龇帷(size 屬性)和它所保存的節(jié)點(diǎn)的數(shù)量(used 屬性)之間的比率:
- 比率在 1:1 時(shí)(一個(gè)哈希表 ht 只存儲(chǔ)一個(gè)節(jié)點(diǎn) entry),哈希表的性能最好每币;
- 如果節(jié)點(diǎn)數(shù)量比哈希表的大小要大很多的話(這個(gè)比例用 ratio 表示携丁,5 表示平均一個(gè) ht 存儲(chǔ) 5 個(gè) entry),那么哈希表就會(huì)退化成多個(gè)鏈表兰怠,哈希表本身的性能
優(yōu)勢(shì)就不再存在梦鉴。
在這種情況下需要擴(kuò)容李茫。Redis 里面的這種操作叫做 rehash。
rehash 的步驟:
- 為字符 ht[1]哈希表分配空間尚揣,這個(gè)哈希表的空間大小取決于要執(zhí)行的操作涌矢,以及 ht[0]當(dāng)前包含的鍵值對(duì)的數(shù)量掖举。
擴(kuò)展:ht[1]的大小為第一個(gè)大于等于 ht[0].used*2快骗。 - 將所有的 ht[0]上的節(jié)點(diǎn) rehash 到 ht[1]上,重新計(jì)算 hash 值和索引塔次,然后放入指定的位置方篮。
- 當(dāng) ht[0]全部遷移到了 ht[1]之后,釋放 ht[0]的空間励负,將 ht[1]設(shè)置為 ht[0]表藕溅,并創(chuàng)建新的 ht[1],為下次 rehash 做準(zhǔn)備继榆。
問題:什么時(shí)候觸發(fā)擴(kuò)容巾表?
負(fù)載因子(源碼位置: dict.c):
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
ratio = used / size,已使用節(jié)點(diǎn)與字典大小的比例
dict_can_resize 為 1 并且 dict_force_resize_ratio 已使用節(jié)點(diǎn)數(shù)和字典大小之間的比率超過 1:5略吨,觸發(fā)擴(kuò)容
擴(kuò)容判斷 _dictExpandIfNeeded(源碼 dict.c)
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
縮容:server.c
int htNeedsResize(dict *dict) {
long long size, used;
?
size = dictSlots(dict);
used = dictSize(dict);
return (size > DICT_HT_INITIAL_SIZE &&
(used*100/size < HASHTABLE_MIN_FILL));
}
應(yīng)用場(chǎng)景
String可以做的事情集币,Hash都可以做。
存儲(chǔ)對(duì)象類型的數(shù)據(jù)
比如對(duì)象或者一張表的數(shù)據(jù)翠忠,比 String 節(jié)省了更多 key 的空間鞠苟,也更加便于集中管理。-
購物車
key:用戶 id秽之;field:商品 id当娱;value:商品數(shù)量。
+1:hincr考榨。-1:hdecr跨细。刪除:hdel。全選:hgetall河质。商品數(shù):hlen扼鞋。
1.7 List 列表
存儲(chǔ)類型
存儲(chǔ)有序的字符串(從左到右),元素可以重復(fù)愤诱≡仆罚可以充當(dāng)隊(duì)列和棧的角色。
操作命令
元素增減:
lpush queue a
lpush queue b c
rpush queue d e
lpop queue
rpop queue
?blpop queue
brpop queue
取值
lindex queue 0
lrange queue 0 -1
存儲(chǔ)(實(shí)現(xiàn))原理
在早期的版本中淫半,數(shù)據(jù)量較小時(shí)用 ziplist 存儲(chǔ)溃槐,達(dá)到臨界值時(shí)轉(zhuǎn)換為 linkedlist 進(jìn)行存儲(chǔ),分別對(duì)應(yīng) OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST 科吭。
3.2 版本之后昏滴,統(tǒng)一用 quicklist 來存儲(chǔ)猴鲫。quicklist 存儲(chǔ)了一個(gè)雙向鏈表,每個(gè)節(jié)點(diǎn)都是一個(gè) ziplist谣殊。
127.0.0.1:6379> object encoding queue
"quicklist"
quicklist
quicklist(快速列表)是 ziplist 和 linkedlist 的結(jié)合體拂共。
quicklist.h,head 和 tail 指向雙向列表的表頭和表尾姻几。
typedef struct quicklist {
quicklistNode *head; /* 指向雙向列表的表頭 */
quicklistNode *tail; /* 指向雙向列表的表尾 */
unsigned long count; /* 所有的 ziplist 中一共存了多少個(gè)元素 */
unsigned long len; /* 雙向鏈表的長(zhǎng)度宜狐, node 的數(shù)量 */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* 壓縮深度, 0: 不壓縮蛇捌; */
} quicklist;
redis.conf 相關(guān)參數(shù):
list-max-ziplist-size(fill):正數(shù)表示單個(gè) ziplist 最多所包含的 entry 個(gè)數(shù);負(fù)數(shù)代表單個(gè) ziplist 的大小抚恒, 默認(rèn) 8k; -1: 4KB; -2: 8KB络拌; -3: 16KB俭驮; -4: 32KB; -5: 64KB
list-compress-depth(compress):壓縮深度春贸, 默認(rèn)是 0;1: 首尾的 ziplist 不壓縮混萝; 2: 首尾第一第二個(gè) ziplist 不壓縮, 以此類推
quicklistNode 中的*zl 指向一個(gè) ziplist萍恕,一個(gè) ziplist 可以存放多個(gè)元素逸嘀。
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一個(gè)節(jié)點(diǎn) */
struct quicklistNode *next; /* 后一個(gè)節(jié)點(diǎn) */
unsigned char *zl; /* 指向?qū)嶋H的 ziplist */
unsigned int sz; /* 當(dāng)前 ziplist 占用多少字節(jié) */
unsigned int count : 16; /* 當(dāng)前 ziplist 中存儲(chǔ)了多少個(gè)元素, 占 16bit(下同) 雄坪, 最大 65536 個(gè) */
unsigned int encoding : 2; /* 是否采用了 LZF 壓縮算法壓縮節(jié)點(diǎn)厘熟, 1: RAW 2: LZF */
unsigned int container : 2; /* 2: ziplist, 未來可能支持其他結(jié)構(gòu)存儲(chǔ) */
unsigned int recompress : 1; /* 當(dāng)前 ziplist 是不是已經(jīng)被解壓出來作臨時(shí)使用 */
unsigned int attempted_compress : 1; /* 測(cè)試用 */
unsigned int extra : 10; /* 預(yù)留給未來使用 */
} quicklistNode;
ziplist 的結(jié)構(gòu)前面已經(jīng)說過了维哈,不再重復(fù)绳姨。
應(yīng)用場(chǎng)景
-
用戶消息時(shí)間線 timeline
因?yàn)?List 是有序的,可以用來做用戶時(shí)間線
消息隊(duì)列
List 提供了兩個(gè)阻塞的彈出操作:BLPOP/BRPOP阔挠,可以設(shè)置超時(shí)時(shí)間飘庄。
BLPOP:BLPOP key1 timeout 移出并獲取列表的第一個(gè)元素, 如果列表沒有元素會(huì)阻塞列表直到等待超時(shí)或發(fā)現(xiàn)可彈出元素為止购撼。
BRPOP:BRPOP key1 timeout 移出并獲取列表的最后一個(gè)元素跪削, 如果列表沒有元素會(huì)阻塞列表直到等待超時(shí)或發(fā)現(xiàn)可彈出元素為止。
隊(duì)列:先進(jìn)先出:rpush blpop迂求,左頭右尾碾盐,右邊進(jìn)入隊(duì)列,左邊出隊(duì)列揩局。
棧:先進(jìn)后出:rpush brpop
1.8 set集合
存儲(chǔ)類型
String 類型的無序集合毫玖,最大存儲(chǔ)數(shù)量 2^32-1(40 億左右)。
操作命令
添加一個(gè)或者多個(gè)元素
sadd myset a b c d e f g
獲取所有元素
smembers myset
統(tǒng)計(jì)元素個(gè)數(shù)
scard myset
隨機(jī)獲取一個(gè)元素
srandmember key
隨機(jī)彈出一個(gè)元素
spop myset
移除一個(gè)或者多個(gè)元素
srem myset d e f
查看元素是否存在
sismember myset a
存儲(chǔ)(實(shí)現(xiàn))原理
Redis 用 intset 或 hashtable 存儲(chǔ) set。如果元素都是整數(shù)類型付枫,就用 inset 存儲(chǔ)烹玉。如果不是整數(shù)類型,就用 hashtable(數(shù)組+鏈表的存來儲(chǔ)結(jié)構(gòu))阐滩。
問題:KV 怎么存儲(chǔ) set 的元素二打?key 就是元素的值,value 為 null掂榔。
如果元素個(gè)數(shù)超過 512 個(gè)继效,也會(huì)用 hashtable 存儲(chǔ)。
配置文件 redis.conf
set-max-intset-entries 512
127.0.0.1:6379> sadd iset 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> object encoding iset
"intset"
127.0.0.1:6379> sadd myset a b c d e f
(integer) 6
127.0.0.1:6379> object encoding myset
"hashtable"
應(yīng)用場(chǎng)景
- 抽獎(jiǎng)
隨機(jī)獲取元素
spop myset -
點(diǎn)贊衅疙、 簽到莲趣、 打卡
這條微博的 ID 是 t1001鸳慈,用戶 ID 是 u3001饱溢。
用 like:t1001 來維護(hù) t1001 這條微博的所有點(diǎn)贊用戶。
點(diǎn)贊了這條微博:sadd like:t1001 u3001
取消點(diǎn)贊:srem like:t1001 u3001
是否點(diǎn)贊:sismember like:t1001 u3001
點(diǎn)贊的所有用戶:smembers like:t1001
點(diǎn)贊數(shù):scard like:t1001
比關(guān)系型數(shù)據(jù)庫簡(jiǎn)單許多走芋。
-
商品標(biāo)簽
用tags:i5001 來維護(hù)商品所有的標(biāo)簽绩郎。
sadd tags:i5001 畫面清晰細(xì)膩
sadd tags:i5001 真彩清晰顯示屏
sadd tags:i5001 流暢至極
- 商品篩選
獲取差集
sdiff set1 set2
獲取交集(intersection )
sinter set1 set2
獲取并集
sunion set1 set2
iPhone11 上市了。
sadd brand:apple iPhone11
sadd brand:ios iPhone11
sad screensize:6.0-6.24 iPhone11
sad screentype:lcd iPhone11
篩選商品翁逞,蘋果的肋杖,iOS 的,屏幕在 6.0-6.24 之間的挖函,屏幕材質(zhì)是 LCD 屏幕
sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd
用戶關(guān)注状植、 推薦模型
思考
1)相互關(guān)注?
2)我關(guān)注的人也關(guān)注了他怨喘?
3)可能認(rèn)識(shí)的人津畸?
1.9ZSet 有序集合
存儲(chǔ)類型
sorted set,有序的 set必怜,每個(gè)元素有個(gè) score肉拓。
score 相同時(shí),按照 key 的 ASCII 碼排序梳庆。
數(shù)據(jù)結(jié)構(gòu)對(duì)比:
操作命令
添加元素
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
獲取全部元素
zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 withscores
根據(jù)分值區(qū)間獲取元素
zrangebyscore myzset 20 30
移除元素
也可以根據(jù) score rank 刪除
zrem myzset php cpp
統(tǒng)計(jì)元素個(gè)數(shù)
zcard myzset
分值遞增
zincrby myzset 5 python
根據(jù)分值統(tǒng)計(jì)個(gè)數(shù)
zcount myzset 20 60
獲取元素 rank
zrank myzset java
獲取元素 score
zsocre myzset java
也有倒序的 rev 操作(reverse)
存儲(chǔ)(實(shí)現(xiàn))原理
同時(shí)滿足以下條件時(shí)使用 ziplist 編碼:
- 元素?cái)?shù)量小于 128 個(gè)
- 所有 member 的長(zhǎng)度都小于 64 字節(jié)
在 ziplist 的內(nèi)部暖途,按照 score 排序遞增來存儲(chǔ)。插入的時(shí)候要移動(dòng)之后的數(shù)據(jù)膏执。
對(duì)應(yīng) redis.conf 參數(shù):
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
超過閾值之后驻售,使用 skiplist+dict 存儲(chǔ)。
問題:什么是 skiplist更米?
我們先來看一下有序鏈表:
在這樣一個(gè)鏈表中欺栗,如果我們要查找某個(gè)數(shù)據(jù),那么需要從頭開始逐個(gè)進(jìn)行比較,直到找到包含數(shù)據(jù)的那個(gè)節(jié)點(diǎn)纸巷,或者找到第一個(gè)比給定數(shù)據(jù)大的節(jié)點(diǎn)為止(沒找到)镇草。
也就是說,時(shí)間復(fù)雜度為 O(n)瘤旨。同樣梯啤,當(dāng)我們要插入新數(shù)據(jù)的時(shí)候,也要經(jīng)歷同樣的查找過程存哲,從而確定插入位置因宇。
而二分查找法只適用于有序數(shù)組,不適用于鏈表祟偷。
假如我們每相鄰兩個(gè)節(jié)點(diǎn)增加一個(gè)指針(或者理解為有三個(gè)元素進(jìn)入了第二層)察滑,讓指針指向下下個(gè)節(jié)點(diǎn)。
這樣所有新增加的指針連成了一個(gè)新的鏈表修肠,但它包含的節(jié)點(diǎn)個(gè)數(shù)只有原來的一半(上圖中是 7, 19, 26)贺辰。在插入一個(gè)數(shù)據(jù)的時(shí)候,決定要放到那一層嵌施,取決于一個(gè)算法(在 redis 中 t_zset.c 有一個(gè) zslRandomLevel 這個(gè)方法)饲化。
現(xiàn)在當(dāng)我們想查找數(shù)據(jù)的時(shí)候,可以先沿著這個(gè)新鏈表進(jìn)行查找吗伤。當(dāng)碰到比待查數(shù)據(jù)大的節(jié)點(diǎn)時(shí)吃靠,再回到原來的鏈表中的下一層進(jìn)行查找。比如足淆,我們想查找 23巢块,查找的路徑是沿著下圖中標(biāo)紅的指針?biāo)赶虻姆较蜻M(jìn)行的:
- 23 首先和 7 比較,再和 19 比較巧号,比它們都大族奢,繼續(xù)向后比較。
- 但 23 和 26 比較的時(shí)候裂逐,比 26 要小歹鱼,因此回到下面的鏈表(原鏈表),與 22比較卜高。
- 23 比 22 要大弥姻,沿下面的指針繼續(xù)向后和 26 比較。23 比 26 小掺涛,說明待查數(shù)據(jù) 23 在原鏈表中不存在
在這個(gè)查找過程中庭敦,由于新增加的指針,我們不再需要與鏈表中每個(gè)節(jié)點(diǎn)逐個(gè)進(jìn)行比較了薪缆。需要比較的節(jié)點(diǎn)數(shù)大概只有原來的一半秧廉。這就是跳躍表伞广。
為什么不用 AVL 樹或者紅黑樹?因?yàn)?skiplist 更加簡(jiǎn)潔疼电。
源碼:server.h
typedef struct zskiplistNode {
sds ele; /* zset 的元素 */
double score; /* 分值 */
struct zskiplistNode *backward; /* 后退指針 */
struct zskiplistLevel {
struct zskiplistNode *forward; /* 前進(jìn)指針嚼锄, 對(duì)應(yīng) level 的下一個(gè)節(jié)點(diǎn) */
unsigned long span; /* 從當(dāng)前節(jié)點(diǎn)到下一個(gè)節(jié)點(diǎn)的跨度(跨越的節(jié)點(diǎn)數(shù)) */
} level[]; /* 層 */
} zskiplistNode;
?
typedef struct zskiplist {
struct zskiplistNode *header, *tail; /* 指向跳躍表的頭結(jié)點(diǎn)和尾節(jié)點(diǎn) */
unsigned long length; /* 跳躍表的節(jié)點(diǎn)數(shù) */
int level; /* 最大的層數(shù) */
} zskiplist;
?
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
隨機(jī)獲取層數(shù)的函數(shù):
源碼:t_zset.c
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
應(yīng)用場(chǎng)景
-
排行榜
id 為 6001 的新聞點(diǎn)擊數(shù)加 1:zincrby hotNews:20190926 1 n6001
獲取今天點(diǎn)擊最多的 15 條:zrevrange hotNews:20190926 0 15 withscores
其他數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)介
BitMaps
Bitmaps 是在字符串類型上面定義的位操作。一個(gè)字節(jié)由 8 個(gè)二進(jìn)制位組成蔽豺。
set k1 a
獲取 value 在 offset 處的值(a 對(duì)應(yīng)的 ASCII 碼是 97区丑, 轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)是 01100001)
getbit k1 0
修改二進(jìn)制數(shù)據(jù)(b 對(duì)應(yīng)的 ASCII 碼是 98, 轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)是 01100010)
setbit k1 6 1
setbit k1 7 0
get k1
統(tǒng)計(jì)二進(jìn)制位中 1 的個(gè)數(shù)
bitcount k1
獲取第一個(gè) 1 或者 0 的位置
bitpos k1 1
bitpos k1 0
BITOP 命令支持 AND 修陡、 OR 沧侥、 NOT 、 XOR 這四種操作中的任意一種參數(shù):
BITOP AND destkey srckey1 … srckeyN 魄鸦, 對(duì)一個(gè)或多個(gè) key 求邏輯與宴杀, 并將結(jié)果保存到 destkey
BITOP OR destkey srckey1 … srckeyN, 對(duì)一個(gè)或多個(gè) key 求邏輯或拾因, 并將結(jié)果保存到 destkey
BITOP XOR destkey srckey1 … srckeyN旺罢, 對(duì)一個(gè)或多個(gè) key 求邏輯異或, 并將結(jié)果保存到 destkey
BITOP NOT destkey srckey盾致, 對(duì)給定 key 求邏輯非主经, 并將結(jié)果保存到 destkey
應(yīng)用場(chǎng)景:
用戶訪問統(tǒng)計(jì)
在線用戶統(tǒng)計(jì)
Hyperloglogs
Hyperloglogs:提供了一種不太準(zhǔn)確的基數(shù)統(tǒng)計(jì)方法荣暮,比如統(tǒng)計(jì)網(wǎng)站的 UV庭惜,存在一定的誤差。
Streams
5.0 推出的數(shù)據(jù)類型穗酥。支持多播的可持久化的消息隊(duì)列护赊,用于實(shí)現(xiàn)發(fā)布訂閱功能,借鑒了 kafka 的設(shè)計(jì)砾跃。
總結(jié)
數(shù)據(jù)結(jié)構(gòu)總結(jié)
編碼轉(zhuǎn)換總結(jié)
應(yīng)用場(chǎng)景總結(jié)
- 緩存——提升熱點(diǎn)數(shù)據(jù)的訪問速度
- 共享數(shù)據(jù)——數(shù)據(jù)的存儲(chǔ)和共享的問題
- 全局 ID —— 分布式全局 ID 的生成方案(分庫分表)
- 分布式鎖——進(jìn)程間共享數(shù)據(jù)的原子操作保證
- 在線用戶統(tǒng)計(jì)和計(jì)數(shù)
- 隊(duì)列骏啰、 棧——跨進(jìn)程的隊(duì)列/棧
- 消息隊(duì)列——異步解耦的消息機(jī)制
- 服務(wù)注冊(cè)與發(fā)現(xiàn) —— RPC 通信機(jī)制的服務(wù)協(xié)調(diào)中心(Dubbo 支持 Redis)
- 購物車
- 新浪/Twitter 用戶消息時(shí)間線
- 抽獎(jiǎng)邏輯(禮物抽高、 轉(zhuǎn)發(fā))
- 點(diǎn)贊判耕、 簽到、 打卡
- 商品標(biāo)簽
- 用戶(商品) 關(guān)注(推薦) 模型
- 電商產(chǎn)品篩選
- 排行榜