08 年的時候有一個意大利西西里島的小夥子,筆名 antirez(http://invece.org/),建立了一個訪客信息網站 LLOOGG.COM。有的時候咱們須要知道網站的訪問狀況,好比訪客的 IP、操做系統、瀏覽器、使用的搜索關鍵詞、所在地區、訪問的網頁地址等等。在國內,有不少網站提供了這個功能,好比 CNZZ,百度統計,國外也有谷歌的 GoogleAnalytics。咱們不用本身寫代碼去實現這個功能,只須要在全局的 footer 裏面嵌入一段JS 代碼就好了,當頁面被訪問的時候,就會自動把訪客的信息發送到這些網站統計的服務器,而後咱們登陸後臺就能夠查看數據了。 LLOOGG.COM 提供的就是這種功能,它能夠查看最多 10000 條的最新瀏覽記錄。這樣的話,它須要爲每個網站建立一個列表(List),不一樣網站的訪問記錄進入到不一樣的列表。若是列表的長度超過了用戶指定的長度,它須要把最先的記錄刪除(先進先出)。
當 LLOOGG.COM 的用戶愈來愈多的時候,它須要維護的列表數量也愈來愈多,這種記錄最新的請求和刪除最先的請求的操做也愈來愈多。LLOOGG.COM 最初使用的數據庫是 MySQL,可想而知,由於每一次記錄和刪除都要讀寫磁盤,由於數據量和併發量太大,在這種狀況下不管怎麼去優化數據庫都無論用了。
考慮到最終限制數據庫性能的瓶頸在於磁盤,因此 antirez 打算放棄磁盤,本身去實現一個具備列表結構的數據庫的原型,把數據放在內存而不是磁盤,這樣能夠大大地提高列表的 push 和 pop 的效率。antirez 發現這種思路確實能解決這個問題,因此用 C 語言重寫了這個內存數據庫,而且加上了持久化的功能,09 年,Redis 橫空出世了。從最開始只支持列表的數據庫,到如今支持多種數據類型,而且提供了一系列的高級特性,Redis 已經成爲一個在全世界被普遍使用的開源項目。爲何叫 REDIS 呢?它的全稱是 REmote DIctionary Service,直接翻譯過來是遠程字典服務。
從 Redis 的誕生歷史咱們看到了,在某些場景中,關係型數據庫並不適合用來存儲
咱們的 Web 應用的數據。那麼,關係型數據庫和非關係型數據庫,或者說 SQL 和 NoSQL,
到底有什麼不同呢?php
在絕大部分時候,咱們都會首先考慮用關係型數據庫來存儲咱們的數據,好比SQLServer,Oracle,MySQL 等等。 關係型數據庫的特色: 一、它以表格的形式,基於行存儲數據,是一個二維的模式。 二、它存儲的是結構化的數據,數據存儲有固定的模式(schema),數據須要適應表結構。 三、表與表之間存在關聯(Relationship)。 四、大部分關係型數據庫都支持 SQL(結構化查詢語言)的操做,支持複雜的關聯查詢。 五、經過支持事務(ACID 酸)來提供嚴格或者實時的數據一致性。 可是使用關係型數據庫也存在一些限制,好比: 一、要實現擴容的話,只能向上(垂直)擴展,好比磁盤限制了數據的存儲,就要擴大磁盤容量,經過堆硬件的方式,不支持動態的擴縮容。水平擴容須要複雜的技術來實現,好比分庫分表。 二、表結構修改困難,所以存儲的數據格式也受到限制。 三、在高併發和高數據量的狀況下,咱們的關係型數據庫一般會把數據持久化到磁盤,基於磁盤的讀寫壓力比較大。 爲了規避關係型數據庫的一系列問題,咱們就有了非關係型的數據庫,咱們通常把它叫作「non-relational」或者「Not Only SQL」。NoSQL 最開始是不提供 SQL 的數據庫的意思,可是後來意思慢慢地發生了變化。 非關係型數據庫的特色: 一、存儲非結構化的數據,好比文本、圖片、音頻、視頻。 二、表與表之間沒有關聯,可擴展性強。 三、保證數據的最終一致性。遵循 BASE(鹼)理論。 Basically Available(基本可用); Soft-state(軟狀態); Eventually Consistent(最終一致性)。 四、支持海量數據的存儲和高併發的高效讀寫。 五、支持分佈式,可以對數據進行分片存儲,擴縮容簡單。 對於不一樣的存儲類型,咱們又有各類各樣的非關係型數據庫,好比有幾種常見的類型 一、KV 存儲,用 Key Value 的形式來存儲數據。比較常見的有 Redis 和MemcacheDB。 二、文檔存儲,MongoDB。 三、列存儲,HBase。 四、圖存儲,這個圖(Graph)是數據結構,不是文件格式。Neo4j。 五、對象存儲。 六、XML 存儲等等等等。
這個網頁列舉了各類各樣的 NoSQL 數據庫 http://nosql-database.org/ 。html
NewSQL 結合了 SQL 和 NoSQL 的特性(例如 PingCAP 的 TiDB)。java
官網介紹:https://redis.io/topics/introduction
中文網站:http://www.redis.cn
硬件層面有 CPU 的緩存;瀏覽器也有緩存;手機的應用也有緩存。咱們把數據緩存起來的緣由就是從原始位置取數據的代價太大了,放在一個臨時位置存儲起來,取回就能夠快一些。
Redis 的特性:
1)更豐富的數據類型
2)進程內與跨進程;單機與分佈式
3)功能豐富:持久化機制、過時策略
4)支持多種編程語言
5)高可用,集羣node
CentOS Redis環境搭建python
默認有 16 個庫(0-15),能夠在配置文件中修改,默認使用第一個 db0。
databases 16
由於Redis沒有徹底把庫隔離,不像數據庫的 database,不適合把不一樣的庫分配給不一樣的業務使用。
切換數據庫ios
select 0
清空當前數據庫redis
flushdb
清空全部數據庫算法
flushall
Redis 是字典結構的存儲方式,採用 key-value 存儲。key 和 value 的最大長度限制是 512M(來自官網 https://redis.io/topics/data-types-intro/ )。
鍵的基本操做。 命令參考: http://redisdoc.com/index.html
密碼驗證spring
auth qazwsx
存值sql
set java 123
取值
get java
查看全部鍵
keys *
獲取鍵總數
dbsize
查看鍵是否存在 (存在返回1 不存在返回0)
exists java
刪除鍵
del java
重命名鍵
rename java php
查看類型
type php
Redis 一共有幾種數據類型?(注意是數據類型不是數據結構)
官網:https://redis.io/topics/data-types-intro
String、Hash、Set、List、Zset、Hyperloglog、Geo、Streams
最基本也是最經常使用的數據類型就是 String。set 和 get 命令就是 String 的操做命令。爲何叫 Binary-safe strings 呢?
能夠用來存儲字符串、整數、浮點數。
設置多個值(批量操做,原子性)若是有重複的 會覆蓋
mset sunda 1314 Java 520
設置值,若是 key 存在,則不成功
setnx sunda
基於此可實現分佈式鎖。用 del key 釋放鎖。
但若是釋放鎖的操做失敗了,致使其餘節點永遠獲取不到鎖,怎麼辦?
加過時時間。單獨用 expire 加過時,也失敗了,沒法保證原子性,怎麼辦?多參數
set key value [expiration EX seconds|PX milliseconds][NX|XX]
使用參數的方式
set lock1 1 EX 10 NX
(整數)值遞增
incr java incrby Java 100
(整數)值遞減
decr java decrby java 100
浮點數增量
set f 2.6 incrbyfloat f 7.3
獲取多個值
mget java php
獲取值長度
strlen Java
字符串追加內容
append sunda good
獲取指定範圍的字符
getrange sunda 0 8
查看對外類型
type sunda
set hello word 爲例,由於 Redis 是 KV 的數據庫,它是經過 hashtable 實現的(咱們把這個叫作外層的哈希)。因此每一個鍵值對都會有一個 dictEntry(源碼位置:dict.h),裏面指向了 key 和 value 的指針。next 指向下一個 dictEntry。
typedef struct dictEntry { void *key; /* key 關鍵字定義 */ union { void *val; uint64_t u64; /* value 定義 */ int64_t s64; double d; } v; struct dictEntry *next; /* 指向下一個鍵值對節點 */ } dictEntry;
key 是字符串,可是 Redis 沒有直接使用 C 的字符數組,而是存儲在自定義的 SDS中。
value 既不是直接做爲字符串存儲,也不是直接存儲在 SDS 中,而是存儲在redisObject 中。實際上五種經常使用的數據類型的任何一種,都是經過 redisObject 來存儲的。
redisObject 定義在 src/server.h 文件中。
typedef struct redisObject { unsigned type:4; /* 對象的類型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */ unsigned encoding:4;/* 具體的數據結構 */ //實際使用的編碼 unsigned lru:LRU_BITS; /* 24 位,對象最後一次被命令程序訪問的時間,與內存回收有關 */ int refcount; /* 引用計數。當 refcount 爲 0 的時候,表示該對象已經不被任何對象引用,則能夠進行垃圾回收了 */ void *ptr; /* 指向對象實際的數據結構 */ } robj;
字符串類型的內部編碼有三種:
一、int,存儲 8 個字節的長整型(long,2^63-1)。
二、embstr, 表明 embstr 格式的 SDS(Simple Dynamic String 簡單動態字符串),存儲小於 44 個字節的字符串。
三、raw,存儲大於 44 個字節的字符串(3.2 版本以前是 39 字節)。爲何是 39?
在源代碼object.c裏面定義44個字節
問題一、什麼是SDS
Redis 中字符串的實現。
在 3.2 之後的版本中,SDS 又有多種結構(sds.h):sdshdr五、sdshdr八、sdshdr1六、sdshdr3二、sdshdr64,用於存儲不一樣的長度的字符串,分別表明 2^5=32byte,2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB
問題二、爲何 Redis 要用 SDS 實現字符串?
咱們知道,C 語言自己沒有字符串類型(只能用字符數組 char[]實現)。
一、使用字符數組必須先給目標變量分配足夠的空間,不然可能會溢出。
二、若是要獲取字符長度,必須遍歷字符數組,時間複雜度是 O(n)。
三、C 字符串長度的變動會對字符數組作內存重分配。
四、經過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,所以不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。
SDS 的特色:
一、不用擔憂內存溢出問題,若是須要會對 SDS 進行擴容。
二、獲取字符串長度時間複雜度爲 O(1),由於定義了 len 屬性。
三、經過「空間預分配」( sdsMakeRoomFor)和「惰性空間釋放」,防止屢次重分配內存。
四、判斷是否結束的標誌是 len 屬性(它一樣以'\0'結尾是由於這樣就可使用 C語言中函數庫操做字符串的函數了),能夠包含'\0'
C字符串 | SDS |
---|---|
獲取字符串長度的複雜度爲 O(N) | 獲取字符串長度的複雜度爲 O(1) |
API 是不安全的,可能會形成緩衝區溢出 | API 是安全的,不會早晨個緩衝區溢出 |
修改字符串長度N次必然須要執行N次內存重分配 | 修改字符串長度N次最多須要執行N次內存重分配 |
只能保存文本數據 | 能夠保存文本或者二進制數據 |
可使用全部<string.h>庫中的函數 | 可使用一部分<string.h>庫中的函數 |
問題三 embstr 和 raw 的區別?
embstr 的使用只分配一次內存空間(由於 RedisObject 和 SDS 是連續的),而 raw須要分配兩次內存空間(分別爲 RedisObject 和 SDS 分配空間)。 所以與 raw 相比,embstr 的好處在於建立時少分配一次空間,刪除時少釋放一次空間,以及對象的全部數據連在一塊兒,尋找方便。 而 embstr 的壞處也很明顯,若是字符串的長度增長鬚要從新分配內存時,整個RedisObject 和 SDS 都須要從新分配空間,所以 Redis 中的 embstr 實現爲只讀
問題 4:int 和 embstr 何時轉化爲 raw?
當 int 數 據 不 再 是 整 數 , 或 大 小 超 過 了 long 的 範 圍(2^63-1=9223372036854775807)時,自動轉化爲 embstr。
192.168.2.171:6379> set k1 1 OK 192.168.2.171:6379> append k1 a (integer) 2 192.168.2.171:6379> object encoding k1 "raw"
問題 5:明明沒有超過閾值,爲何變成 raw 了?
192.168.2.171:6379> set k2 a OK 192.168.2.171:6379> object encoding k2 "embstr" 192.168.2.171:6379> append k2 b (integer) 2 192.168.2.171:6379> object encoding k2 "raw"
對於 embstr,因爲其實現是隻讀的,所以在對 embstr 對象進行修改時,都會先轉化爲 raw 再進行修改。
所以,只要是修改 embstr 對象,修改後的對象必定是 raw 的,不管是否達到了 44個字節。
問題 6:當長度小於閾值時,會還原嗎?
關於 Redis 內部編碼的轉換,都符合如下規律:編碼轉換在 Redis 寫入數據時完成,且轉換過程不可逆,只能從小內存編碼向大內存編碼轉換(可是不包括從新 set)
問題 7:爲何要對底層的數據結構進行一層包裝呢?
經過封裝,能夠根據對象的類型動態地選擇存儲結構和可使用的命令,實現節省
空間和優化查詢速度。
String 類型
例如:熱點數據緩存(例如報表,明星出軌),對象緩存,全頁緩存。
能夠提高熱點數據的訪問速度。
STRING 類型,由於 Redis 是分佈式的獨立服務,能夠在多個應用之間共享
例如:分佈式 Session
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
STRING 類型 setnx 方法,只有不存在時才能添加成功,返回 true。
http://redisdoc.com/string/set.html 建議用參數的形式
public Boolean getLock(Object lockObject){ jedisUtil = getJedisConnetion(); boolean flag = jedisUtil.setNX(lockObj, 1); if(flag){ expire(locakObj,10); } return flag; } public void releaseLock(Object lockObject){ del(lockObj); }
INT 類型,INCRBY,利用原子性
incrby userid 1000
(分庫分表的場景,一次性拿一段)
INT 類型,INCR 方法
例如:文章的閱讀量,微博點贊數,容許必定的延遲,先寫入 Redis 再定時同步到
數據庫。
INT 類型,INCR 方法
以訪問者的 IP 和其餘信息做爲 key,訪問一次增長一次計數,超過次數則返回 false。
String 類型的 BITCOUNT(1.6.6 的 bitmap 數據結構介紹)。
字符是以 8 位二進制存儲的。
set k1 a setbit k1 6 1 setbit k1 7 0 get k1
a 對應的 ASCII 碼是 97,轉換爲二進制數據是 01100001
b 對應的 ASCII 碼是 98,轉換爲二進制數據是 01100010
由於 bit 很是節省空間(1 MB=8388608 bit),能夠用來作大數據量的統計。
例如:在線用戶統計,留存用戶統計
setbit onlineusers 0 1 setbit onlineusers 1 1 setbit onlineusers 2 0
支持按位與、按位或等等操做。
BITOP AND destkey key [key ...] ,對一個或多個 key 求邏輯並,並將結果保存到 destkey 。 BITOP OR destkey key [key ...] ,對一個或多個 key 求邏輯或,並將結果保存到 destkey 。 BITOP XOR destkey key [key ...] ,對一個或多個 key 求邏輯異或,並將結果保存到 destkey 。 BITOP NOT destkey key ,對給定 key 求邏輯非,並將結果保存到 destkey 。
計算出 7 天都在線的用戶
BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
若是一個對象的 value 有多個值的時候,怎麼存儲?
例如用一個 key 存儲一張表的數據。
序列化?例如 JSON/Protobuf/XML,會增長序列化和反序列化的開銷,而且不能
單獨獲取、修改一個值。
能夠經過 key 分層的方式來實現,例如:
mset student:1:sno 16666 student:1:sname 孫達 student:1:company 騰訊
獲取值的時候一次獲取多個值:
mget student:1:sno student:1:sname student:1:company
缺點:key 太長,佔用的空間太多。有沒有更好的方式?
包含鍵值對的無序散列表。value 只能是字符串,不能嵌套其餘類型。
一樣是存儲字符串,Hash 與 String 的主要區別?
一、把全部相關的值彙集到一個 key 中,節省內存空間
二、只使用一個 key,減小 key 衝突
三、當須要批量獲取值的時候,只須要使用一個命令,減小內存/IO/CPU 的消耗
Hash 不適合的場景:
一、Field 不能單獨設置過時時間
二、沒有 bit 操做
三、須要考慮數據量分佈的問題(value 值很是大的時候,沒法分佈到多個節點)
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
Redis 的 Hash 自己也是一個 KV 的結構,相似於 Java 中的 HashMap。
外層的哈希(Redis KV 的實現)只用到了 hashtable。當存儲 hash 數據類型時,
咱們把它叫作內層的哈希。內層的哈希底層可使用兩種數據結構實現:
ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)
hashtable:OBJ_ENCODING_HT(哈希表)
/* 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 是一個通過特殊編碼的雙向鏈表,它不存儲指向上一個鏈表節點和指向下一
個鏈表節點的指針,而是存儲上一個節點長度和當前節點長度,經過犧牲部分讀寫性能,
來換取高效的內存空間利用率,是一種時間換空間的思想。只用在字段個數少,字段值
小的場景裏面。
ziplist.c 源碼第 16 行的註釋:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
typedef struct zlentry { unsigned int prevrawlensize; /* 上一個鏈表節點佔用的長度 */ unsigned int prevrawlen; /* 存儲上一個鏈表節點的長度數值所須要的字節數 */ unsigned int lensize; /* 存儲當前鏈表節點長度數值所須要的字節數 */ unsigned int len; /* 當前鏈表節點佔用的長度 */ unsigned int headersize; /* 當前鏈表節點的頭部大小(prevrawlensize + lensize),即非數據域的大小 */ unsigned char encoding; /* 編碼方式 */ unsigned char *p; /* 壓縮鏈表以字符串的形式保存,該指針指向當前節點起始位置 */ } zlentry;
編碼 encoding(ziplist.c 源碼第 204 行)
define ZIP_STR_06B (0 << 6) //長度小於等於 63 字節
當 hash 對象同時知足如下兩個條件的時候,使用 ziplist 編碼:
1)全部的鍵值對的健和值的字符串長度都小於等於 64byte(一個英文字母一個字節);
2)哈希對象保存的鍵值對數量小於 512 個。
在redis.conf配置文件
hash-max-ziplist-value 64 // ziplist 中最大能存放的值長度 hash-max-ziplist-entries 512 // ziplist 中最多能存放的 entry 節點數量
在源代碼t_hash.c
if (hashTypeLength(o) > server.hash_max_ziplist_entries) hashTypeConvert(o, OBJ_ENCODING_HT); /*源碼位置: t_hash.c,當字段值長度過大,轉爲 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; } }
一個哈希對象超過配置的閾值(鍵和值的長度有>64byte,鍵值對個數>512 個)時,會轉換成哈希表(hashtable)。
在 Redis 中,hashtable 被稱爲字典(dictionary),它是一個數組+鏈表的結構。
源碼位置:dict.h
前面咱們知道了,Redis 的 KV 結構是經過一個 dictEntry 來實現的。
Redis 又對 dictEntry 進行了多層的封裝。
typedef struct dictEntry { void *key; /* key 關鍵字定義 */ union { void *val; uint64_t u64; /* value 定義 */ int64_t s64; double d; } v; struct dictEntry *next; /* 指向下一個鍵值對節點 */ } 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; /* 哈希表數組 */ unsigned long size; /* 哈希表大小 */ unsigned long sizemask; /* 掩碼大小,用於計算索引值。老是等於 size-1 */ unsigned long used; /* 已有節點數 */ } dictht;
ht 放到了 dict 裏面:
typedef struct dict { dictType *type; /* 字典類型 */ void *privdata; /* 私有數據 */ dictht ht[2]; /* 一個字典有兩個哈希表 */ long rehashidx; /* rehash 索引 */ unsigned long iterators; /* 當前正在使用的迭代器數量 */ } dict;
從最底層到最高層 dictEntry——dictht——dict——OBJ_ENCODING_HT
總結:哈希的存儲結構
注意:dictht 後面是 NULL 說明第二個 ht 還沒用到。dictEntry*後面是 NULL 說明沒有 hash 到這個地址。dictEntry 後面是NULL 說明沒有發生哈希衝突。
String 能夠作的事情,Hash 均可以作。
好比對象或者一張表的數據,比 String 節省了更多 key 的空間,也更加便於集中管理。
key:用戶 id;field:商品 id;value:商品數量。
+1:hincr。-1:hdecr。刪除:hdel。全選:hgetall。商品數:hlen。
問題:爲何要定義兩個哈希表呢?ht[2]
redis 的 hash 默認使用的是 ht[0],ht[1]不會初始化和分配空間。
哈希表 dictht 是用鏈地址法來解決碰撞問題的。在這種狀況下,哈希表的性能取決於它的大小(size 屬性)和它所保存的節點的數量(used 屬性)之間的比率:
比率在 1:1 時(一個哈希表 ht 只存儲一個節點 entry),哈希表的性能最好;
若是節點數量比哈希表的大小要大不少的話(這個比例用 ratio 表示,5 表示平均一個 ht 存儲 5 個 entry),那麼哈希表就會退化成多個鏈表,哈希表自己的性能優點就再也不存在。
在這種狀況下須要擴容。Redis 裏面的這種操做叫作 rehash。
rehash 的步驟:
一、爲字符 ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操做,以及 ht[0]當前包含的鍵值對的數量。擴展:ht[1]的大小爲第一個大於等於 ht[0].used*2。
二、將全部的 ht[0]上的節點 rehash 到 ht[1]上,從新計算 hash 值和索引,而後放入指定的位置。
三、當 ht[0]所有遷移到了 ht[1]以後,釋放 ht[0]的空間,將 ht[1]設置爲 ht[0]表,並建立新的 ht[1],爲下次 rehash 作準備。
問題:何時觸發擴容?
static int dict_can_resize = 1; static unsigned int dict_force_resize_ratio = 5;
ratio = used / size,已使用節點與字典大小的比例
dict_can_resize 爲 1 而且 dict_force_resize_ratio 已使用節點數和字典大小之間的比率超過 1:5,觸發擴容
擴容判斷 _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;
擴容方法 dictExpand(源碼 dict.c)
int dictExpand(dict *d, unsigned long size) { /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; dictht n; /* the new hash table */ unsigned long realsize = _dictNextPower(size); /* Rehashing to the same table size is not useful. */ if (realsize == d->ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } /* Prepare a second hash table for incremental rehashing */ d->ht[1] = n; d->rehashidx = 0; 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)); }
存儲有序的字符串(從左到右),元素能夠重複。能夠充當隊列和棧的角色。
元素增減:
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
在早期的版本中,數據量較小時用 ziplist 存儲,達到臨界值時轉換爲 linkedlist 進行存儲,分別對應 OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST 。
3.2 版本以後,統一用 quicklist 來存儲。quicklist 存儲了一個雙向鏈表,每一個節點都是一個 ziplist。
quicklist(快速列表)是 ziplist 和 linkedlist 的結合體。
quicklist.h,head 和 tail 指向雙向列表的表頭和表尾
typedef struct quicklist { quicklistNode *head; /* 指向雙向列表的表頭 */ quicklistNode *tail; /* 指向雙向列表的表尾 */ unsigned long count; /* 全部的 ziplist 中一共存了多少個元素 */ unsigned long len; /* 雙向鏈表的長度,node 的數量 */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* 壓縮深度,0:不壓縮; */ } quicklist;
redis.conf 相關參數:
參數 | 含義 |
---|---|
list-max-ziplist-size(fill) | 正數表示單個 ziplist 最多所包含的 entry 個數。 負數表明單個 ziplist 的大小,默認 8k。 -1:4KB;-2:8KB;-3:16KB;-4:32KB;-5:64KB |
list-compress-depth(compress) | 壓縮深度,默認是 0。 1:首尾的 ziplist 不壓縮;2:首尾第一第二個 ziplist 不壓縮,以此類推 |
quicklistNode 中的*zl 指向一個 ziplist,一個 ziplist 能夠存放多個元素
typedef struct quicklistNode { struct quicklistNode *prev; /* 前一個節點 */ struct quicklistNode *next; /* 後一個節點 */ unsigned char *zl; /* 指向實際的 ziplist */ unsigned int sz; /* 當前 ziplist 佔用多少字節 */ unsigned int count : 16; /* 當前 ziplist 中存儲了多少個元素,佔 16bit(下同),最大 65536 個 */ unsigned int encoding : 2; /* 是否採用了 LZF 壓縮算法壓縮節點,1:RAW 2:LZF */ unsigned int container : 2; /* 2:ziplist,將來可能支持其餘結構存儲 */ unsigned int recompress : 1; /* 當前 ziplist 是否是已經被解壓出來做臨時使用 */ unsigned int attempted_compress : 1; /* 測試用 */ unsigned int extra : 10; /* 預留給將來使用 */ } quicklistNode;
ziplist 的結構前面已經說過了,再也不重複。
由於 List 是有序的,能夠用來作用戶時間線
List 提供了兩個阻塞的彈出操做:BLPOP/BRPOP,能夠設置超時時間。
BLPOP:BLPOP key1 timeout 移出並獲取列表的第一個元素, 若是列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
BRPOP:BRPOP key1 timeout 移出並獲取列表的最後一個元素, 若是列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
隊列:先進先出:rpush blpop,左頭右尾,右邊進入隊列,左邊出隊列。
棧:先進後出:rpush brpop
String 類型的無序集合,最大存儲數量 2^32-1(40 億左右)。
添加一個或者多個元素
sadd myset a b c d e f g
獲取全部元素
smembers myset
統計元素個數
scard myset
隨機獲取一個元素
srandmember key
隨機彈出一個元素
spop myset
移除一個或者多個元素
srem myset d e f
查看元素是否存在
sismember myset a
Redis 用 intset 或 hashtable 存儲 set。若是元素都是整數類型,就用 inset 存儲。若是不是整數類型,就用 hashtable(數組+鏈表的存來儲結構)。
問題:KV 怎麼存儲 set 的元素?key 就是元素的值,value 爲 null。
若是元素個數超過 512 個,也會用 hashtable 存儲。
隨機獲取元素
spop myset
這條微博的 ID 是 t1001,用戶 ID 是 u3001。
用 like:t1001 來維護 t1001 這條微博的全部點贊用戶。
點讚了這條微博:sadd like:t1001 u3001
取消點贊:srem like:t1001 u3001
是否點贊:sismember like:t1001 u3001
點讚的全部用戶:smembers like:t1001
點贊數:scard like:t1001
比關係型數據庫簡單許多。
用 tags:i5001 來維護商品全部的標籤。
sadd tags:i5001 畫面清晰細膩
sadd tags:i5001 真彩清晰顯示屏
sadd tags:i5001 流暢至極
獲取差集
sdiff set1 set2
獲取交集( intersection )
sinter set1 set2
獲取並集
sunion set1 set2
Phone11 上市了。
sadd brand:apple iPhone11 sadd brand:ios iPhone11 sad screensize:6.0-6.24 iPhone11 sad screentype:lcd iPhone11
篩選商品,蘋果的,iOS 的,屏幕在 6.0-6.24 之間的,屏幕材質是 LCD 屏幕
sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd
sorted set,有序的 set,每一個元素有個 score。
score 相同時,按照 key 的 ASCII 碼排序。
數據結構對比:
數據 結構 | 是否容許 重複元素 | 是否有序 | 有序實現方式 |
---|---|---|---|
列表 list | 是 | 是 | 索引下標 |
集合 set | 否 | 否 | 無 |
有序集合 zset | 否 | 是 | 分值 score |
添加元素
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
獲取所有元素
zrange myzset 0 -1 withscores zrevrange myzset 0 -1 withscores
根據分值區間獲取元素
zrangebyscore myzset 20 30
移除元素
也能夠根據 score rank 刪除
zrem myzset php cpp
統計元素個數
zcard myzset
分值遞增
zincrby myzset 5 python
根據分值統計個數
zcount myzset 20 60
獲取元素 rank
zrank myzset java
獲取元素 score
zsocre myzset java
也有倒序的 rev 操做(reverse)
同時知足如下條件時使用 ziplist 編碼:
元素數量小於 128 個
全部 member 的長度都小於 64 字節
在 ziplist 的內部,按照 score 排序遞增來存儲。插入的時候要移動以後的數據。
在 redis.conf 參數
zset-max-ziplist-entries 128 zset-max-ziplist-value 64
超過閾值以後,使用 skiplist+dict 存儲
問題:什麼是 skiplist?
咱們先來看一下有序鏈表:
在這樣一個鏈表中,若是咱們要查找某個數據,那麼須要從頭開始逐個進行比較,直到找到包含數據的那個節點,或者找到第一個比給定數據大的節點爲止(沒找到)。也就是說,時間複雜度爲 O(n)。一樣,當咱們要插入新數據的時候,也要經歷一樣的查找過程,從而肯定插入位置。
而二分查找法只適用於有序數組,不適用於鏈表。
假如咱們每相鄰兩個節點增長一個指針(或者理解爲有三個元素進入了第二層),讓指針指向下下個節點。
這樣全部新增長的指針連成了一個新的鏈表,但它包含的節點個數只有原來的一半(上圖中是 7, 19, 26)。在插入一個數據的時候,決定要放到那一層,取決於一個算法
(在 redis 中 t_zset.c 有一個 zslRandomLevel 這個方法)。
如今當咱們想查找數據的時候,能夠先沿着這個新鏈表進行查找。當碰到比待查數據大的節點時,再回到原來的鏈表中的下一層進行查找。好比,咱們想查找 23,查找的路徑是沿着下圖中標紅的指針所指向的方向進行的:
在這個查找過程當中,因爲新增長的指針,咱們再也不須要與鏈表中每一個節點逐個進行比較了。須要比較的節點數大概只有原來的一半。這就是跳躍表。
爲何不用 AVL 樹或者紅黑樹?由於 skiplist 更加簡潔。
源碼:server.h
typedef struct zskiplistNode { sds ele; /* zset 的元素 */ double score;/* 分值 */ struct zskiplistNode *backward; /* 後退指針 */ struct zskiplistLevel { struct zskiplistNode *forward; /* 前進指針,對應 level 的下一個節點 */ unsigned long span; /* 從當前節點到下一個節點的跨度(跨越的節點數) */ } level[]; /* 層 */ } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; /* 指向跳躍表的頭結點和尾節點 */ unsigned long length;/* 跳躍表的節點數 */ int level;/* 最大的層數 */ } zskiplist; typedef struct zset { dict *dict; zskiplist *zsl; } zset;
隨機獲取層數的函數:
源代碼 t_zset.c
/* Returns a random level for the new skiplist node we are going to create. * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL * (both inclusive), with a powerlaw-alike distribution where higher * levels are less likely to be returned. */ int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }
d 爲 6001 的新聞點擊數加 1:zincrby hotNews:20190926 1 n6001
獲取今天點擊最多的 15 條:zrevrange hotNews:20190926 0 15 withscores
https://redis.io/topics/data-types-intro
Bitmaps 是在字符串類型上面定義的位操做。一個字節由 8 個二進制位組成。
set k1 a
獲取 value 在 offset 處的值(a 對應的 ASCII 碼是 97,轉換爲二進制數據是 01100001)
getbit k1 0
修改二進制數據(b 對應的 ASCII 碼是 98,轉換爲二進制數據是 01100010)
setbit k1 6 1 setbit k1 7 0 get k1
統計二進制位中 1 的個數
bitcount k1
獲取第一個 1 或者 0 的位置
bitpos k1 1 bitpos k1 0
BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 這四種操做中的任意一種參數:
BITOP AND destkey srckey1 … srckeyN ,對一個或多個 key 求邏輯與,並將結果保存到 destkey
BITOP OR destkey srckey1 … srckeyN,對一個或多個 key 求邏輯或,並將結果保存到 destkey
BITOP XOR destkey srckey1 … srckeyN,對一個或多個 key 求邏輯異或,並將結果保存到 destkey
BITOP NOT destkey srckey,對給定 key 求邏輯非,並將結果保存到 destkey
應用場景:
用戶訪問統計
在線用戶統計
Hyperloglogs:提供了一種不太準確的基數統計方法,好比統計網站的 UV,存在
必定的偏差。HyperLogLogTest.java
5.0 推出的數據類型。支持多播的可持久化的消息隊列,用於實現發佈訂閱功能,借
鑑了 kafka 的設計。
對象 | 對象 type 屬性 值 | type 命令輸出 | 底層可能的存儲結構 | object encoding |
---|---|---|---|---|
字符串對象 | OBJ_STRING | string | OBJ_ENCODING_INT OBJ_ENCODING_EMBSTR OBJ_ENCODING_RAW |
int embstr raw |
列表對象 | OBJ_LIST | list | OBJ_ENCODING_QUICKLIST | quicklist |
哈希對象 | OBJ_HASH | hash | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_HT |
ziplist hashtable |
集合對象 | OBJ_SET | set | OBJ_ENCODING_INTSET OBJ_ENCODING_HT |
intset hashtable |
有序集合對象 | OBJ_ZSET | zset | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_SKIPLIST |
ziplist skiplist(包含 ht) |
對象 | 原始編碼 | 升級編碼 | |
---|---|---|---|
字符串對象 | INT 整數而且小於 long 2^63-1 |
embstr 超過 44 字節,被修改 |
raw |
哈希對象 | ziplist 鍵和值的長度小於 64byte,鍵值對個數不 超過 512 個,同時知足 |
hashtable |
|
列表對象 | quicklist | ||
集合對象 | intset 元素都是整數類型,元素個數小於 512 個, 同時知足 |
hashtable |
|
有序集合對象 | ziplist 元素數量不超過 128 個,任何一個 member 的長度小於 64 字節,同時知足。 |
skiplist |
1 Redis 爲何要本身實現一個 SDS ?
問題二、爲何 Redis 要用 SDS 實現字符串?
C 語言自己沒有字符串類型(只能用字符數組 char[]實現)。
一、使用字符數組必須先給目標變量分配足夠的空間,不然可能會溢出。
二、若是要獲取字符長度,必須遍歷字符數組,時間複雜度是 O(n)。
三、C 字符串長度的變動會對字符數組作內存重分配。
四、經過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,所以不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。
SDS 的特色:
一、不用擔憂內存溢出問題,若是須要會對 SDS 進行擴容。
二、獲取字符串長度時間複雜度爲 O(1),由於定義了 len 屬性。
三、經過「空間預分配」( sdsMakeRoomFor)和「惰性空間釋放」,防止屢次重分配內存。
四、判斷是否結束的標誌是 len 屬性(它一樣以'\0'結尾是由於這樣就可使用 C語言中函數庫操做字符串的函數了),能夠包含'\0'
C字符串 | SDS |
---|---|
獲取字符串長度的複雜度爲 O(N) | 獲取字符串長度的複雜度爲 O(1) |
API 是不安全的,可能會形成緩衝區溢出 | API 是安全的,不會早晨個緩衝區溢出 |
修改字符串長度N次必然須要執行N次內存重分配 | 修改字符串長度N次最多須要執行N次內存重分配 |
只能保存文本數據 | 能夠保存文本或者二進制數據 |
可使用全部<string.h>庫中的函數 | 可使用一部分<string.h>庫中的函數 |
二、基於Set 如何實現用戶關注模型?
我:me 他:he 我關注的人:focus 關注個人人:attention
操做命令
sadd attention:me he u3 u4 sadd attention:me u3 he sadd focus:me he u3 sadd focus:he u4
1)相互關注?
sinter attention:me focus:me 1) "he" 2) "u3"
2)我關注的人也關注了他?
sunion focus:me attention:he 2) "he" 3) "u3"
3)可能認識的人?
我關注的人 和他關注的人差集 可是要去除我和他
三、dict裏面爲何要定義兩個哈希表ht[0] ht[1]?hash擴容是怎麼實現的?
redis 的 hash 默認使用的是 ht[0],ht[1]不會初始化和分配空間。
哈希表 dictht 是用鏈地址法來解決碰撞問題的。在這種狀況下,哈希表的性能取決於它的大小(size 屬性)和它所保存的節點的數量(used 屬性)之間的比率:
比率在 1:1 時(一個哈希表 ht 只存儲一個節點 entry),哈希表的性能最好;
若是節點數量比哈希表的大小要大不少的話(這個比例用 ratio 表示,5 表示平均一個 ht 存儲 5 個 entry),那麼哈希表就會退化成多個鏈表,哈希表自己的性能優點就再也不存在。
在這種狀況下須要擴容。Redis 裏面的這種操做叫作 rehash。
hash擴容是怎麼實現的?
一、爲字符 ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操做,以及 ht[0]當前包含的鍵值對的數量。擴展:ht[1]的大小爲第一個大於等於 ht[0].used*2。
二、將全部的 ht[0]上的節點 rehash 到 ht[1]上,從新計算 hash 值和索引,而後放入指定的位置。
三、當 ht[0]所有遷移到了 ht[1]以後,釋放 ht[0]的空間,將 ht[1]設置爲 ht[0]表,並建立新的 ht[1],爲下次 rehash 作準備。
若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號。