Redis入墳(一)redis的前世今生央拖、redis基礎(chǔ)及存儲(chǔ)結(jié)構(gòu)源碼講解

本篇博文目標(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):

  1. 它以表格的形式,基于行存儲(chǔ)數(shù)據(jù)糠聪,是一個(gè)二維的模式荒椭。
  2. 它存儲(chǔ)的是結(jié)構(gòu)化的數(shù)據(jù),數(shù)據(jù)存儲(chǔ)有固定的模式(schema)舰蟆,數(shù)據(jù)需要適應(yīng)表結(jié)構(gòu)趣惠。
  3. 表與表之間存在關(guān)聯(lián)(Relationship)。
  4. 大部分關(guān)系型數(shù)據(jù)庫都支持 SQL(結(jié)構(gòu)化查詢語言)的操作身害,支持復(fù)雜的關(guān)聯(lián)查詢味悄。
  5. 通過支持事務(wù)(ACID 酸)來提供嚴(yán)格或者實(shí)時(shí)的數(shù)據(jù)一致性。

但是使用關(guān)系型數(shù)據(jù)庫也存在一些限制塌鸯,比如:

  1. 要實(shí)現(xiàn)擴(kuò)容的話侍瑟,只能向上(垂直)擴(kuò)展,比如磁盤限制了數(shù)據(jù)的存儲(chǔ)界赔,就要擴(kuò)大磁盤容量,通過堆硬件的方式牵触,不支持動(dòng)態(tài)的擴(kuò)縮容淮悼。水平擴(kuò)容需要復(fù)雜的技術(shù)來實(shí)現(xiàn),比如分庫分表揽思。
  2. 表結(jié)構(gòu)修改困難袜腥,因此存儲(chǔ)的數(shù)據(jù)格式也受到限制。
  3. 在高并發(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):

  1. 存儲(chǔ)非結(jié)構(gòu)化的數(shù)據(jù),比如文本辽社、圖片伟墙、音頻、視頻滴铅。
  2. 表與表之間沒有關(guān)聯(lián)戳葵,可擴(kuò)展性強(qiáng)。
  3. 保證數(shù)據(jù)的最終一致性汉匙。遵循 BASE(堿)理論拱烁。 Basically Available(基本可用)生蚁; Soft-state(軟狀態(tài)); Eventually Consistent(最終一致性)邻梆。
  4. 支持海量數(shù)據(jù)的存儲(chǔ)和高并發(fā)的高效讀寫守伸。
  5. 支持分布式,能夠?qū)?shù)據(jù)進(jìn)行分片存儲(chǔ)浦妄,擴(kuò)縮容簡(jiǎn)單尼摹。

對(duì)于不同的存儲(chǔ)類型,我們又有各種各樣的非關(guān)系型數(shù)據(jù)庫剂娄,比如有幾種常見的類型:

  1. KV 存 儲(chǔ) 蠢涝, 用 Key Value 的 形 式 來 存 儲(chǔ) 數(shù) 據(jù) 。 比 較 常 見 的 有 Redis 和
    MemcacheDB阅懦。
  2. 文檔存儲(chǔ)和二,MongoDB。
  3. 列存儲(chǔ)耳胎,HBase惯吕。
  4. 圖存儲(chǔ),這個(gè)圖(Graph)是數(shù)據(jù)結(jié)構(gòu)怕午,不是文件格式废登。Neo4j。
  5. 對(duì)象存儲(chǔ)郁惜。
  6. 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 的特性:

  1. 更豐富的數(shù)據(jù)類型
  2. 進(jìn)程內(nèi)與跨進(jìn)程缩多;單機(jī)與分布式
  3. 功能豐富:持久化機(jī)制、過期策略
  4. 支持多種編程語言
  5. 高可用养晋,集群

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)部編碼有三種:

  1. int叠赐,存儲(chǔ) 8 個(gè)字節(jié)的長(zhǎng)整型(long欲账,2^63-1)。
  2. embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 簡(jiǎn)單動(dòng)態(tài)字符串)芭概,
    存儲(chǔ)小于 44 個(gè)字節(jié)的字符串赛不。
  3. 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))鹅髓。

  1. 使用字符數(shù)組必須先給目標(biāo)變量分配足夠的空間皆愉,否則可能會(huì)溢出鳞贷。
  2. 如果要獲取字符長(zhǎng)度,必須遍歷字符數(shù)組剧蹂,時(shí)間復(fù)雜度是 O(n)确徙。
  3. C 字符串長(zhǎng)度的變更會(huì)對(duì)字符數(shù)組做內(nèi)存重分配醒串。
  4. 通過從字符串開始到結(jié)尾碰到的第一個(gè)'\0'來標(biāo)記字符串的結(jié)束,因此不能保存圖片鄙皇、音頻芜赌、視頻、壓縮文件等二進(jìn)制(bytes)保存的內(nèi)容育苟,二進(jìn)制不安全较鼓。

SDS 的特點(diǎn):

  1. 不用擔(dān)心內(nèi)存溢出問題椎木,如果需要會(huì)對(duì) SDS 進(jìn)行擴(kuò)容违柏。
  2. 獲取字符串長(zhǎng)度時(shí)間復(fù)雜度為 O(1),因?yàn)槎x了 len 屬性香椎。
  3. 通過“空間預(yù)分配”( sdsMakeRoomFor)和“惰性空間釋放”漱竖,防止多次重分配內(nèi)存。
  4. 判斷是否結(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ū)別?

  1. 把所有相關(guān)的值聚集到一個(gè) key 中呛伴,節(jié)省內(nèi)存空間
  2. 只使用一個(gè) key寥掐,減少 key 沖突
  3. 當(dāng)需要批量獲取值的時(shí)候,只需要使用一個(gè)命令磷蜀,減少內(nèi)存/IO/CPU 的消耗

Hash 不適合的場(chǎng)景:

  1. Field 不能單獨(dú)設(shè)置過期時(shí)間
  2. 沒有 bit 操作
  3. 需要考慮數(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 的步驟:

  1. 為字符 ht[1]哈希表分配空間尚揣,這個(gè)哈希表的空間大小取決于要執(zhí)行的操作涌矢,以及 ht[0]當(dāng)前包含的鍵值對(duì)的數(shù)量掖举。
    擴(kuò)展:ht[1]的大小為第一個(gè)大于等于 ht[0].used*2快骗。
  2. 將所有的 ht[0]上的節(jié)點(diǎn) rehash 到 ht[1]上,重新計(jì)算 hash 值和索引塔次,然后放入指定的位置方篮。
  3. 當(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 編碼:

  1. 元素?cái)?shù)量小于 128 個(gè)
  2. 所有 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)行的:


  1. 23 首先和 7 比較,再和 19 比較巧号,比它們都大族奢,繼續(xù)向后比較。
  2. 但 23 和 26 比較的時(shí)候裂逐,比 26 要小歹鱼,因此回到下面的鏈表(原鏈表),與 22比較卜高。
  3. 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)品篩選
  • 排行榜
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翘骂,一起剝皮案震驚了整個(gè)濱河市壁熄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碳竟,老刑警劉巖草丧,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異莹桅,居然都是意外死亡昌执,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來懂拾,“玉大人煤禽,你說我怎么就攤上這事♂常” “怎么了呜师?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)贾节。 經(jīng)常有香客問我汁汗,道長(zhǎng),這世上最難降的妖魔是什么栗涂? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任知牌,我火速辦了婚禮,結(jié)果婚禮上斤程,老公的妹妹穿的比我還像新娘角寸。我一直安慰自己,他們只是感情好忿墅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布扁藕。 她就那樣靜靜地躺著,像睡著了一般疚脐。 火紅的嫁衣襯著肌膚如雪亿柑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天棍弄,我揣著相機(jī)與錄音望薄,去河邊找鬼。 笑死呼畸,一個(gè)胖子當(dāng)著我的面吹牛痕支,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛮原,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼卧须,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了儒陨?” 一聲冷哼從身側(cè)響起花嘶,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎框全,沒想到半個(gè)月后察绷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡津辩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年拆撼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了容劳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闸度,死狀恐怖竭贩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情莺禁,我是刑警寧澤留量,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站哟冬,受9級(jí)特大地震影響楼熄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜浩峡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一可岂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧翰灾,春花似錦缕粹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至咽块,卻和暖如春绘面,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背糜芳。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工飒货, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人峭竣。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晃虫,于是被迫代替她去往敵國(guó)和親皆撩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345