## 一、Redis 鍵(Key) 構成
```C
typedef struct redisObject {
unsigned type:4;//類型 五種對象類型 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
unsigned encoding:4; //編碼 4表示位數
void *ptr;//指向底層實現數據結構的指針,指向具體數據
//...
int refcount;//引用計數
//...
unsigned lru:LRU_BITS; //LRU_BITS為24bit 記錄最后一次被命令程序訪問的時間
? ? ? ?//高16位存儲一個分鐘數級別的時間戳; 低8位存儲訪問計數; lfu; 最近訪問次數;
//...
} robj;
```
1. 查看對象類型 `type key`
:-: 
2. 獲取value具體編碼 `object encoding key`
:-: 
> 可以看出,value為string類型的,編碼可以有3種:`embstr`,`int`,`raw`;當字符串長度大于44(整個sds總體超過64)時,就用raw存儲。sds結構自身占用19個字節,字符串結尾標識"\\0"占一個字節,所以**64 - 19 -1 = 44**
3. 查看key的結構信息 `debug object key`
:-: 
## 二、Redis二進制安全
1. 什么是二進制安全?
二進制安全就是指,在傳輸數據時,保證二進制數據的信息安全,即不被篡改、破譯等。簡單來說,就是只關心二進制化的字符串,不關心具體格式,只會嚴格的按照二進制數據存取,不會妄圖以某種特殊格式解析數據。例如:在C語言中,就以"\\0"來判定字符串的結尾。
2. Redis是如何保證二進制安全的?
~~~c
struct sdshdr{
? ? ? ?int len;//buf數組中已經使用的字節的數量,也就是SDS字符串長度
? ? ? ?int ?free;//buf數組中未使用的字節的數量
? ? ? ?char buf[];//字節數組,字符串就保存在這里面
};
~~~
Redis 通過定義上述結構體的方式,擴展了C語言底層字符串的缺點;不再像C語言那樣,以"\\0"作為字符串的結尾,而是使用了獨立的`len`字段來表示字符串的長度。這樣就避免了如果字符串中出現"\\0"而被截取忽略后面字符串的問題;保證了二進制安全。
> 基于此,Redis的string可以支持各種類型(圖片、視頻、文本等);而C語言的字符串就只能存儲文本格式的數據。
:-: 
3. Redis的簡單動態字符串SDS對比C語言中的字符串char,有什么優勢?
* 可以在O(1)的時間復雜度得到字符串的長度 (sds結構中,len字段直接記錄了長度)
* 二進制安全
* 可以高效的追加字符串操作
原理:sds會判斷當前字符串空余(free)的長度與需要追加的字符串長度;如果空余大于需要追加,就會直接追加,減少了重新分配內存的操作;如果空余小于需要追加,那么就先對sds進行擴展,然后再追加;只是這里擴展內存是按照一定的機制進行的,擴展后多余的空間不釋放,方便下次追加字符串,會造成一定的內存浪費,但是在頻繁追加操作下,這種機制就很高效。
4. 1. SDS 內存擴展機制
在已經分配的內存低于1M時,每次擴容都是以現有內存2倍的方式擴容;當超過1M時,每次只擴容1M
## 三、5種基本數據類型
### 1、字符串—string
1). 常見命令
~~~
set get | mset mget | setrange getrange | getset | append | strlen
?
incr decr | incr by decrby |
?
setbit getbit | bitcount | bitop | bitpos
~~~
2). 使用場景
* 計數器。如:文章瀏覽數、帖子點贊數等
~~~
incr article:100.view # ID為100的文章,每打開一次,瀏覽次數增加1
~~~
* 分布式鎖。如:秒殺活動中,商品庫存問題
~~~
set goods:100 1 EX 30 NX # 當key goods:100 不存在時拿到鎖,并設置過期時間為30s
~~~
* 緩存。如果:熱點數據的緩存
~~~
set hotgoods:100 '{"id":100, "price": 998, "title": "鳥哥筆記"}' # 緩存熱賣書籍基本信息
~~~
* 活躍用戶數統計
~~~
# 假如:某東 618 活動,需要為用戶準備禮品;現需要統計最近三天活躍用戶,用于備貨
# 通過bitmap,以日期為key記錄用戶的登錄,然后拿對應日期做與運算,就很容易實現
?
setbit 20210615 10 1 ?# 6月15號,ID為10的用戶登錄
setbit 20210615 8 1 ? # 6月15號,ID為8的用戶登錄
?
setbit 20210616 10 1 ?# 6月16號,ID為10的用戶登錄
setbit 20210616 15 1 ?# 6月16號,ID為15的用戶登錄
setbit 20210616 5 1 ? # 6月16號,ID為5的用戶登錄
?
setbit 20210617 15 1 ?# 6月17號,ID為15的用戶登錄
setbit 20210617 13 1 ?# 6月17號,ID為13的用戶登錄
?
# 可以看出15、16、17號這三天,總共有5位用戶活躍
bitop or mau 20210615 20210616 20210617 # 會將計算結果以mau作為key存儲
bitcount mau # 結果為5
~~~
:-: 
* 用戶登錄天數統計
```bash
# 需求:需要統計某個用戶某個時間段的登錄天數
# ID為88的用戶,1月份中有登陸行為的天數;以1月1號為第1天,12月31號為第365天
setbit login:88 0 1 # 第一天有登陸
setbit login:88 8 1 # 第8天有登陸
setbit login:88 12 1 # 第12天有登陸
setbit login:88 13 1 # 第13天有登陸
setbit login:88 20 1 # 第20天有登陸
setbit login:88 25 1 # 第25天有登陸
setbit login:88 27 1 # 第27天有登陸
bitcount login:88 # 結果為:7
```
### 2、列表—list
> List, 元素可以重復,可以按照添加的先后保證順序
1)、常用命令
~~~
lpush rpush | lpop rpop | blpop brpop | lrem linsert | llen | lrange | lindex
~~~
2)、使用場景
* 棧
~~~
lpush
lpop
~~~
* 隊列
~~~
lpush
rpop
~~~
* 阻塞MQ
~~~
lpush
brpop
~~~
:-: 
### 3、哈希—hash
1)、常用命令
~~~
hset hget | hmset | hmget | hstrlen | hgetall | hlen | hincrby |
hincrbyfloat | hexists | hdel | hkeys
~~~
2)、使用場景
* 存儲對象信息
~~~
# 存儲用戶信息;如姓名,年齡,地址等
hmset uid:101 name zhangsan age 18 addr 北京
~~~
### 4、集合—set
> 集合中的元素會去重,保證不重復
1)、常用命令
~~~
sadd srem spop | sinter sdiff sunion| sinterstore sdiffstore sunionstore |
smembers | sismember | srandmember | scard
~~~
2)、使用場景
> 抽獎和關注模型兩大類
* 抽獎程序
~~~
# 100張購物卡,共30人參與抽獎
# 獎品大于抽獎人
sadd k1 tom xxoo xoox xoxo oxxo ooxx oxox # 將30個參與抽獎的人添加到集合
srandmember k1 -100 # 這里-100,會返回100個元素,隨機重復
?
# 公司年會抽獎,一等獎1 二等獎3 三等獎 5 中獎者會從參與者中剔除,不再參與其他獎項
sadd k1 tom xxoo xoox xoxo oxxo ooxx oxox # 將參與抽獎員工添加到集合
spop k1 1 # 一等獎
spop k1 3 # 二等獎
spop k1 5 # 三等獎
~~~
* 共同關注
~~~
# 小王關注了:劉德華、羅大佑、郭富城、黎明
# 小李關注了:黎明、張曼玉、馬龍、張繼科
# 求小王和小李共同關注的人
sadd k1 劉德華 羅大佑 郭富城 黎明
sadd k2 黎明 張曼玉 馬龍 張繼科
sinter k1 k2 # 結果為黎明
~~~
* 猜你喜歡
~~~
# 我關注了:劉德華、羅大佑、郭富城、黎明
# 小王關注了:黎明、張曼玉、馬龍、張繼科
# 當我進入小王的主頁后,可以推薦我關注 張曼玉、馬龍、張繼科
sadd k1 劉德華 羅大佑 郭富城 黎明
sadd k2 黎明 張曼玉 馬龍 張繼科
sdiff k2 k1 # 張曼玉 馬龍 張繼科
~~~
### 5、有序集合—sort\_set
> 集合中數據去重,并可以按照給出的規則(一定的分值)排序
1)、常用命令
~~~
zadd zrange zrangebyscore zcount | zincrby | zinterstore zunionstore |
zrevrange zrevrangebyscore | zremrangebyscore
~~~
2)、使用場景
* 排行榜
~~~
# 熱點新聞,每點擊一次,熱搜值加1
zincrby hotnews:20210822 1 汪峰開演唱會
?
# 展示當天排行榜前十的熱搜
zrevrange hotnews:20210822 0 9 withscores
?
# 計算最近3日熱搜榜
zunionstore unkey 3 hotnews:20210820 hotnews:20210821 hotnews:20210822
?
# 從上述3日熱搜榜中,展示前3
zrevrange unkey 0 2 withscores
~~~
* 帶有權重的隊列
## 四、跳表(skip list)
跳表是一個特殊的鏈表,相比一般的鏈表有更高的查找效率;Redis中的有序列表就使用了這種結構
:-: 
1. 查找 (例如:查找40)
* 第一步拿目標40跟三級索引第一個節點18比較;由于40 大于 18,則跟18的下一個節點45進行比較,而40小于45
* 通過第一步,可以知道目標40介于三級所以第一個節點和下一個節點之間,所以直接來到三級索引第一個節點18的二級索引
* 此時在二級索引,40大于18的下一個節點36,則移動指針到36;由于40小于36的下一個節點45,所以將指針移動到二級索引36的上級原始鏈表
* 此時發現,原始鏈表36的下一個節點復合條件
2. 插入節點
* 新插入的節點和各級索引節點逐個比較,確定原始鏈表的位置(同上述查找過程)
* 把新的數據插入到原始鏈表
* 利用拋硬幣的隨機方式,決定是否將新的節點提升為上一級索引
3. 刪除節點
* 自上而下,查找第一次出現節點的索引,并逐層找到每一層對應的節點
* 刪除每一層找到的節點,如果該層只剩下一個節點,則刪除整個層(原始鏈表除外)
## 五、管道(Pipeline)
當有多個命令(command)需要被及時提交,并且這些命令對相應結果沒有互相依賴、對響應也無需立即獲得;那么就可以使用管道來實現這種批處理。這在一定程度上可以提升性能,原因主要是TCP連接中減少了交互往返時間。假如有三條命令,單個提交就得需要3次往返;而使用管道批處理,只需要一次往返。Redis server 收到Pipeline發送過來的數據后,會以隊列的形式放在內存中后,開始一條條的執行。
:-: 
1. 注意事項
* 可以為Pipeline操作新建Client連接,讓其與其他正常操作隔離開在不同的Client連接中(由于Pipeline是獨占連接的,所以在此期間是不可以進行其他操作的)
* Pipeline所能容忍的操作數,和socket緩沖區大小有很大的關系,受限于server的物理內存和網絡接口的緩沖能力
* Pipeline只是讓一批命令按順序執行,不能保證原子性
2. 使用場景
將數據庫中的一批數據,一次性批量的存入Redis;可以考慮采用Pipeline實現
## 六、發布/訂閱 (Pub/Sub)
1. 常用命令
~~~
publish subscribe unsubscribe | psubscribe punsubscribe
~~~
2. 使用場景
* 普通的實時聊天、群聊功能
* 網站某一模塊更新后,推送消息給到訂閱者
3. 與Redis通過list結構實現消息隊列的區別:
list 消息隊列,再有多個消費者的情況下,一條消息只會有一個消費者獲取消費;
而發布訂閱者,監聽同一發布者同一頻道的消息,多個訂閱者都會收到消息
> 假如,現在需要開發一個聊天軟件,需要實現:
>
> 1、好友可以接收消息
>
> 2、可以查詢聊天信息(考慮3天以內查詢頻次高,3天以外基本不會查詢)
>
> 請給出架構方案
:-: 
上述架構方案,重點關注紅色虛線的部分。Client負責將消息Pub出去,這時候同一個頻道的好友就會收到消息;同時還需負責將數據存入DB和sort\_set以備滿足不同時間的查詢(紅色虛線部分),這種情況下很容易出現當發生網絡錯誤時,DB和sort\_set丟數據。
針對上述問題,可以將保存DB和sort\_set的部分,也以訂閱者身份去監聽Client的消息發布,然后通過監聽的服務負責保存數據。
:-: 