關注wx:CodingTechWork
Redis介紹
概述
- Redis是NoSQL,是key-value分布式內存數據庫。
緩存
- 緩存是將數據從慢的介質換到快的介質上,提高讀寫效率和性能,并降低數據庫的讀寫成本。
- 內存的速度一般都遠遠大于硬盤的速度,大量請求數據庫或遠程應用時,會導致大量的時間消耗在調用上,從而降低系統應用調用效率,若使用緩存,則可以充分利用資源,提高系統調用效率。
特點
- Redis支持數據的持久化:Redis運行在內存中,但可以持久化到磁盤。將內存中的數據保存到磁盤中,重啟時可以再次加載進行使用。
- Redis提供多種數據結構類型存儲,如list、set、zset、hash。
- Redis支持數據的備份,即主從(master-slave)模式的數據備份。
- Redis支持發布-訂閱,通知key過期等特性。
- Redis性能高,讀的速度是11萬次/s,寫的速度是8萬次/s。
- Redis支持原子性,即要么全部執行成功,要么全部不執行。單個操作是原子的,多個操作也支持原子性事務,通過MULTI和EXEC指令包裝。
熱點數據和冷點數據
熱數據
- 需要被計算節點頻繁訪問的在線類數據,如某導航信息,緩存后,會被讀取很多次;
- 新建的通告信息,緩存后也會被讀取很多次。
冷數據
- 對于理想類不經常訪問的數據,如企業備份數據、業務與操作日志數據、話單與統計數據。
- 大部分數據可能還未再次訪問就已被擠出內存,不僅占用內存,且價值不大。
Memcached和Redis區別
存儲方式
:Memcached存儲在內存中,斷電掛掉丟失,數據不能超過內存大小;redis有部分存在硬盤上,也可以持久化數據到數據庫中。數據支持類型
:Memcached所有的值均為簡單的字符串,只是keyt-value類型數據;redis可提供list、set、zset、hash等數據結構存儲。底層實現方式
:redis直接構建自己的虛擬機機制value值大小
:Memcached1MB,redis最大達1GB速度
:redis速度快于Memcached,因為redis內存操作、單線程操作、采用非阻塞I/O多路復用機制災難恢復
:Memcached掛掉后,數據不可恢復;redis數據丟失后可通過AOF恢復。備份
:redis支持數據備份,即master-slave模式的數據備份。
Redis緩存問題
緩存雪崩
介紹
- 原有緩存失效,新緩存未到期。緩存中采用相同過期時間,同一時刻出現大面積緩存過期,原本訪問Redis緩存的請求都直接去查詢數據庫,對數據庫cpu和內存造成巨大壓力,嚴重時造成數據庫宕機,從而形成一系列連鎖反應,造成整個系統崩潰。
- 與緩存擊穿區別是,雪崩是很多key采用相同過期時間,同時多個key失效。擊穿是某一個key緩存失效。
解決方案
- 考慮加鎖、或者隊列的方式保證不會有大量的線程對數據庫一次性讀寫,避免失效時大量的并發請求落到底層存儲系統上。
- 緩存失效時間隨機化,給每個key的失效時間加個隨機值,保證數據不會在同一時間大面積失效。
- 設置熱點數據永不過期,有更新操作直接更新緩存。如系統首頁,有新產品展示,直接刷緩存,無需設置過期時間。
緩存穿透
介紹
- 查詢一個不存在的數據:緩存和數據庫中都沒有數據,用戶不斷發起請求,在緩存中找不到,每次去數據庫中再查一遍(兩次無用查詢),然后返回空。
- 緩存穿透一般出現在攻擊場景下,攻擊者知道請求路徑的規則后,傳遞一些不存在的id進行查詢數據。
解決方案
- 采用布隆過濾器(bloom-filter):將所有可能存在的數據哈希到一個足夠大的bitmap中,不存在的數據會被這個bitmap過濾攔截掉,避免對底層存儲系統查詢壓力。布隆過濾器用于檢索一個元素是否在一個集合中,使用redisson可實現。底層是主要是初始化一個比較大數據,里面存放二進制0或1,一開始都是0,當一個key進來后經歷3次hash計算,模于數組長度找到數據的下標然后把數組中原來的0改為1,三個
- 若一個查詢返回數據為空,仍然將空結果進行緩存,過期時間可以設置很短(如30s,不超過5min),通過直接設置默認值放入Redis緩存,第二次查詢緩存就可以獲取值,不會繼續訪問數據庫。
- 代碼層面的接口層增加校驗,如用戶鑒權校驗、參數校驗等,不合法參數直接返回異常。
緩存擊穿
介紹
- 對于一個key非常熱點,不停的去訪問這個key,承受高并發,當key失效瞬間,持續的高并發瞬間擊穿緩存,直接請求數據庫。
- 對于設置了過期時間的key,緩存在某個時間點過期的時候,恰好這個時間點對這個key有大量的并發請求進來,發現緩存過期直接從后端DB加載數據并回設到緩存,壓垮DB。
解決方案
- 設置熱點數據永遠不過期。
- 加互斥鎖,緩存失效是,不立即去load db,先使用如redis的setnx去設置一個互斥鎖,當操作成功返回時在進行load db的操作并回設緩存,否則重試get緩存方法。分布式鎖可以保證數據強一致性,但是性能不高。
- 邏輯過期:在設置key的時候,設置一個過期時間字段一塊存入緩存中,不給當前key設置過期時間,當查詢的時候,從redis取出數據后判斷時間是否過期,瑞過期則開通另外一個線程進行數據同步,當前線程正常返回數據,這個數據是舊的,服務降級而已,適用于to c,無需保證數據強一致性時可以用。
解決思路
- 事前:Redis 高可用,主從+哨兵,Redis cluster,避免全盤崩潰。
- 事中:本地 ehcache 緩存 + Hystrix 限流+降級,避免MySQL 被打死。
- 事后:Redis 持久化 RDB+AOF,一旦重啟,自動從磁盤上加載數據,快速恢復緩存數據。
雙寫一致性
- 方法一:使用redisson實現的讀寫鎖,在讀的時候添加共享鎖,保證讀讀不互斥,讀寫互斥。當更新數據時,添加排他鎖,讀寫、讀讀都互斥,保證在寫數據的同時不會有其他線程讀寫數據,避免臟數據。底層使用setnx,保證同時只有一個線程操作鎖住的方法。
- 方法二:延時雙刪,若是寫操作,先刪除緩存數據,然后更新數據庫,最后再延時刪除緩存中的數據,但這個延時玄學,也可能出現臟數據,并不能保證強一致性。
- 方法三:采用阿里的canal組件實現數據同步。部署一個canal服務,偽裝成mysql的一個從節點,當mysql數據更新后,canal讀取binlog數據,然后通過canal的客戶端獲取到數據更新緩存。
Redis數據類型
概述
String類型
:value可以是String也可以是數字,做一些復雜的計數功能緩存。hash類型
:value存放結構化對象,如單點登錄時存儲用戶信息,以cookieId作為key,設置30min為緩存時間。list類型
:可做簡單的消息隊列。set類型
:可做全局去重功能。sorted set類型
:可按照score進行排序,可做排行榜應用。
字符串(string)
介紹
- redis的字符串是動態字符串,可修改。
- 采用預分配冗余空間來減少內存的頻繁分配,內部為當前字符串分配的實際空間大小,一般要高于實際字符串長度。
- 當字符串長度小于1MB時,擴容都是加倍。字符串最大長度為512MB。
常用命令
get、set、incr、decr、mget
應用場景
計數器
:控制接口調用次數,如通過incrby命令進行遞增。共享session
:分布式服務會將用戶信息訪問負載均衡到不同的節點上。限速
:如短信獲取驗證碼功能,為了短信服務不會被頻繁訪問,限制用戶每分鐘獲取驗證碼的頻率、當天最大獲取短信的次數。
散列(hash)
介紹
- hash是一個鍵值對集合,適合存儲對象。
- hash內部是無序字典,內部存儲多個鍵值對,采用漸進式的rehash策略,在rehash時,保留新舊兩個hash結構,查詢會同時查詢新舊hash結構,漸進將舊hash內容遷移到新hash中。最終,舊hash被自動刪除,內存被回收。
常用命令
hget、hset、hmget、hmset、hgetall
應用場景
- 存儲對象:value存儲為HashMap
集合(set)
介紹
- set是string類型的無序集合,通過散列表實現。
- set內部相當于一個特殊字典,所有value都一個NULL值。
- 當集合中最后一個元素被移除后,數據結構自動刪除,內存被回收。
常用命令
sadd、srem、spop、sdiff、smemners、sunion
應用場景
- 去重數據功能,如用戶訪問列表查詢。
有序集合(zset)
介紹
- zset類似set,區別是每個元素都會關聯一個double類型的分數,通過分數來為集合的成員進行從小到大的排序。
- zset中的成員是唯一的,但分數可以重復。
- zset內部是使用“跳躍列表”的數據結構實現的。
- zset集合中最后一個元素被移除后,數據結構自動刪除,內存被回收。
常用命令
zadd、zrange、zrem、zcard
應用場景
- 去重并自動排序的數據。
列表(list)
介紹
- list是簡單的字符串列表,按照插入順序排序,可增加一個元素到列表的頭部或尾部。
- list的插入和刪除速度快,索引定位慢。
- list中的每個元素之間都是使用雙向指針順序連接,同時支持前后遍歷。
- 當最后一個元素被彈出后,數據結構自動刪除,內存被回收。
常用命令
lpush、rpush、lpop、rpop、lrange、blpop
應用場景
- 消息隊列,如最新通知排名,利用list的push操作,將任務存list中,再通過工作線程用pop操作將任務取出執行。
Redis原理
Redis單線程
為何Redis是單線程
- Redis是基于內存操作,CPU不是瓶頸,最大的瓶頸可能是機器內存的大小或者網絡帶寬,采用隊列技術將并發訪問變成串行訪問。
- 絕大部分請求是純粹的內存操作;
- 采用單線程避免不必要的上下文切換和競爭條件。
- 非阻塞I/O速度快,支持豐富數據類型,如String、list、set、sorted set、hash。
支持事務,操作都是原子性,具有豐富的特性,如可用于緩存、消息隊列、按key設置過期時間。
原子性
- 因為redis是單線程的,一個操作不可再分,要么都執行,要么都不執行。
- 多個命令在并發中不一定是原子的:如get set操作。使用redis事務或者redis+lua可以實現。
Redis過期策略
為何不采用定時刪除?
- 用定時器負責監視key,過期則刪除,雖然可以及時釋放內存,但消耗cpu資源,在大并發情況下,cpu要將時間應用于處理各種請求,而不是刪除key。
redis采用定期刪除+惰性刪除策略。
定期刪除
:默認每隔100ms檢查,是否有過期的key,有則刪除,但不是所有key都檢查一次,而是隨機抽取進行檢查,這種會造成很多key到時間未刪除。惰性刪除
:獲取某個key會檢查是否設置過期時間,過期,則刪除;這樣就可以比避免定期刪除時遺留的未刪除的key。
問題
- 問題:若定期刪除沒有刪除到key,且沒有及時請求該key,導致惰性刪除未生效,導致redis內存越來越高。。
- 解決方案:采用內存淘汰機制,在redis.conf中配置maxmemory-policy volatile-lru,即從已設置過期時間的數據集中挑選出最近最少使用的數據進行淘汰。
分類
惰性刪除
- 在設置該key過期時間后,不去管它,當需要該key時,會檢查是否過期,若過期直接刪除,否則,返回該key。
- 定期刪除會導致很多過期key到了時間并沒有被刪除掉,所以需要惰性刪除,主動檢查過期的key,發現key過期立即刪除,不會返回任何東西。
- 惰性刪除屬于零散處理。
定期刪除
- 定期刪除就是每隔一段時間,對一些key進行檢查,刪除里面過期的key。
- redis會將每個設置了國企時間的key都放入一個獨立的字典中,后續會定期遍歷這個字典來刪除到期的key;默認是每秒進行十次過期掃描,所以是100ms/次掃描。
- 定期刪除是采用簡單的貪心策略(避免全部key掃描對CPU帶來負載):從過期字典中隨機掃描20個key,刪除這20個key中已經過期的key,若過期的key比率超過1/4,就繼續隨機掃描。
- 定期刪除屬于集中處理。
- 模式分類:
1)SLOW模式:定時任務,執行頻率默認為10hz,每次不超過25ms,通過修改配置文件redis.conf的hz選項來調整次數。
2)FAST模式:執行頻率不固定,每次事件循環會嘗試執行,但兩次間隔不低于2ms,每次耗時不超過1ms。
Redis事務
介紹
- Redis事務功能通過MULTI、EXEC、DISCARD、WATCH四個原語實現。
- redis會將一個事務中所有命令序列化,然后按順序執行。redis不能在一個事務執行過程中插入零一二客戶端發出的請求。
- redis不支持回滾,redis在事務失敗時,會繼續執行剩余命令,內部可以保持簡單且快速。
- 如果在一個事務中命令出現錯誤,則所有命令都不會執行。
- 如果一個事務中出現運行錯誤,則正確的命令會被繼續執行。
四個原語
MULTI
- MULTI命令用于標記事務塊的開始,將后續命令逐個放入隊列中,然后才能使用EXEC命令原子化執行該命令序列。
- 返回值是一個簡單的字符串,總是OK。
EXEC
- 在一個事務中執行所有先前放入隊列的命令,然后恢復正常的連接狀態。
- 使用WATCH命令時,只有當受監控的鍵未被修改時,才能使用EXEC命令執行事務塊命令,采用CAS檢查再設置的機制。
- 返回值是衣蛾數組,每個元素分別是原子化事務中每個命令的返回值。
- 當使用WATCH命令時,若事務執行中止,則返回一個Null值。
DISCARD
- 清除所有先前在一個事務塊中放入隊列的命令,然后恢復正常的連接狀態。
- 返回值是一個簡單的字符串,總是OK。
WATCH
- 當某個事務需要按條件執行時,就需要使用WATCH命令將給定的鍵設置為受監控。
- 返回值是一個簡單的字符串,總是OK。
- 為redis事務踢動CAS(check-and-set)行為,可以監控一個或多個鍵,一旦有一個鍵被修改(或刪除),之后的事務不會執行,監控一直持續到EXEC命令執行時。
Redis事件
基于事件
核心原理是基于事件的處理流程。
- 主程序處于一個阻塞狀態的事件循環(event loop)中等待事件,當有事件發生時,根據事件的屬性分發到相對應的處理函數中進行處理。事件是以并發的方式發送到服務處理器,服務處理器將事件整合到一個有序隊列中,并發到具體的請求處理器進行處理。
- Redis程序都是圍繞事件循環進行的。事件循環同時監控多個事件(Redis對于連接套接字的抽象),當套接字變為可讀或者可寫狀態,則會觸發該事件,把就緒的事件放在一個待處理事件的隊列中,以有序、同步的方式發送給事件處理器進行處理。(Fire過程)
- Redis事件循環會保存兩個表:events和fired列表,前者存儲正在監聽的事件,后者存儲就緒的事件。
- Redis處理所有命令都是順序執行的,包括客戶端的連接請求、內部定時執行的任務等。所以當Redis處理一個復雜度高、時間很長的請求(如keys命令、自動刪除一個過期的大key),則其他客戶端連接有可能被阻塞。所以一般不使用大key,否則可能會造成業務卡頓。
Redis事件處理流程
- 加載配置
- 配置參數初始化:創建事件循環
- 循環事件注冊一個可讀事件,用于處理響應客戶端請求
- 執行事件循環,等待連接和命令請求
- 注冊事件,被eventLoop監聽
- 讀寫操作需要執行的就緒事件
Redis持久化
RDB
- RDB是一個快照文件,是把redis內存存儲的數據寫到磁盤上,當redis實例宕機恢復數據的時候,方便從RDB的快照文件中恢復數據。
- RDB是二進制文件,保存的時候體積比較小,恢復快,但可能會丟失數據。
AOF
- AOF是追加文件,當redis操作寫命令的時候,都會存儲這個文件中,當redis實例宕機恢復數據的時候,會從這個文件中再執行一遍命令恢復數據。
- 通常在項目中使用AOF恢復數據,速度雖然慢,但丟數據的風險小,AOF文件可設置刷盤策略,如每秒批量寫入一次命令。
Redis數據淘汰策略
概述
- 談淘汰策略之前,我們知道redis有過期刪除,根據TTL時間進行定期采樣刪除或惰性刪除
- 有過期策略為何還要淘汰策略?因為過期策略不能夠完全精準的全部刪除數據,會存在key沒有被刪除的場景,所以需要內存淘汰策略進行兜底。
內存淘汰策略
noeviction
:當內存使用超過配置的時候會返回錯誤,不會驅逐任何鍵allkeys-lru
:加入鍵的時候,如果過限,首先通過LRU算法驅逐最久沒有使用的鍵volatile-lru
:加入鍵的時候如果過限,首先從設置了過期時間的鍵集合中驅逐最久沒有使用的鍵allkeys-random
:加入鍵的時候如果過限,從所有key隨機刪除volatile-random
:加入鍵的時候如果過限,從過期鍵的集合中隨機驅逐volatile-ttl
:從配置了過期時間的鍵中驅逐馬上就要過期的鍵volatile-lfu
:從所有配置了過期時間的鍵中驅逐使用頻率最少的鍵allkeys-lfu
:從所有鍵中驅逐使用頻率最少的鍵