Redis高級
發(fā)布訂閱
Redis提供了發(fā)布訂閱功能,可以用于消息的傳輸
Redis的發(fā)布訂閱機制包括三個部分,publisher,subscriber和Channel

發(fā)布者和訂閱者都是Redis客戶端,Channel則為Redis服務器端。 發(fā)布者將消息發(fā)送到某個的頻道,訂閱了這個頻道的訂閱者就能接收到這條消息。
頻道/模式的訂閱與退訂
subscribe:訂閱 subscribe channel1 channel2 ..
Redis客戶端1訂閱頻道1和頻道2
127.0.0.1:6379> subscribe ch1 ch2
Reading messages... (press Ctrl-C to quit) 1) "subscribe"
2) "ch1"
3) (integer) 1
1) "subscribe"
2) "ch2"
3) (integer) 2
publish:發(fā)布消息 publish channel message Redis客戶端2將消息發(fā)布在頻道1和頻道2上
127.0.0.1:6379> publish ch1 hello (integer) 1
127.0.0.1:6379> publish ch2 world (integer) 1
Redis客戶端1接收到頻道1和頻道2的消息
1) "message"
2) "ch1"
3) "hello"
1) "message"
2) "ch2"
3) "world"
unsubscribe:退訂 channel Redis客戶端1退訂頻道1
127.0.0.1:6379> unsubscribe ch1 1) "unsubscribe"
2) "ch1"
3) (integer) 0
psubscribe :模式匹配 psubscribe +模式 Redis客戶端,訂閱所有以ch開頭的頻道
127.0.0.1:6379> psubscribe ch*
Reading messages... (press Ctrl-C to quit) 1) "psubscribe"
2) "ch*"
3) (integer) 1
Redis客戶端2發(fā)布信息在頻道5上
127.0.0.1:6379> publish ch5 helloworld
(integer) 1
Redis客戶端1收到頻道5的信息
1) "pmessage"
2) "ch*"
3) "ch5"
4) "helloworld"
punsubscribe 退訂模式
127.0.0.1:6379> punsubscribe ch*
1) "punsubscribe"
2) "ch*"
3) (integer) 0
發(fā)布訂閱機制
訂閱某個頻道或模式:
客戶端(client):
屬性為pubsub_channels,該屬性表明了該客戶端訂閱的所有頻道
屬性為pubsub_patterns,該屬性表示該客戶端訂閱的所有模式
服務器端(RedisServer):
- 屬性為pubsub_channels,該服務器端中的所有頻道以及訂閱了這個頻道的客戶端
- 屬性為pubsub_patterns,該服務器端中的所有模式和訂閱了這些模式的客戶端
typedef struct redisClient { ...
dict *pubsub_channels; //該client訂閱的channels,以channel為key用dict的方式組織
list *pubsub_patterns; //該client訂閱的pattern,以list的方式組織
...
} redisClient;
struct redisServer { ...
//redis server進程中維護的channel dict,它以channel為key,訂閱channel的client list為value
dict *pubsub_channels;
//redis server進程中維護的pattern list
list *pubsub_patterns;
int notify_keyspace_events;
...
};
當客戶端向某個頻道發(fā)送消息時,Redis首先在redisServer中的pubsub_channels中找出鍵為該頻道的結點,遍歷該結點的值,即遍歷訂閱了該頻道的所有客戶端,將消息發(fā)送給這些客戶端。然后,遍歷結構體redisServer中的pubsub_patterns,找出包含該頻道的模式的結點,將消息發(fā)送給訂閱了該模式的客戶端。
使用場景
在Redis哨兵模式中,哨兵通過發(fā)布與訂閱的方式與Redis主服務器和Redis從服務器進行通信。這個我們將在后面的章節(jié)中詳細講解。
Redisson是一個分布式鎖框架,在Redisson分布式鎖釋放的時候,是使用發(fā)布與訂閱的方式通知的, 這個我們將在后面的章節(jié)中詳細講解。
事務
所謂事務(Transaction) ,是指作為單個邏輯工作單元執(zhí)行的一系列操作
ACID回顧
- Atomicity(原子性):構成事務的的所有操作必須是一個邏輯單元,要么全部執(zhí)行,要么全部不 執(zhí)行。Redis:一個隊列中的命令執(zhí)行或不執(zhí)行
- Consistency(一致性):數(shù)據(jù)庫在事務執(zhí)行前后狀態(tài)都必須是穩(wěn)定的或者是一致的。 Redis: 集群中不能保證時時的一致性,只能是最終一致性
- Isolation(隔離性):事務之間不會相互影響。Redis: 命令是順序執(zhí)行的,在一個事務中,有可能被執(zhí)行其他客戶端的命令的
- Durability(持久性):事務執(zhí)行成功后必須全部寫入磁盤。 Redis有持久化但不保證數(shù)據(jù)的完整性
Redis事務
- Redis的事務是通過multi、exec、discard和watch這四個命令來完成的。
- Redis的單個命令都是原子性的,所以這里需要確保事務性的對象是命令集合。
- Redis將命令集合序列化并確保處于同一事務的命令集合連續(xù)且不被打斷的執(zhí)行
- Redis不支持回滾操作
事務命令
- multi:用于標記事務塊的開始,Redis會將后續(xù)的命令逐個放入隊列中,然后使用exec原子化地執(zhí)行這個命令隊列
- exec:執(zhí)行命令隊列
- discard:清除命令隊列
- watch:監(jiān)視key
- unwatch:清除監(jiān)視key

127.0.0.1:6379> set name:1 aaa
OK
127.0.0.1:6379> get name:1
"aaa"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name:2 lishi
QUEUED
127.0.0.1:6379> set name:3 zhangsan
QUEUED
127.0.0.1:6379> get name:2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "lishi"
127.0.0.1:6379> get name:3
"zhangsan"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name:4 1111
QUEUED
127.0.0.1:6379> get name:4
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379>
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> get name:4
(nil)
127.0.0.1:6379> get name:1
"aaa"
127.0.0.1:6379> watch name:1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name:4 444444
QUEUED
127.0.0.1:6379> exec
1) OK
127.0.0.1:6379> get name:4
"444444"
127.0.0.1:6379> get name:1
"aaa"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name:4 sfjskljdf
QUEUED
127.0.0.1:6379> exec
1) OK
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> watch name:1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name:4 sdf
QUEUED
127.0.0.1:6379> get name:4
QUEUED
127.0.0.1:6379> exec
(nil)
事務機制
事務的執(zhí)行
事務開始 :在RedisClient中,有屬性flags,用來表示是否在事務中 flags=REDIS_MULTI
命令入隊 :RedisClient將命令存放在事務隊列中 (EXEC,DISCARD,WATCH,MULTI除外)
事務隊列 : multiCmd *commands 用于存放命令
執(zhí)行事務 : RedisClient向服務器端發(fā)送exec命令,RedisServer會遍歷事務隊列,執(zhí)行隊列中的命令,最后將執(zhí)行的結果一次性返回給客戶端。
如果某條命令在入隊過程中發(fā)生錯誤,redisClient將flags置為REDIS_DIRTY_EXEC,EXEC命令將會失敗 返回。

127.0.0.1:6379> set aa 11
QUEUED
127.0.0.1:6379> set bb 22
QUEUED
127.0.0.1:6379> get aa
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "11"
Watch的執(zhí)行
使用WATCH命令監(jiān)視數(shù)據(jù)庫鍵:redisDb有一個watched_keys字典,key是某個被監(jiān)視的數(shù)據(jù)的key,值是一個鏈表.記錄了所有監(jiān)視這個數(shù)據(jù)的客戶端。
監(jiān)視機制的觸發(fā):當修改數(shù)據(jù)后,監(jiān)視這個數(shù)據(jù)的客戶端的flags置為REDIS_DIRTY_CAS
-
事務執(zhí)行:RedisClient向服務器端發(fā)送exec命令,服務器判斷RedisClient的flags,如果為REDIS_DIRTY_CAS,則清空事務隊列。
Watch的執(zhí)行.png
typedef struct redisDb{
// .....
// 正在被WATCH命令監(jiān)視的鍵
dict *watched_keys;
// .....
}redisDb;
Redis的弱事務性
-
Redis運行錯誤
在隊列里正確的命令可以執(zhí)行 (弱事務性)
弱事務性 :
- 在隊列里正確的命令可以執(zhí)行 (非原子操作)
- 不支持回滾
127.0.0.1:6379> multi OK 127.0.0.1:6379> set aa 11 QUEUED 127.0.0.1:6379> sets aa 22 (error) ERR unknown command `sets`, with args beginning with: `aa`, `22`, 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get aa (nil) 127.0.0.1:6379> multi OK 127.0.0.1:6379> set aa 11 QUEUED 127.0.0.1:6379> lpush aa 1 2 3 4 QUEUED 127.0.0.1:6379> exec 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
-
Redis不支持事務回滾(為什么呢)
- 大多數(shù)事務失敗是因為語法錯誤或者類型錯誤,這兩種錯誤,在開發(fā)階段都是可以預見的
- Redis為了性能方面就忽略了事務回滾。 (回滾記錄歷史版本)
Lua腳本
lua是一種輕量小巧的腳本語言,用標準C語言編寫并以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。 Lua應用場景:游戲開發(fā)、獨立應用腳本、Web應用腳本、擴展和數(shù)據(jù)庫插件。
nginx上使用lua 實現(xiàn)高并發(fā)
OpenRestry:一個可伸縮的基于Nginx的Web平臺,是在nginx之上集成了lua模塊的第三方服務器
OpenRestry是一個通過Lua擴展Nginx實現(xiàn)的可伸縮的Web平臺,內(nèi)部集成了大量精良的Lua庫、第三 方模塊以及大多數(shù)的依賴項。 用于方便地搭建能夠處理超高并發(fā)(日活千萬級別)、擴展性極高的動態(tài) Web應用、Web服務和動態(tài)網(wǎng)關。 功能和nginx類似,就是由于支持lua動態(tài)腳本,所以更加靈活。 OpenRestry通過Lua腳本擴展nginx功能,可提供負載均衡、請求路由、安全認證、服務鑒權、流量控 制與日志監(jiān)控等服務。
類似的還有Kong(Api Gateway)、tengine(阿里)
創(chuàng)建并修改lua環(huán)境
可以本地下載上傳到linux,也可以使用curl命令在linux系統(tǒng)中進行在線下載
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
安裝
yum -y install readline-devel ncurses-devel
tar -zxvf lua-5.3.5.tar.gz
#在src目錄下
make linux
或make install
如果報錯,說找不到readline/readline.h, 可以通過yum命令安裝
yum -y install readline-devel ncurses-devel
安裝完以后再
make linux
或make install
最后,直接輸入 lua命令即可進入lua的控制臺
Lua環(huán)境協(xié)作組建
從Redis2.6.0版本開始,通過內(nèi)置的lua編譯/解釋器,可以使用EVAL命令對lua腳本進行求值。
腳本的命令是原子的,RedisServer在執(zhí)行腳本命令中,不允許插入新的命令,腳本的命令可以復制,RedisServer在獲得腳本后不執(zhí)行,生成標識返回,Client根據(jù)標識就可以隨時執(zhí) 行
EVAL命令
通過執(zhí)行redis的eval命令,可以運行一段lua腳本。
EVAL script numkeys key [key ...] arg [arg ...]
命令說明:
script參數(shù):是一段Lua腳本程序,它會被運行在Redis服務器上下文中,這段腳本不必(也不應該)定義為一個Lua函數(shù)。
numkeys參數(shù):用于指定鍵名參數(shù)的個數(shù)。
key [key ...]參數(shù): 從EVAL的第三個參數(shù)開始算起,使用了numkeys個鍵(key),表示在腳本中 所用到的那些Redis鍵(key),這些鍵名參數(shù)可以在Lua中通過全局變量KEYS數(shù)組,用1為基址的形 式訪問( KEYS[1] , KEYS[2] ,以此類推)。
-
arg [arg ...]參數(shù):可以在Lua中通過全局變量ARGV數(shù)組訪問,訪問的形式和KEYS變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
Lua腳本中調用Redis命令
- redis.call():
- 返回值就是redis命令執(zhí)行的返回值
- 如果出錯,則返回錯誤信息,不繼續(xù)執(zhí)行
- redis.pcall():
- 返回值就是redis命令執(zhí)行的返回值
- 如果出錯,則記錄錯誤信息,繼續(xù)執(zhí)行
- 注意事項
- 在腳本中,使用return語句將返回值返回給客戶端,如果沒有return,則返回nil
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 n1 zhaoyun
EVALSHA
EVAL 命令要求你在每次執(zhí)行腳本的時候都發(fā)送一次腳本主體(script body)。Redis 有一個內(nèi)部的緩存機制,因此它不會每次都重新編譯腳本,不過在很多場合,付出無謂的帶寬來傳送腳本主體并不是最佳選擇。為了減少帶寬的消耗, Redis 實現(xiàn)了 EVALSHA 命令,它的作用和 EVAL 一樣,都用于對腳本求值,但它接受的第一個參數(shù)不是腳本,而是腳本的 SHA1 校驗和(sum)
SCRIPT命令
SCRIPT FLUSH :清除所有腳本緩存
SCRIPT EXISTS :根據(jù)給定的腳本校驗和,檢查指定的腳本是否存在于腳本緩存
-
SCRIPT LOAD :將一個腳本裝入腳本緩存,返回SHA1摘要,但并不立即運行它
192.168.24.131:6380> script load "return redis.call('set',KEYS[1],ARGV[1])" "c686f316aaf1eb01d5a4de1b0b63cd233010e63d" 192.168.24.131:6380> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 n2 zhangfei OK 192.168.24.131:6380> get n2 SCRIPT KILL :殺死當前正在運行的腳本
腳本管理命令實現(xiàn)
使用redis-cli直接執(zhí)行l(wèi)ua腳本。
test.lua:
return redis.call('set',KEYS[1],ARGV[1])
./redis-cli -h 127.0.0.1 -p 6379 --eval test.lua name:6 , caocao
#key和value中間的逗號兩邊有空格
list.lua
local key=KEYS[1]
local list=redis.call("lrange",key,0,-1);
return list;
./redis-cli --eval list.lua list
利用Redis整合Lua,主要是為了性能以及事務的原子性。因為redis幫我們提供的事務功能太差。
腳本復制
Redis 傳播 Lua 腳本,在使用主從模式和開啟AOF持久化的前提下: 當執(zhí)行l(wèi)ua腳本時,Redis 服務器有兩種模式:腳本傳播模式和命令傳播模式。
-
腳本傳播模式
腳本傳播模式是 Redis 復制腳本時默認使用的模式 Redis會將被執(zhí)行的腳本及其參數(shù)復制到 AOF 文件以及從服務器里面。 執(zhí)行以下命令:
eval "redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])" 2 n1 n2 zhaoyun1 zhaoyun2那么主服務器將向從服務器發(fā)送完全相同的 eval 命令:
eval "redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])" 2 n1 n2 zhaoyun1 zhaoyun2注意:在這一模式下執(zhí)行的腳本不能有時間、內(nèi)部狀態(tài)、隨機函數(shù)(spop)等。執(zhí)行相同的腳本以及參數(shù) 必須產(chǎn)生相同的效果。在Redis5,也是處于同一個事務中。下面是AOF的展示:
MULTI^M *3^M $3^M set^M $2^M n1^M $8^M zhaoyun1^M *3^M $3^M set^M $2^M n2^M $8^M zhaoyun2^M *1^M $4^M EXEC^M腳本執(zhí)行的操作在一個事務里面。
-
命令傳播模式
處于命令傳播模式的主服務器會將執(zhí)行腳本產(chǎn)生的所有寫命令用事務包裹起來,然后將事務復制到 AOF 文件以及從服務器里面。因為命令傳播模式復制的是寫命令而不是腳本本身,所以即使腳本本身包含時間、內(nèi)部狀態(tài)、隨機函數(shù) 等,主服務器給所有從服務器復制的寫命令仍然是相同的。為了開啟命令傳播模式,用戶在使用腳本執(zhí)行任何寫操作之前,需要先在腳本里面調用以下函數(shù):
redis.replicate_commands()redis.replicate_commands() 只對調用該函數(shù)的腳本有效:在使用命令傳播模式執(zhí)行完當前腳本之后, 服務器將自動切換回默認的腳本傳播模式。如果我們在主服務器執(zhí)行以下命令:
eval "redis.replicate_commands();redis.call('set',KEYS[1],ARGV[1]);redis.call('set',K EYS[2],ARGV[2])" 2 n1 n2 zhaoyun11 zhaoyun22那么主服務器將向從服務器復制以下命令:
MULTI *3 $3 set $2 n1 $9 zhaoyun11 *3 $3 set $2 n2 $9 zhaoyun22 *1 $4 EXEC -
管道(pipeline),事務和腳本(lua)三者的區(qū)別
三者都可以批量執(zhí)行命令
管道無原子性,命令都是獨立的,屬于無狀態(tài)的操作
事務和腳本是有原子性的,其區(qū)別在于腳本可借助Lua語言可在服務器端存儲的便利性定制和簡化操作
腳本的原子性要強于事務,腳本執(zhí)行期間,另外的客戶端 其它任何腳本或者命令都無法執(zhí)行,腳本的執(zhí)行時間應該盡量短,不能太耗時的腳本
慢查詢?nèi)罩?/h4>
我們都知道MySQL有慢查詢?nèi)罩?Redis也有慢查詢?nèi)罩?,可用于監(jiān)視和優(yōu)化查詢
慢查詢設置
在redis.conf中可以配置和慢查詢?nèi)罩鞠嚓P的選項:
#執(zhí)行時間超過多少微秒的命令請求會被記錄到日志上 0 :全記錄 <0 不記錄
slowlog-log-slower-than 10000
#slowlog-max-len 存儲慢查詢?nèi)罩緱l數(shù)
slowlog-max-len 128
Redis使用列表存儲慢查詢?nèi)罩荆捎藐犃蟹绞?FIFO)
- config set的方式可以臨時設置,redis重啟后就無效
- config set slowlog-log-slower-than 微秒 config set slowlog-max-len 條數(shù)
查看日志:slowlog get [n]
127.0.0.1:6379> config set slowlog-log-slower-than 0
OK
127.0.0.1:6379> config set slowlog-max-len 2
OK
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> slowlog get
1) 1) (integer) 3
2) (integer) 1596615097
3) (integer) 8
4) 1) "set"
2) "k2"
3) "v2"
5) "127.0.0.1:59194"
6) ""
2) 1) (integer) 2
2) (integer) 1596615094
3) (integer) 12
4) 1) "set"
2) "k1"
3) "v1"
5) "127.0.0.1:59194"
6) ""
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> slowlog get
1) 1) (integer) 6
2) (integer) 1596615119
3) (integer) 5
4) 1) "get"
2) "k2"
5) "127.0.0.1:59194"
6) ""
2) 1) (integer) 5
2) (integer) 1596615115
3) (integer) 4
4) 1) "get"
2) "k1"
5) "127.0.0.1:59194"
6) ""
慢查詢記錄的保存
在redisServer中保存和慢查詢?nèi)罩鞠嚓P的信息
struct redisServer { // ...
// 下一條慢查詢?nèi)罩镜?ID
long long slowlog_entry_id;
// 保存了所有慢查詢?nèi)罩镜逆湵鞦IFO
FIFO list *slowlog;
// 服務器配置 slowlog-log-slower-than 選項的值
long long slowlog_log_slower_than;
// 服務器配置 slowlog-max-len 選項的值
unsigned long slowlog_max_len;
// ...
};
lowlog 鏈表保存了服務器中的所有慢查詢?nèi)罩荆?鏈表中的每個節(jié)點都保存了一個 slowlogEntry 結 構, 每個 slowlogEntry 結構代表一條慢查詢?nèi)罩尽?/p>
typedef struct slowlogEntry {
// 唯一標識符
long long id;
// 命令執(zhí)行時的時間,格式為 UNIX 時間戳
time_t time;
// 執(zhí)行命令消耗的時間,以微秒為單位
long long duration;
// 命令與命令參數(shù)
robj **argv;
// 命令與命令參數(shù)的數(shù)量
int argc;
} slowlogEntry;
慢查詢?nèi)罩镜拈営[&刪除
初始化日志列表
void slowlogInit(void) {
/* 創(chuàng)建一個list列表 */
server.slowlog = listCreate();
/* 日志ID從0開始 */
server.slowlog_entry_id = 0;
/* 指定慢查詢?nèi)罩緇ist空間的釋放方法 */
listSetFreeMethod(server.slowlog,slowlogFreeEntry);
}
獲得慢查詢?nèi)罩居涗?slowlog get [n]
def SLOWLOG_GET(number=None):
# 用戶沒有給定 number 參數(shù)
# 那么打印服務器包含的全部慢查詢?nèi)罩?if number is None:
number = SLOWLOG_LEN()
# 遍歷服務器中的慢查詢?nèi)罩? for log in redisServer.slowlog:
if number <= 0:
# 打印的日志數(shù)量已經(jīng)足夠,跳出循環(huán)
break
else:
# 繼續(xù)打印,將計數(shù)器的值減一
number -= 1
# 打印日志
printLog(log)
查看日志數(shù)量的 slowlog len
def SLOWLOG_LEN():
# slowlog 鏈表的長度就是慢查詢?nèi)罩镜臈l目數(shù)量
return len(redisServer.slowlog)
清除日志 slowlog reset
def SLOWLOG_RESET():
# 遍歷服務器中的所有慢查詢?nèi)罩? for log in redisServer.slowlog:
# 刪除日志
deleteLog(log)
添加日志的實現(xiàn)
在每次執(zhí)行命令的之前和之后, 程序都會記錄微秒格式的當前 UNIX 時間戳, 這兩個時間戳之間的差就是服務器執(zhí)行命令所耗費的時長, 服務器會將這個時長作為參數(shù)之一傳給slowlogPushEntryIfNeeded 函數(shù), 而 slowlogPushEntryIfNeeded 函數(shù)則負責檢查是否需要為這次執(zhí)行的命令創(chuàng)建慢查詢?nèi)罩?/p>
// 記錄執(zhí)行命令前的時間
before = unixtime_now_in_us()
//執(zhí)行命令
execute_command(argv, argc, client)
//記錄執(zhí)行命令后的時間
after = unixtime_now_in_us()
// 檢查是否需要創(chuàng)建新的慢查詢?nèi)罩?
slowlogPushEntryIfNeeded(argv, argc, before-after)
void slowlogPushEntryIfNeeded(robj **argv, int argc, long long duration) {
//Slowlog disabled 負數(shù)表示禁用
if (server.slowlog_log_slower_than < 0) return;
// 如果執(zhí)行時間 > 指定閾值
if (duration >= server.slowlog_log_slower_than)
//創(chuàng)建一個slowlogEntry對象,添加到列表首部
listAddNodeHead(server.slowlog,slowlogCreateEntry(argv,argc,duration));
//如果列表長度 > 指定長度
while (listLength(server.slowlog) > server.slowlog_max_len)
//移除列表尾部元素
listDelNode(server.slowlog,listLast(server.slowlog));
}
slowlogPushEntryIfNeeded 函數(shù)的作用有兩個:
- 檢查命令的執(zhí)行時長是否超過 slowlog-log-slower-than 選項所設置的時間, 如果是的話, 就為命令創(chuàng)建一個新的日志, 并將新日志添加到 slowlog 鏈表的表頭。
- 檢查慢查詢?nèi)罩镜拈L度是否超過 選項所設置的長度, 如果是的話, 那么將多 出來的日志從 slowlog 鏈表中刪除掉。
慢日志查詢&處理
使用slowlog get 可以獲得執(zhí)行較慢的redis命令,針對該命令可以進行優(yōu)化:
- 盡量使用短的key,對于value有些也可精簡,能使用int就int。
- 避免使用keys *、hgetall等全量操作。
- 減少大key的存取,打散為小key 100K以上
- 將rdb改為aof模式,rdb fork 子進程 數(shù)據(jù)量過大主進程阻塞 redis性能大幅下降;關閉持久化 , (適合于數(shù)據(jù)量較小,有固定數(shù)據(jù)源)
- 想要一次添加多條數(shù)據(jù)的時候可以使用管道
- 盡可能地使用哈希存儲
- 盡量限制下redis使用的內(nèi)存大小,這樣可以避免redis使用swap分區(qū)或者出現(xiàn)OOM錯誤 內(nèi)存與硬盤的swap
監(jiān)視器
Redis客戶端通過執(zhí)行MONITOR命令可以將自己變?yōu)橐粋€監(jiān)視器,實時地接受并打印出服務器當前處理 的命令請求的相關信息。此時,當其他客戶端向服務器發(fā)送一條命令請求時,服務器除了會處理這條命令請求之外,還會將這條 命令請求的信息發(fā)送給所有監(jiān)視器。

Redis客戶端1
127.0.0.1:6379> monitor
OK
1589706136.030138 [0 127.0.0.1:42907] "COMMAND"
1589706145.763523 [0 127.0.0.1:42907] "set" "name:10" "zhaoyun"
1589706163.756312 [0 127.0.0.1:42907] "get" "name:10"
Redis客戶端2
127.0.0.1:6379>
127.0.0.1:6379> set name:10 zhaoyun
OK
127.0.0.1:6379> get name:10
"zhaoyun"
實現(xiàn)監(jiān)視器
redisServer 維護一個 monitors 的鏈表,記錄自己的監(jiān)視器,每次收到 MONITOR 命令之后,將客戶端追加到鏈表尾。
void monitorCommand(redisClient *c) {
//ignore MONITOR if already slave or in monitor mode
if (c->flags & REDIS_SLAVE) return;
c->flags |= (REDIS_SLAVE|REDIS_MONITOR);
listAddNodeTail(server.monitors,c);
addReply(c,shared.ok); //回復OK
}
向監(jiān)視器發(fā)送命令信息
// call() 函數(shù)是執(zhí)行命令的核心函數(shù),這里只看監(jiān)視器部分
/*src/redis.c/call*/
/* Call() is the core of Redis execution of a command */
void call(redisClient *c, int flags) {
long long dirty, start = ustime(), duration;
int client_old_flags = c->flags;
/* Sent the command to clients in MONITOR mode, only if the commands are * not generated from reading an AOF. */
cif (listLength(server.monitors) &&!server.loading &&
!(c->cmd->flags & REDIS_CMD_SKIP_MONITOR))
{
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
} ......
}
call 主要調用了 replicationFeedMonitors ,這個函數(shù)的作用就是將命令打包為協(xié)議,發(fā)送給監(jiān)視器。
Redis監(jiān)控平臺
grafana、prometheus以及redis_exporter。
Grafana 是一個開箱即用的可視化工具,具有功能齊全的度量儀表盤和圖形編輯器,有靈活豐富的圖形化選項,可以混合多種風格,支持多個數(shù)據(jù)源特點。
Prometheus是一個開源的服務監(jiān)控系統(tǒng),它通過HTTP協(xié)議從遠程的機器收集數(shù)據(jù)并存儲在本地的時序數(shù)據(jù)庫上。
redis_exporter為Prometheus提供了redis指標的導出,配合Prometheus以及grafana進行可視化及監(jiān)控。
