1、前言
從我第一次使用Redis來幫助加速算法運行速度開始,就把Redis應用在了各個項目中,每次帶來的體驗都很是得好,python多進程+Redis的使用幫助我把單進程運行十幾個小時的程序加速到了只須要10分鐘左右,也幫助我把原本須要運行十幾分鐘的項目加速到了幾十秒就能運行結束,同時我也喜歡Redis項目自己的小巧和精緻。因此在這裏計劃寫些關於Redis的介紹,計劃總共寫兩篇,第一篇主要介紹Redis的總體的一些設計和思想,第二篇會主要介紹Redis集羣的一些研究,但願能幫助你們熟悉認識Redis,並鼓勵在你的項目中能嘗試使用Redis。本篇主要會涉及到以下內容:
• Redis是什麼
• 爲何Redis速度可以這麼快
• Redis支持寫入的數據結構都有哪些及其底層實現方式是什麼
• 內存資源稀缺,可以存儲的鍵值數目有限,當Redis鍵值存不下時,該如何淘汰掉已有的鍵
• Redis進程在內存中存儲數據,若是Redis進程崩潰了,進程中的數據會丟失,那麼Redis如何利用持久化來保證數據的安全性
• Redis的python程序實例及一些經常使用的高效使用手段
2、Redis是什麼
Redis的全稱是REmote DIctionary Server,是一個高效的內存鍵值數據庫,相比較咱們常規使用的Mysql、MongoDB等數據庫,Redis的最大特色在於數據讀寫所有在內存中進行,進而帶來極大的效率優點。相比較其餘的內存鍵值存儲系統如Memcached, Redis支持更多的數據結構,提高了使用的易用性。同時Redis採用典型的CS架構, 而且有着很是豐富的不一樣語言客戶端支持,本篇文章的最後也會向你們介紹同步和異步模式下的兩個python語言的Redis客戶端使用。
Redis採用CS架構
3、Redis爲何這麼快
Redis最大的好處就是快,Redis爲何能作到這麼快呢?主要的緣由有三點
• 數據讀寫都在內存中完成。從下圖中咱們能夠看出,即便使用SSD,內存的讀寫速度要比外存的數據的讀寫速度快1000倍左右,若是你的電腦還沒裝上SSD,仍是機械硬盤,那內存的讀寫速度比硬盤的讀寫速度就要快100000倍,那麼基於內存的數據庫的讀寫速度優點天然就是巨大的。
不一樣存儲層次的訪問速度對比
• 單線程請求處理,這個主要是實現上的選擇。也許同窗會有疑惑,爲何不採用多線程進行並行讀寫呢?這裏主要的緣由仍然是Redis基於內存讀寫,多線程並行對數據讀取的確能帶來好處,可是一樣帶來了數據寫入時鎖的開銷以及線程切換的開銷。再大量的寫入狀況下,過多的鎖帶來的時間消耗比多線程帶來的多核利用優點更大。
• I/O多路複用技術。I/O多路複用咱們又稱之爲事件驅動,Redis基於epoll等技術實現了網絡部分響應的同步非阻塞I/O模式。Redis的I/O主要集中在了讀寫socket上,同步阻塞下,向客戶端發送數據的時候,Redis須要一直等到對應客戶端的socket可寫纔會去寫,直到寫完了再服務下一個請求,使用epoll等系統調用,把socket是否可讀寫的狀態監控交給了操做系統,即Redis只會在操做系統告知其可讀或者可寫的socket文件的時候採起讀寫,進而節省了等IO的時間。關於epoll的具體介紹能夠參考這一篇文章。
以上三點是Redis爲何這麼快的緣由,內存讀寫是最主要的,其餘兩個技術選型對此也有所幫助。
4、Redis支持的數據結構
咱們要把數據存到內存裏面,怎麼存呢?理論上來說,內存KV數據存儲其實只須要支持字符串數據存取就能支持全部的數據類型存儲了,至於列表、字典的存儲,咱們只須要將數據進行序列化就行。缺點就是用戶每次要修改數據都要得到全部的數據,修改結束以後還得把全部的數據再傳回去,這樣不但增長了每次網絡的傳輸數據體積,並且使用體驗也不是很好,由於須要用戶本身來解析數據,事實上這就是Memcached的作法。Redis爲了提升易用性,支持了更加豐富的數據結構,最經常使用的即是String、List、Hash、Set、Sorted Set五種。接下來咱們一一介紹五種數據結構,主要介紹其特色和底層實現,這樣咱們就好估計每種數據結構的操做時間複雜度。
String
String和咱們常規理解的字符串基本一致,主要存儲序列化後的字符串,支持寫入原生字符串也支持寫入數字類型。String的存取複雜度均爲O(1)。主要支持的操做以下表
命令 含義
SET 設置鍵值
GET 得到給定鍵的值
DEL 刪除給定的鍵
List
List即爲列表,List在Redis底層採用的是雙向鏈表實現的,因此咱們會發現Redis的List操做命令有左右之分,好比LPUSH、RPUSH,實際上就是雙端列表左右兩端的存取。對於列表的端點插入和查詢時間複雜度爲O(1), 對於中間某個index的位置的值的獲取以及對於index處於[start, end]的連續多個值的讀取就是O(n)的複雜度(n爲列表的長度),在咱們的項目中,咱們用List來存儲疾病列表,來幫助實現用戶搜索疾病時的即時自動補全。列表的主要命令以下:
命令 含義
LPUSH/RPUSH 向列表的左端/右端插入數據
LPOP/RPOP 從列表的左端/右端刪除數據
LRANGE/RANGE 去除從左/右端開始計數的位置處於給定[start, end]之間的全部value
LINDEX/RINDEX 刪除從左/右端開始計數的第INDEX個值
Hash
Hash能夠理解爲咱們常規使用的字典數據結構,Redis採用散列表來實現Hash, 一個Hash結構裏面能夠存在不少的key和value,Hash是Redis比較推薦使用的一種數據結構,聽說內存使用會更好,具體我尚未研究。在咱們的項目裏,咱們主要用Hash保存用戶的token信息來幫助快速驗證用戶是否已登陸。Hash中的鍵值存取效率能夠認爲是O(1),Hash結構操做的主要命令以下表
命令 含義
HSET 向Hash中添加k:v
HGET 獲取Hash中的給定key的值
HKEYS 獲取Hash中全部的key
Sort
Sort是集合,知足集合肯定性、無序性、惟一性三個性質,能夠用來進行元素的去重操做。集合的底層實現仍然採用散列表,因此單個元素的存取能夠認爲是O(1)的時間複雜度,同時Redis支持對不一樣的集合的交併等計算,集合的操做命令主要以下
命令 含義
SADD 向集合中添加元素
SISMEMBER 判斷鍵是否在集合中
SMEMBERS 獲取集合中全部的鍵
SREM 刪除集合中的給定的鍵
Sorted Set
Sorted Set是有序集合,知足集合惟一性的要求,同時也知足有序的性質。向Sorted Set中插入元素的時候須要同時指定一個Score,用於做爲排序的標準,以下面的這條命令,咱們向知乎熱榜這個有序集合中插入一個文章的題目及其閱讀量,經過有知乎熱榜這個有序結合咱們能夠方便的獲得天天排名靠前的文章題目及其對應的閱讀量。 Sorted Set的底層實現採用的是Skip List, 因此其單個元素的存取效率能夠近似認爲是O(1)的。有序集合的操做命令主要以下:
ZADD 知乎熱榜 2000 如何看待xxxxx
命令 含義 示例
ZADD 向有序集合中添加元素 ZADD 知乎熱榜 2000 如何看待xxxx
ZREM 刪除集合中的元素 ZREM 知乎熱榜 如何看待xxxx
ZRANGE 獲取有序集合中排名處於[start, end]之間的值 ZRANGE 知乎熱榜 0 10
ZSCORE 獲取集合中給定鍵的score ZSCORE 知乎熱榜 如何看待xxxx
5、Redis鍵淘汰策略
前文提過,Redis的全部數據是存儲在內存中的,可是內存自己就是稀缺資源,咱們常規使用的筆記本內存只有8G或者16G,並且這個內存是給全部的進程使用的,Redis做爲咱們運行的其中一個進程咱們通常會限制Redis的使用內存上限,好比2G,不然Redis就會把可用內存耗光。2G實際上能存儲的鍵值是有限的,那麼若是用戶把Redis的存儲存滿了該怎麼辦呢?就像咱們把家裏的冰箱都裝滿了,再想裝東西就得扔掉一部分不吃的東西或者過時的東西同樣,Redis也會選擇淘汰掉一些鍵來爲新的鍵提供空間。同時Redis支持用戶給鍵值設置過時時間,若是檢查到某些鍵過時了,就刪除掉鍵來空餘出空間。爲了方便管理,Redis把全部設置了過時時間的鍵放到一個單獨的列表裏面進行維護。這裏咱們主要介紹三類策略:
不淘汰策略
第一條淘汰鍵的策略就是不淘汰哈哈,實際上是代表Redis不主動清除鍵,清除鍵的操做所有交給用戶來決定,若是用戶始終不清除鍵,當Redis被寫滿了後,用戶在往裏面寫Redis就會報錯,拒絕寫入數據。這種策略叫noeviction。
隨機淘汰
隨機抽樣淘汰即Redis隨機選取一些鍵而後進行刪除,這樣帶來的問題是用戶也不知道哪些鍵被刪除了,可能用戶吃着火鍋唱着歌,回頭一看,本身的數據沒了!那顯然是很糟糕的,但Redis提供了這樣一個選項,用不用那天然是用戶的選擇問題了。根據隨機抽樣的集合不一樣又細分爲兩個策略,從全部的鍵中隨機抽取就是allkeys-random, 從只設置了過時時間的鍵集合中進行抽取,就是volatile-random
LRU策略
LRU策略就是淘汰掉最不經常使用的鍵,每次用戶訪問某個鍵的時候,Redis就會記錄這個鍵的訪問時間,若是一個鍵距離上次訪問已經過久沒有被訪問到了,那麼Redis就認爲這個鍵用戶用不上了,就會把鍵清除掉。按照標準的LRU算法,咱們應該統計全部鍵中最不經常使用的鍵,而後淘汰掉他,可是Redis是單線程響應用戶請求的,不能每次都遍歷全部的鍵來進行檢查,不然就會嚴重的影響到服務的響應。因此Redis採用一種隨機抽樣的方法。即每次隨機抽取K個鍵值,而後淘汰掉這K個鍵中上次訪問時間最先的的那個鍵。一樣,針對隨機收取的集合不一樣又細分爲兩個策略,從全部的鍵中進行抽取,就是allkeys-lru策略,從只設置了過時時間的鍵集合中進行抽取,就是volatile-lru策略。volatile-lru策略是比較推薦的一種策略。關於LRU的策略,Redis的源碼實現以下,我加了註釋,還比較易懂
......python
/* 若是選擇了volatile-lru 或者 allkeys-lru 策略 */ else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU || server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) { /*每次隨機抽取maxmeory_samples個元素進行檢查淘汰,默認設置爲3*/ for (k = 0; k < server.maxmemory_samples; k++) { sds thiskey; long thisval; robj *o; /*隨機抽取一個鍵*/ de = dictGetRandomKey(dict); thiskey = dictGetKey(de); /*若是用戶設置的是volatile-lru,則從設置了有效期的集合中進行抽樣*/ if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) de = dictFind(db->dict, thiskey); o = dictGetVal(de); thisval = estimateObjectIdleTime(o); /* 找到距離上次訪問過去時間最久的鍵*/ if (bestkey == NULL || thisval > bestval) { bestkey = thiskey; bestval = thisval; } } } ......
6、Redis持久化策略
Redis是把數據存儲在本身進程的內存中,可是若是Redis進程掛了或者說電腦斷電了,那麼存儲的數據就所有丟失了。爲了保證數據的安全性,就須要把數據從內存的數據備份到硬盤上,這就是持久化操做。這樣即便內存中的數據丟失了,那麼也能夠從硬盤上把數據恢復出來。Redis提供兩種持久化策略:RDB持久化和AOF持久化,不要被這兩個名字嚇到,RDB,AOF只是兩種持久化文件的後綴名,並非什麼神奇的策略。都比較容易懂,下面一一介紹。
RDB持久化
RDB持久化就是快照持久化,即按期把內存中的數據所有拷貝保存到文件中。咱們前面提到Redis是單線程響應用戶需求的,若是把持久化這樣涉及到大量IO的操做也放到這個線程中,會嚴重影響服務的響應。因而Redis採用fork一個子進程出來進行持久化。可是咱們都知道,fork出來的子進程會拷貝父進程全部的數據,這樣理論上當Redis要持久化2G的內存數據的時候,子進程也會佔據幾乎2G的內存,那麼Redis相關的進程內存佔用就會達到4G左右,這在數據比較小的時候還不嚴重,可是好比你的電腦內存是8G, 目前備份的Redis的數據自己體積是5G,那麼按照上面的計算備份必定是沒法進行的,所幸在Unix類操做系統上面,作了以下的優化:在剛開始的時候父子進程共享相同的內存,直到父進程或者子進程進行內存的寫入後,對被寫入的內存共享才結束。這樣就會減小Redis持久化時對內存的消耗。
AOF持久化
AOF(AppendOnlyFile)持久化就是Redis把每次的用戶寫操做日誌append到一個文件中,若是數據丟失了,那麼按照AOF文件的操做順序再進行操做一遍就能夠恢復數據,並且這樣每次咱們都只須要寫一小部分數據到文件中。可是這樣會帶來一個什麼問題呢?因爲程序一直在運行,因此不停的會往AOF文件中添加寫的操做日誌,這樣終有一天AOF文件體積會大到不可想象。因此就又有一個操做叫AOF重寫用於刪除掉冗餘的命令,好比用戶對同一個key執行100遍SET操做,若是不AOF重寫,那麼AOF文件中就會有100條SET記錄,數據恢復的時候也須要操做100次SET,但實際上只有最後一條SET命令是真正有意義的,因此AOF重寫就會把前99條SET命令刪除掉,只保留最後一條SET命令,這樣不只文件內存儲的內容就變少了,Redis恢復數據的速度也加快了。
除了上面兩條策略,Redis還支持主從備份,這又是一塊比較大的內容,限於篇幅,咱們將主從備份放到第二篇的Redis集羣中介紹。
7、talk is cheap, show me the code
redis-py和aredis
又到了喜聞樂見的代碼部分了。這部分主要介紹兩個python的Redis客戶端,redis-py和aredis前者是同步redis客戶端,後者是異步redis客戶端。aredis就是在redis-py的基礎上利用了協程的技術來重寫了接口,試圖省去客戶端等待服務器結果的時間。若是你是本地機器使用Redis,那麼使用前者就能很好的知足你的需求,若是你使用的遠端的Redis服務器並且網絡還比較差的話,aredis也許會有些幫助。我以前嘗試使用aredis客戶端與本地運行的Redis服務器搭配使用,發現性能降低了不少,主要的緣由就是由於本地Redis服務器網絡延遲幾乎爲0,但過多的協程切換反而帶來了高昂的開銷。我使用redis-py客戶端,處理完須要288s, 用aredis客戶端處理完須要340s,後來我重寫了客戶端的一些接口,把一些協程的接口改爲了普通的函數接口,減小了協程數目,運行結束爲330s,快了10s。redis
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('foo', 'bar')
True
r.get('foo')
'bar'
import aredis
import asyncio
loop = asyncio.get_event_loop()
r = aredis.StrictRedis(loop=loop)
async def set_key(key, value):算法
await r.set(key, value) return
流水線
Redis客戶端和服務器的請求響應過程以下圖所示,客戶端發送一個命令,等待服務器返回結果以後再提交下一個命令。若是網絡狀況比較差,咱們就會須要花許多的時間來等待服務器的響應。一種解決方案就是利用上文提到的aredis,能夠在等待響應的同時切換協程作點其餘的計算。另外一種解決方案就是把全部的命令打包一塊兒發送,而後等服務器計算完了以後把結果一塊兒返回來,這就是流水線的概念。
逐個命令提交(圖片出處:https://blog.csdn.net/w1lgy/a...)
使用pipeline,進行多個命令一塊兒提交(圖片出處:https://blog.csdn.net/w1lgy/a...)
代碼以下:
import redis
r = redis.Redis()sql
pipeline = r.pipeline()
pipeline.set("thu", "No.1")
pipeline.set("xxu", "No.2")數據庫
pipeline.execute()
8、結語
本篇從Redis的單線程運行、支持的數據結構、到鍵驅逐策略以及持久化策略幾個方面進行介紹,試圖給讀者一個Redis的全貌,這樣使用的時候能對命令有更加清晰的瞭解,而不僅是拘泥於客戶端提供的接口。鼓勵你們能嘗試在本身的項目中使用Redis,相信我,它會給你從未有過的船新體驗hh。下篇會主要研究Redis集羣的相關內容,若是感興趣的話,能夠考慮訂閱下個人專欄hh~。安全