為什麼使用Redis
- 2019 年 10 月 3 日
- 筆記
Redis是什麼?
Redis (REmote DIctionary Server)是一個開源(BSD許可),記憶體存儲的數據結構伺服器,可用作資料庫,高速快取和消息隊列,是一個高性能的key-value資料庫。
Redis與其他key-value快取產品有以下三個特點:
- Redis支援數據的持久化,可以將記憶體中的數據保持在磁碟中,重啟的時候可以再次載入進行使用。
- Redis不僅支援簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
- Redis支援數據的備份,即master-slave模式的數據備份。
為什麼要用Redis
- 性能極高 – Redis讀的速度是110000次/s,寫的速度是81000次/s 。
- 豐富的數據類型 – Redis支援Strings, Lists, Hashes, Sets 及 Ordered Sets 數據類型操作。
- 原子 – Redis的所有操作都是原子性的,即要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支援事務,即原子性,通過MULTI和EXEC指令包起來。
- 豐富的特性 – Redis還支援publish/subscribe, key過期等特性。
Redis的數據類型及使用場景
Redis支援五種數據類型:string(字元串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。每種數據類型有其適合的使用場景,下面具體介紹.
String(字元串)
string 是 redis 最基本的類型,你可以理解成與 Memcached 一模一樣的類型,一個 key 對應一個 value。string 類型是二進位安全的。意思是 redis 的 string 可以包含任何數據。比如jpg圖片或者序列化的對象。string 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB。
使用方法
SET key value 設置指定 key 的值 GET key 獲取指定 key 的值。 SETEX key seconds value 將值 value 關聯到 key ,並將 key 的過期時間設為 seconds (以秒為單位)。
使用場景
1.會話快取
用戶登錄系統後,使用Redis保存用戶的Session資訊,每次用戶查詢登錄資訊都直接從Redis中獲取。
2.計數器
- 比如登錄系統會限制密碼錯誤次數,當一個用戶在一定時間內連續輸入密碼錯誤,就不能登錄,需要一段時間後才能登錄,我們可以使用redis,把username作為key,錯誤的次數作為value,同時設置過期時間即可.
- 手機驗證碼限收到簡訊的次數
- 統計其他計數
3.定時器
redis的key可以設置過期時間,我們基於此特性設置一個定時器.
4.對象
我們把對象序列化後,可以使用redis保存該對象,然後在獲取對象資訊的時候,反序列化value
5.分散式鎖
redis提供了setnx()方法,即SET IF NOT EXIST,只有在key不存在的時候才能set成功,這就意味著同一時間多個請求只有一個請求能保存成功,這塊的可以自行搜索redis的分散式鎖
Hash(哈希)
Redis hash 是一個鍵值(key=>value)對集合,即程式語言中的Map類型.
Redis hash 是一個 string 類型的 field 和 value 的映射表.
使用方法
HSET key field value 將哈希表 key 中的欄位 field 的值設為 value 。 HGET key field 獲取存儲在哈希表中指定欄位的值。 HKEYS key 獲取所有哈希表中的欄位 HMSET key field1 value1 [field2 value2 ] 同時將多個 field-value (域-值)對設置到哈希表 key 中。
使用場景
hash 特別適合用於存儲對象,並且可以像資料庫中update一個屬性一樣只修改某一項屬性值(Memcached中需要取出整個字元串反序列化成對象修改完再序列化存回去)
List(列表)
Redis 列表是簡單的字元串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)。
使用方法
LPUSHX key value 將一個值插入到已存在的列表頭部 LPUSH key value1 [value2] 將一個或多個值插入到列表頭部 LPOP key 移出並獲取列表的第一個元素 BLPOP key1 [key2 ] timeout 移出並獲取列表的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止。
使用場景
1.消息隊列
Redis的lpush+brpop命令組合即可實現阻塞隊列,生產者客戶端使用lrpush從列表左側插入元素,多個消費者客戶端使用brpop命令阻塞式的"搶"列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。
2.類目/文章/活動等列表
最常見的就是各個系統的首頁數據,包括電商系統的商品類目,拼團活動列表,部落格園的首頁文章列表等
3.其他
根據push和pop的方式不同,有以下組合方式
lpush + lpop = Stack(棧) lpush + rpop = Queue(隊列) lpush + ltrim = Capped Collection(有限集合) lpush + brpop = Message Queue(消息隊列)
Set(集合)
Redis 的 Set 是 String 類型的無序集合。集合成員是唯一的,這就意味著集合中不能出現重複的數據。
Redis 中集合是通過哈希表實現的,所以添加,刪除,查找的複雜度都是 O(1)。
使用方法
SADD key member1 [member2] 向集合添加一個或多個成員 SDIFF key1 [key2] 返回給定所有集合的差集 SINTER key1 [key2] 返回給定所有集合的交集 SMEMBERS key 返回集合中的所有成員
使用場景
1.標籤(tag)
比如在點餐評價系統中,用戶給某商家評價,商家會有多個評價標籤,但是不會重複的,如果100萬人給某商家評價打了標籤,如果使用MySQL資料庫獲取大數據量去重後的評價標籤,會影響資料庫的性能和系統的並發量.
2.相同點/異同點
利用交集、並集、差集等操作,可以計算兩個人的共同喜好,全部的喜好,自己獨有的喜好等功能。
zset(sorted set:有序集合)
Redis 有序集合和集合一樣也是string類型元素的集合,且不允許重複的成員。
不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
有序集合的成員是唯一的,但分數(score)卻可以重複。
使用方法
ZADD key score1 member1 [score2 member2] 向有序集合添加一個或多個成員,或者更新已存在成員的分數 ZCARD key 獲取有序集合的成員數 ZREM key member [member ...] 移除有序集合中的一個或多個成員
使用場景
1.排行榜
例如部落格園需要對用戶發表的文章做排行榜,榜單的維度可能是多個方面的:按照時間、按照點贊數、按照熱度,瀏覽數等
Redis持久化
Redis 提供了不同級別的持久化方式:
- RDB持久化,該方式能夠在指定的時間間隔能對數據進行快照存儲.
- AOF持久化,該方式記錄每次對伺服器寫的操作,當伺服器重啟的時候會重新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操作到文件末尾.Redis還能對AOF文件進行後台重寫,使得AOF文件的體積不至於過大.
- 不持久化,如果你只希望數據在伺服器運行的時候存在,你也可以不使用任何持久化方式.
- RDB+AOF模式, 在這種情況下, 當redis重啟的時候會優先載入AOF文件來恢復原始的數據,因為在通常情況下AOF文件保存的數據集要比RDB文件保存的數據集要完整.
RDB的優點
- RDB是一個非常緊湊的文件,它保存了某個時間點得數據集,非常適用於數據集的備份,比如你可以在每個小時報保存一下過去24小時內的數據,同時每天保存過去30天的數據,這樣即使出了問題你也可以根據需求恢復到不同版本的數據集.
- 與AOF相比,在恢復大的數據集的時候,RDB方式會更快一些.
RDB的缺點
- 如果你希望在redis意外停止工作(例如電源中斷)的情況下丟失的數據最少的話,那麼RDB不適合你.雖然你可以配置不同的save時間點(例如每隔5分鐘並且對數據集有100個寫的操作),是Redis要完整的保存整個數據集是一個比較繁重的工作,你通常會每隔5分鐘或者更久做一次完整的保存,萬一在Redis意外宕機,你可能會丟失幾分鐘的數據.
- RDB 需要經常fork子進程來保存數據集到硬碟上,當數據集比較大的時候,fork的過程是非常耗時的,可能會導致Redis在一些毫秒級內不能響應客戶端的請求.
AOF優點
- 使用AOF 會讓你的Redis更加耐久: 你可以使用不同的fsync策略:無fsync,每秒fsync,每次寫的時候fsync.使用默認的每秒fsync策略,Redis的性能依然很好(fsync是由後台執行緒進行處理的,主執行緒會儘力處理客戶端請求),一旦出現故障,你最多丟失1秒的數據.
AOF缺點
- 對於相同的數據集來說,AOF 文件的體積通常要大於 RDB 文件的體積。
- 根據所使用的 fsync 策略,AOF 的速度可能會慢於 RDB 。
如何選擇持久化方式
如果你非常關心你的數據, 但仍然可以承受數分鐘以內的數據丟失, 那麼你可以只使用 RDB 持久化。
有很多用戶都只使用 AOF 持久化,但我們並不推薦這種方式: 因為定時生成 RDB 快照(snapshot)非常便於進行資料庫備份, 並且 RDB 恢複數據集的速度也要比 AOF 恢復的速度要快
一般來說,如果想達到足以媲美 PostgreSQL 的數據安全性, 你應該同時使用兩種持久化功能.
使用Redis出現的問題
在一個高頻訪問的應用系統中,每次用戶的請求需要去DB中獲取數據,會對資料庫造成很大的壓力、容易導致資料庫的奔潰。所以才會出現快取來分擔一部分的資料庫的壓力。 但是使用快取也帶來了一系列問題:
1.快取一致性問題
當數據時效性要求很高時,需要保證快取中的數據與資料庫中的保持一致,而且需要保證快取節點和副本中的數據也保持一致,不能出現差異現象。這就比較依賴快取的過期和更新策略。一般會在數據發生更改的時,主動移除對應的快取。 所以需要通過事物機制來保證快取的一致性。
2.快取雪崩問題
在高並發場景下,有多個請求去共同請求一份相同的業務數據。有可能多個請求先去從快取中獲取數據、獲取不到的並發的去從資料庫獲取數據,對後端資料庫造成極大的衝擊,甚至導致 「雪崩」現象。
- 方案一: 可以做一個隨機的等待、錯峰去訪問快取的資訊。這樣就能保證同一時刻高並發的訪問、經過時間離散之後只有小部分的請求訪問資料庫、大部分的請求去命中快取。
- 方案二: 可以按照比例限制有部分數據直接訪問資料庫然後更新快取、大部分的數據直接請求快取。
按照實際的場景去做判斷例如 1%的場景直接訪問資料庫,99%的可以通過快取獲取到數據。
3.快取擊穿的問題
在系統設計的的時候預期是通過快取來減輕資料庫的壓力、防止數據奔潰的情況。在某個實際發生的場景中、大量的請求並沒有命中快取而導致了大量請求達到資料庫、從而導致資料庫有巨大衝擊和壓力。
3.1快取中沒有數據
在某個大促活動中有大量的熱點數據,互動一開始需要訪問這些數據。由於活動開始的時候洪峰流量到來,所有的請求快取、快取直接擊穿,訪問資料庫導致資料庫直接cpu 100%,業務系統直接奔潰。
對於這種場景可以提前對數據進行預熱,開活動開始前先將數據推送到快取中。
3.2快取集中失效
由於我們在快取使用的過程中會設置快取的失效時間、如果設置的不合理可能會導致數據集中失效的情況。由於快取集中失效會導致系統快取穿透、在同一時刻高並發的訪問數據,造成數據雪崩。
解決這種場景的可以將失效的時間由固定值+隨機值來構成。EXPIRETIME=FIXTIME+RUND_TIME 例如你想保證整個EXPIRETIME是5S 左右,可以 通過EXPIRETIME=4000+Random(1000)
Redis的過期策略
通常Redis keys創建時沒有設置相關過期時間,他們會一直存在,除非使用顯示的命令移除,例如,使用DEL命令。
EXPIRE一類命令能關聯到一個有額外記憶體開銷的key。當key執行過期操作時,Redis會確保按照規定時間刪除他們。
key的過期時間和永久有效性可以通過EXPIRE和PERSIST命令(或者其他相關命令)來進行更新或者刪除過期時間。
Redis keys過期有兩種方式:定期刪除和惰性刪除。
1.定期刪除
定時刪除,用一個定時器來負責監視key,當key過期則自動刪除key。
雖然記憶體及時釋放,但是十分消耗CPU資源。在大並發請求下,會影響redis的性能.
2.惰性刪除
客戶端嘗試訪問key時,key會被發現並主動的過期. 但是這樣是不夠的,因為有些過期的keys,永遠不會訪問他們,那麼他們就永遠不會被刪除,而佔用記憶體,導致redis記憶體被過期的key佔用.
3.定期刪除+惰性刪除
redis默認每100ms檢查一次,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每100ms將所有的key檢查一次,而是隨機抽取20個keys進行過期檢查,同時刪除已經過期的keys,如果有多於25%的keys過期,重複抽取。直到過期的keys的百分比低於25%,這意味著,在任何給定的時刻,最多會清除1/4的過期keys
那麼問題來了,採用定期刪除+惰性刪除就能保證過期的key會全部刪除掉么?
記憶體淘汰機制
如果定期刪除沒刪除key。然後也沒去請求key,也就是說惰性刪除也沒生效。這樣,redis的記憶體會越來越高。那麼就應該採用記憶體淘汰機制。
在redis.conf中有配置
maxmemory-policy volatile-lru
記憶體淘汰策略如下:
- noeviction:當記憶體不足以容納新寫入數據時,新寫入操作會報錯,不建議使用
- allkeys-lru:當記憶體不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key。推薦使用
- allkeys-random:當記憶體不足以容納新寫入數據時,在鍵空間中,隨機移除某個key
- volatile-lru:當記憶體不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key
- volatile-random:當記憶體不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key
- volatile-ttl:當記憶體不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除