Redis使用
存儲用戶信息時
方案一:key-value形式存儲,key為用戶id,value為用戶結構信息序列化后的字符串內容
方案二:hash存儲,子key為用戶id,value為用戶結構信息,無需序列化
方案一在存取時需要序列化與反序列化;且key可不唯一,用戶id存儲可能重復;并發操作時需要將整個對象取回并對修改操作作并發保護。
方案二可防重,且不存在序列化開銷,沒有并發修改控制問題
TODO 這里有個問題,網上說hash可以避免cas問題,怎么體現的
分布式鎖
用于控制并發流程,如多個線程需要頻繁對某個數據進行讀取和修改引起的并發問題,使用分布式鎖可保證原子操作即執行某一過程不會因線程切換而打斷。
實現:setnx(set if not exits)一個時間只允許設置一個鎖,用完了del指令釋放
setnx lockKey true
del lockKey
問題一:如果在獲得鎖后發生事故導致del指令沒能執行,那么會引起死鎖問題,鎖不能被釋放,下一個線程永遠拿不到
解決一:在獲取鎖的同時設置一個過期時間expire lockKey seconds,保證發生異常不能主動釋放鎖也能自動釋放
問題二:雖然設置了過期時間保證自動釋放鎖,但如果在exprie前服務進程掛了,也是會引起死鎖的,因為expire和setnx指令不是原子指令不能一起執行 (TODO 不使用Redis事務的原因)
解決二:redis2.8添加set指令的擴展參數使expire和setnx指令可以一起執行,setnx+expire的原子指令(set lockKey true ex seconds nx)
問題三:加鎖后執行時間過長超過鎖的超時限制,導致當前線程還在執行任務時釋放鎖,下一個線程獲取鎖,臨界區代碼執行異常
解決三:lua腳本
解決四:設置隨機超時時間,要釋放鎖的線程需要和這個隨機數作比較確定是當前持有線程釋放的,但比較和釋放是兩個指令不是原子操作有風險
延時隊列
方法一:使用list列表作為異步消息隊列,支持多生產者多消費者并發進出消息
操作:獲?。簂pop/rpop,放入:lpush/rpush
問題一:隊列如果空了,消費者會一直循環pop,直到有數據返回;但這樣的空輪詢會導致客戶端CPU高消耗,redis的慢查詢增多,QPS升高
解決一:線程sleep,但會導致消息延遲;因為集群下你睡一下我睡一下,而且睡眠時間是定長的,那整個延遲時間會被拉長
解決二:阻塞讀blocking,blpop/brpop代替lpop/rpop,阻塞讀在隊列沒有數據時會進入休眠,有數據就醒來;但是如果長時間阻塞還是和解決一的問題一樣,同時引發redis客戶端連接閑置,服務器檢測到閑置連接主動斷開,blpop/brpop拋異常
解決三:針對阻塞讀帶來的拋異常,用戶捕捉異常時自定義重試時間
方法二:使用zset有序列表作為延時隊列的實現,消息序列化為字符串作為zset的value,score是消息到期處理時間,采用多線程輪詢方式從zset種獲取消息任務,這樣某個線程掛了也能繼續工作
操作
zrangebyscore:獲取消息
zrem:同一個任務被多線程取到后,使用zrem進行移除,誰移成功就是主人
問題:zrem確定多線程中的一個主人,相當于其他線程白白消耗資源
解決:lua scripting優化,保證zrem和zrangebyscore一起
問題二:沒有ack等安全機制保證消息可靠性
統計
用戶簽到記錄
使用位圖數據結構存儲某用戶一年內每天簽到的bool值,365天即365位46個字節;位圖的內容其實是字符串即byte數組,redis的位數組是自動擴展的,比如設置的索引位置超出長度,位數組將自動進行零擴充。
1). 整個位圖內容設置/獲?。篻et/set
2). 單個位操作:設置位圖某位置內容setbit bitName bitIndex bitValue(0/1);獲取某位置內容getbit bitName bitIndex;
3). 位圖統計:bitcount統計位圖指定范圍start-end為1的個數bitcount bitName startIndex endIndex
4). 位圖查找:bitpos查找指定范圍出現的第一個0或1bitpos bitName 1/0 startIndex endIndex
5). 多個位操作(redis 2.3v):bitfiled對指定位片段讀寫,最多連續64個連續位,超過64個位可以一次執行多指令;set/field/get指令可混合執行
設置 bitfield bitName set 無符號數 startIndex 替換符ASCII
獲取某位開始幾個符號數 bitfiled bitName get 符號數 startIndex,符號數指位數組中第一個位是符號位剩下是值,有符號數最多取64位,無符號數最多取63位;
對指定范圍的位進行自增操作bitfield bitName incrby 無符號數 startIndex 要加的數;提供溢出策略子指令overflow:飽和階段sat(超過范圍就停留最大/小值),失敗不執行fail,默認折返wrap
PV:每個網頁當天所有點擊量,為每個網頁單獨維護一個redis計數器,計數器名+日期為key,數值value incrby自增
UV:每個網頁用戶不重復點擊量
解決一:量少的時候,set集合sadd存儲用戶唯一id,scard獲取集合大小即為某頁面的UV數
解決二:一的場景適用于量少,如果量多會非常浪費空間。HyperLogLog是一種不精確的解決方案,誤差為0.81%,計數較少時,其存儲空間采用稀疏矩陣存儲,當占用超過閾值時,一次性轉為稠密矩陣占用12kb空間。
操作:增加計數pfadd 集合名 用戶0id....,獲取計數pfcount 集合名,多個計數累加pfmerge
實現:TODO 給出一個隨機數,獲取最低位連續零的個數
命中,去重
使用數據結構布隆過濾器判斷查找是否命中,缺失一定的精確度,類似set結構,判斷存在不是很準但判斷不存在很準。HyperLogLog適用于海量統計,但不適用于海量查找是否存在;如果采用查找數據庫判斷exits在高并發下會有系統瓶頸,或是緩存存儲會浪費大量存儲空間。
可通過修改誤判率提高精確度,但缺點是
操作
添加元素bf.add bfKey bfValue1,批量添加bf.madd bfKey bfValue1 bfValue2...
查詢存在與否bf.exits,批量查詢存在與否bf.mexits bfKey bfValue1 bfValue2...
自定義參數bf.reserve bfKey error_rate(錯誤率越低需要的空間越大) initialSize(預計要放入的元素數量),不設置默認錯誤率為0.01,默認size為100
使用:java客戶端lettuce支持指令擴展,jedis-2.x沒有提供
原理:
add:布隆過濾數據結構由位數組+hash函數組成,元素存入時,由多種hash函數取hash值(能使hash值對數組長度取模運算映射到的位置比較平均),多個hash值對數組長度取模運算映射到的多個位置置為1
exits:取hash值,計算位置。如果這幾個位置都為1說明存在
空間占用估計
hash函數的最佳數量k = 0.7 * (1/預計元素的數量)
錯誤率 = 0.6185 ^ (1/預計的元素數量)
限流
用于控制用戶行為,控制服務器訪問壓力,使用zset數據結構實現;一個用戶的一種行為作為一個zset記錄,key作為用戶行為,score作為時間窗口,value是唯一的時間戳其實和score是一樣的 但沒有意義
操作:記錄所有用戶行為,其他記錄刪除,只在時間窗口期內比較數量
記錄行為:zadd 用戶行為key 時間窗口 當前時間戳
移除時間窗口前的數據:zremrangeByScore 用戶行為key 正序 時間score(score-period:時間窗口前的時間)
獲取剩下的時間窗口內的行為數量:zcard 用戶行為key
設置過期時間,避免后續不再訪問的用戶(冷用戶)占內存,過期時間大于等于窗口期 expire key period+1
比較用戶行為數量是否超標
漏斗限流
在一定容量的漏斗中,允許漏斗中的水以一定的速度流出,水流出漏斗就有空間灌水,沒有就阻塞等待足夠的空間。redis的數據結構hash可以存儲這個過程的數據,但是取出,計算,更新這三個步驟不是原子性操作,會有性能或安全問題。Redis4.0提供了限流模塊Redis-Cell,提供了原子的限流指令
操作
cl.throttle 限流對象key 漏斗容量 灌水次數 固定時間內(灌水次數/固定時間內:構成流水速率)
執行返回1是否允許,2漏斗剩余容量,3剩余容量,4被拒絕后的重試時間,5漏斗完全空出的時間;如果被拒絕了可以根據返回重試時間進行重試
計算附近的人
根據當前用戶的經緯度信息排序附近距離的人,數量較小的時候可以以用戶的經緯度劃出一個半徑為r的矩形區域,通過數據庫語句查找所有條件用戶。
Redis使用GeoHash算法,先將地球當作一個二維平面并劃分為一系列方格,方格越小坐標越精確,將用戶經緯度通過某一算法整數化為一個整數(這個過程是有損的,GeoHash算法會繼續對這個整數做一次base32編碼變成一個字符串),這樣距離近的人整數大小也是相近的。Redis使用52位的整數進行編碼經緯度并放入zset中,value是元素的key即用戶,score是GeoHash的52位整數即坐標,通過排序score就可以實現附近的人。
操作
增加:geoadd 集合key 經度1 緯度1 元素value1或批量增加geoadd 集合key 經度1 緯度1 元素value1 經度2 緯度2 元素value2
計算元素之間的距離:geodist 集合key 元素value1 元素value2 距離單位(km/m/ml/ft)
獲取元素位置,返回經緯度:geopops 集合key 元素value1
查找元素附近的其他元素:georadiusbymember 集合key 元素value1 距離 單位 count 3(元素數量) asc/desc(正逆序)
查找元素附近的其他元素并顯示距離:georadiusbymember 集合key 元素value1 距離 單位 withcoord withdist withhash count 3(元素數量) asc/desc(正逆序)
根據經緯度查詢附近的元素:georadius 集合key 經度 緯度 距離 單位 withdist count 3(元素數量) asc/desc(正逆序)
問題:如果在數量十分大的場景zset的容量不應超過1M,不然在redis集群中節點遷移時會出現卡頓的現象,建議geo數據單獨redis實例部署
找出特點的key列表
方式一:keys *或keys prefix**suffix
缺點:沒有限制limit,一次性列出所有符合的key,而且復雜度是O(n),如果數量超大會導致Redis服務卡頓,因為Redis是單線程,所以這個指令不執行完其他讀寫指令都要延后或超時
方式二:
使用:
遍歷所有的key:scan 游標值 key的正則模式 count 數量(遍歷的limit hint)
對指定容器集合遍歷:zscan/hscan/sscan,因為這些容器結構的key存儲底層都是字典
定位大小較大的key:redis-cli指令提供:redis-cli -h 127.0.0.1 -p 7001 –-bigkeys(-i 0.1表示每隔100條休眠0.1s,減少ops大幅提升,但相應會延遲掃描時間),要盡量避免大key的產生,不然新增/刪除都會對Redis帶來卡頓
特點:
時間復雜度為O(n),但通過游標分布進行,不阻塞線程
有limit參數,限定服務器單次遍歷的字典槽位數量
具備正則模式匹配功能
會返回給客戶端游標整數,如果游標不為0但是返回空列表,不代表返回的是空數據,可能遍歷還未結束,只是暫時沒找到匹配的而已
返回的結果集可能有重復結果,需要客戶端自主去重
Redis key存儲字典結構類似于HashMap,由一維數組和列表組成。數組空間為2^n,擴容一次數組大小空間加倍n++。
scan遍歷過程:scan采用高位進位加法來遍歷一維數組,根據指定游標值開始從對應的數組索引(或稱槽slot)開始從左邊加進位往右邊移,與普通加法相反,經過limit數量的槽。這是為了保證字典擴容或縮容時避免槽位的遍歷重復/遺漏
字典擴容:擴容后槽位為高進位加1,如字典長度由16位擴容到32位,二進制槽位xxxx中的元素將被rehas到0xxxx和1xxxx(xxxx+16)中;8位擴容到16位就加8
漸進式rehash:不同于Java中一次性移動所有舊數組下掛連的元素到新數組,但這樣線程會出現卡頓現象。而漸進式rehash會保留新舊數組,漸漸將舊數組遷移到新數組,如果這時有其他操作訪問這個數組,需要同時遍歷新舊數組。
總結
- 上一篇: badblocks坏道检测
- 下一篇: 开学前的准备(有了这份开学准备工作清单,