為什么要用緩存梅尤?
高性能
假設這么個場景澜汤,你有個操作,一個請求過來放妈,吭哧吭哧你各種亂七八糟操作mysql骂倘,半天查出來一個結果眼滤,耗時600ms。但是這個結果可能接下來幾個小時都不會變了历涝,或者變了也可以不用立即反饋給用戶诅需。那么此時咋辦漾唉?
緩存啊,折騰600ms查出來的結果堰塌,扔緩存里赵刑,一個key對應一個value,下次再有人查场刑,別走mysql折騰600ms了般此。直接從緩存里,通過一個key查出來一個value牵现,2ms搞定铐懊。性能提升300倍。
這就是所謂的高性能瞎疼。
就是把你一些復雜操作耗時查出來的結果科乎,如果確定后面不咋變了,然后但是馬上還有很多讀請求贼急,那么直接結果放緩存茅茂,后面直接讀緩存就好了。
高并發(fā)
mysql這么重的數(shù)據(jù)庫太抓,壓根兒設計不是讓你玩兒高并發(fā)的空闲,雖然也可以玩兒,但是天然支持不好走敌。mysql單機支撐到2000qps也開始容易報警了碴倾。
所以要是你有個系統(tǒng),高峰期一秒鐘過來的請求有1萬掉丽,那一個mysql單機絕對會死掉影斑。你這個時候就只能上緩存,把很多數(shù)據(jù)放緩存机打,別放mysql。緩存功能簡單片迅,說白了就是key-value式操作残邀,單機支撐的并發(fā)量輕松一秒幾萬十幾萬,支撐高并發(fā)so easy柑蛇。單機承載并發(fā)量是mysql單機的幾十倍芥挣。
所以你要結合這倆場景考慮一下,你為啥要用緩存耻台?
一般很多同學項目里沒啥高并發(fā)場景空免,那就別折騰了,直接用高性能那個場景吧盆耽,就思考有沒有可以緩存結果的復雜查詢場景蹋砚,后續(xù)可以大幅度提升性能扼菠,優(yōu)化用戶體驗,有坝咐,就說這個理由循榆,沒有?墨坚?那你也得編一個出來吧秧饮,不然你不是在搞笑么
(1) redis 和 memcached 的區(qū)別
redis支持更豐富的數(shù)據(jù)類型(支持更復雜的應用場景):Redis不僅僅支持簡單的k/v類型的數(shù)據(jù),同時還提供list泽篮,set盗尸,zset,hash等數(shù)據(jù)結構的存儲帽撑。memcache支持簡單的數(shù)據(jù)類型泼各,String。
Redis支持數(shù)據(jù)的持久化油狂,可以將內存中的數(shù)據(jù)保持在磁盤中历恐,重啟的時候可以再次加載進行使用,而Memecache把數(shù)據(jù)全部存在內存之中。
集群模式:memcached沒有原生的集群模式专筷,需要依靠客戶端來實現(xiàn)往集群中分片寫入數(shù)據(jù)弱贼;但是 redis 目前是原生支持 cluster 模式的.
Memcached是多線程,非阻塞IO復用的網(wǎng)絡模型磷蛹;Redis使用單線程的多路 IO 復用模型
(2)redis的線程模型
1)文件事件處理器
redis基于reactor模式開發(fā)了網(wǎng)絡事件處理器吮旅,這個處理器叫做文件事件處理器,file event handler味咳。這個文件事件處理器庇勃,是單線程的,redis才叫做單線程的模型槽驶,采用IO多路復用機制同時監(jiān)聽多個socket责嚷,根據(jù)socket上的事件來選擇對應的事件處理器來處理這個事件。
如果被監(jiān)聽的socket準備好執(zhí)行accept掂铐、read罕拂、write、close等操作的時候全陨,跟操作對應的文件事件就會產(chǎn)生爆班,這個時候文件事件處理器就會調用之前關聯(lián)好的事件處理器來處理這個事件。
文件事件處理器是單線程模式運行的辱姨,但是通過IO多路復用機制監(jiān)聽多個socket柿菩,可以實現(xiàn)高性能的網(wǎng)絡通信模型,又可以跟內部其他單線程的模塊進行對接雨涛,保證了redis內部的線程模型的簡單性枢舶。
文件事件處理器的結構包含4個部分:多個socket懦胞,IO多路復用程序,文件事件分派器祟辟,事件處理器(命令請求處理器医瘫、命令回復處理器、連接應答處理器旧困,等等)醇份。
多個socket可能并發(fā)的產(chǎn)生不同的操作,每個操作對應不同的文件事件吼具,但是IO多路復用程序會監(jiān)聽多個socket僚纷,但是會將socket放入一個隊列中排隊,每次從隊列中取出一個socket給事件分派器拗盒,事件分派器把socket給對應的事件處理器怖竭。
然后一個socket的事件處理完之后,IO多路復用程序才會將隊列中的下一個socket給事件分派器陡蝇。文件事件分派器會根據(jù)每個socket當前產(chǎn)生的事件痊臭,來選擇對應的事件處理器來處理。
2)文件事件
當socket變得可讀時(比如客戶端對redis執(zhí)行write操作登夫,或者close操作)广匙,或者有新的可以應答的sccket出現(xiàn)時(客戶端對redis執(zhí)行connect操作),socket就會產(chǎn)生一個AE_READABLE事件恼策。
當socket變得可寫的時候(客戶端對redis執(zhí)行read操作)鸦致,socket會產(chǎn)生一個AE_WRITABLE事件。
IO多路復用程序可以同時監(jiān)聽AE_REABLE和AE_WRITABLE兩種事件涣楷,要是一個socket同時產(chǎn)生了AE_READABLE和AE_WRITABLE兩種事件分唾,那么文件事件分派器優(yōu)先處理AE_REABLE事件,然后才是AE_WRITABLE事件狮斗。
3)文件事件處理器
如果是客戶端要連接redis绽乔,那么會為socket關聯(lián)連接應答處理器
如果是客戶端要寫數(shù)據(jù)到redis,那么會為socket關聯(lián)命令請求處理器
如果是客戶端要從redis讀數(shù)據(jù)碳褒,那么會為socket關聯(lián)命令回復處理器
4)客戶端與redis通信的一次流程
在redis啟動初始化的時候迄汛,redis會將連接應答處理器跟AE_READABLE事件關聯(lián)起來,接著如果一個客戶端跟redis發(fā)起連接骤视,此時會產(chǎn)生一個AE_READABLE事件,然后由連接應答處理器來處理跟客戶端建立連接鹃觉,創(chuàng)建客戶端對應的socket专酗,同時將這個socket的AE_READABLE事件跟命令請求處理器關聯(lián)起來。
當客戶端向redis發(fā)起請求的時候(不管是讀請求還是寫請求盗扇,都一樣)祷肯,首先就會在socket產(chǎn)生一個AE_READABLE事件沉填,然后由對應的命令請求處理器來處理。這個命令請求處理器就會從socket中讀取請求相關數(shù)據(jù)佑笋,然后進行執(zhí)行和處理翼闹。
接著redis這邊準備好了給客戶端的響應數(shù)據(jù)之后,就會將socket的AE_WRITABLE事件跟命令回復處理器關聯(lián)起來蒋纬,當客戶端這邊準備好讀取響應數(shù)據(jù)時猎荠,就會在socket上產(chǎn)生一個AE_WRITABLE事件,會由對應的命令回復處理器來處理蜀备,就是將準備好的響應數(shù)據(jù)寫入socket关摇,供客戶端來讀取。
命令回復處理器寫完之后碾阁,就會刪除這個socket的AE_WRITABLE事件和命令回復處理器的關聯(lián)關系输虱。
(3)為啥redis單線程模型也能效率這么高?
1)純內存操作
2)核心是基于非阻塞的IO多路復用機制
3)單線程反而避免了多線程的頻繁上下文切換問題(百度)
redis都有哪些數(shù)據(jù)類型脂凶?分別在哪些場景下使用比較合適宪睹?
(1)string
這是最基本的類型了,沒啥可說的蚕钦,就是普通的set和get亭病,做簡單的kv緩存
(2)hash
這個是類似map的一種結構,這個一般就是可以將結構化的數(shù)據(jù)冠桃,比如一個對象(前提是這個對象沒嵌套其他的對象)給緩存在redis里命贴,然后每次讀寫緩存的時候,可以就操作hash里的某個字段食听。
key=150
value={
??“id”: 150,
??“name”: “zhangsan”,
??“age”: 20
}
hash類的數(shù)據(jù)結構胸蛛,主要是用來存放一些對象,把一些簡單的對象給緩存起來樱报,后續(xù)操作的時候葬项,你可以直接僅僅修改這個對象中的某個字段的值
value={
??“id”: 150,
??“name”: “zhangsan”,
??“age”: 21
}
(3)list
有序列表,這個是可以玩兒出很多花樣的
微博迹蛤,某個大v的粉絲民珍,就可以以list的格式放在redis里去緩存
key=某大v
value=[zhangsan, lisi, wangwu]
比如可以通過list存儲一些列表型的數(shù)據(jù)結構,類似粉絲列表了盗飒、文章的評論列表了之類的東西
比如可以通過lrange命令嚷量,就是從某個元素開始讀取多少個元素,可以基于list實現(xiàn)分頁查詢逆趣,這個很棒的一個功能蝶溶,基于redis實現(xiàn)簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西,性能高抖所,就一頁一頁走
比如可以搞個簡單的消息隊列梨州,從list頭懟進去,從list尾巴那里弄出來
(4)set
無序集合田轧,自動去重
直接基于set將系統(tǒng)里需要去重的數(shù)據(jù)扔進去暴匠,自動就給去重了,如果你需要對一些數(shù)據(jù)進行快速的全局去重傻粘,你當然也可以基于jvm內存里的HashSet進行去重每窖,但是如果你的某個系統(tǒng)部署在多臺機器上呢?
得基于redis進行全局的set去重
可以基于set玩兒交集抹腿、并集岛请、差集的操作,比如交集吧警绩,可以把兩個人的粉絲列表整一個交集崇败,看看倆人的共同好友是誰?對吧
把兩個大v的粉絲都放在兩個set中肩祥,對兩個set做交集
(5)sorted set
排序的set后室,去重但是可以排序,寫進去的時候給一個分數(shù)混狠,自動根據(jù)分數(shù)排序岸霹,這個可以玩兒很多的花樣,最大的特點是有個分數(shù)可以自定義排序規(guī)則
比如說你要是想根據(jù)時間對數(shù)據(jù)排序将饺,那么可以寫入進去的時候用某個時間作為分數(shù)贡避,人家自動給你按照時間排序了
排行榜:將每個用戶以及其對應的什么分數(shù)寫入進去,zadd board score username予弧,接著zrevrange board 0 99刮吧,就可以獲取排名前100的用戶;zrank board username掖蛤,可以看到用戶在排行榜里的排名
redis的過期策略都有哪些杀捻?內存淘汰機制都有哪些?手寫一下LRU代碼實現(xiàn)蚓庭?
定期刪除+惰性刪除
所謂定期刪除致讥,指的是redis默認是每隔100ms就隨機抽取一些設置了過期時間的key,檢查其是否過期器赞,如果過期就刪除垢袱。假設redis里放了10萬個key,都設置了過期時間港柜,你每隔幾百毫秒惶桐,就檢查10萬個key,那redis基本上就死了,cpu負載會很高的姚糊,消耗在你的檢查過期key上了。注意授舟,這里可不是每隔100ms就遍歷所有的設置過期時間的key救恨,那樣就是一場性能上的災難。實際上redis是每隔100ms隨機抽取一些key來檢查和刪除的释树。
但是問題是肠槽,定期刪除可能會導致很多過期key到了時間并沒有被刪除掉,那咋整呢奢啥?所以就是惰性刪除了秸仙。這就是說,在你獲取某個key的時候桩盲,redis會檢查一下 寂纪,這個key如果設置了過期時間那么是否過期了?如果過期了此時就會刪除赌结,不會給你返回任何東西捞蛋。
并不是key到時間就被刪除掉,而是你查詢這個key的時候柬姚,redis再懶惰的檢查一下
通過上述兩種手段結合起來拟杉,保證過期的key一定會被干掉。
很簡單量承,就是說搬设,你的過期key,靠定期刪除沒有被刪除掉撕捍,還停留在內存里拿穴,占用著你的內存呢,除非你的系統(tǒng)去查一下那個key卦洽,才會被redis給刪除掉贞言。
但是實際上這還是有問題的,如果定期刪除漏掉了很多過期key阀蒂,然后你也沒及時去查该窗,也就沒走惰性刪除,此時會怎么樣蚤霞?如果大量過期key堆積在內存里酗失,導致redis內存塊耗盡了,咋整昧绣?
答案是:走內存淘汰機制规肴。
(2)內存淘汰
如果redis的內存占用過多的時候,此時會進行內存淘汰,有如下一些策略:
redis 10個key拖刃,現(xiàn)在已經(jīng)滿了删壮,redis需要刪除掉5個key
1個key,最近1分鐘被查詢了100次
1個key兑牡,最近10分鐘被查詢了50次
1個key央碟,最近1個小時倍查詢了1次
1)noeviction:當內存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯均函,這個一般沒人用吧亿虽,實在是太惡心了
2)allkeys-lru:當內存不足以容納新寫入數(shù)據(jù)時,在鍵空間中苞也,移除最近最少使用的key(這個是最常用的)
3)allkeys-random:當內存不足以容納新寫入數(shù)據(jù)時洛勉,在鍵空間中,隨機移除某個key如迟,這個一般沒人用吧收毫,為啥要隨機,肯定是把最近最少使用的key給干掉啊
4)volatile-lru:當內存不足以容納新寫入數(shù)據(jù)時氓涣,在設置了過期時間的鍵空間中牛哺,移除最近最少使用的key(這個一般不太合適)
5)volatile-random:當內存不足以容納新寫入數(shù)據(jù)時,在設置了過期時間的鍵空間中劳吠,隨機移除某個key
6)volatile-ttl:當內存不足以容納新寫入數(shù)據(jù)時引润,在設置了過期時間的鍵空間中,有更早過期時間的key優(yōu)先移除
要不你手寫一個LRU算法痒玩?
我確實有時會問這個淳附,因為有些候選人如果確實過五關斬六將,前面的問題都答的很好蠢古,那么其實讓他寫一下LRU算法奴曙,可以考察一下編碼功底
你可以現(xiàn)場手寫最原始的LRU算法,那個代碼量太大了草讶,我覺得不太現(xiàn)實
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
//這里就是傳遞進來最多能緩存多少數(shù)據(jù)
????public LRUCache(int cacheSize) {
????????super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);//這塊就是設置一個hashmap的初始大小洽糟,同時最后一個true指的是讓linkedhashmap按照訪問順序來進行排序,最近訪問的放在頭堕战,最老訪問的就在尾
????????CACHE_SIZE = cacheSize;
????}
????@Override
????protected boolean removeEldestEntry(Map.Entry eldest) {
????????return size() > CACHE_SIZE;//這個意思就是說當map中的數(shù)據(jù)量大于指定的緩存?zhèn)€數(shù)的時候坤溃,就自動刪除最老的數(shù)據(jù)
????}
}
我給你看上面的代碼,是告訴你最起碼你也得寫出來上面那種代碼嘱丢,不求自己純手工從底層開始打造出自己的LRU薪介,但是起碼知道如何利用已有的jdk數(shù)據(jù)結構實現(xiàn)一個java版的LRU