為什么要用緩存?
高性能
假設(shè)這么個場景,你有個操作,一個請求過來,吭哧吭哧你各種亂七八糟操作mysql,半天查出來一個結(jié)果,耗時600ms。但是這個結(jié)果可能接下來幾個小時都不會變了,或者變了也可以不用立即反饋給用戶。那么此時咋辦?
緩存啊,折騰600ms查出來的結(jié)果,扔緩存里,一個key對應(yīng)一個value,下次再有人查,別走mysql折騰600ms了。直接從緩存里,通過一個key查出來一個value,2ms搞定。性能提升300倍。
這就是所謂的高性能。
就是把你一些復(fù)雜操作耗時查出來的結(jié)果,如果確定后面不咋變了,然后但是馬上還有很多讀請求,那么直接結(jié)果放緩存,后面直接讀緩存就好了。

高并發(fā)
mysql這么重的數(shù)據(jù)庫,壓根兒設(shè)計不是讓你玩兒高并發(fā)的,雖然也可以玩兒,但是天然支持不好。mysql單機(jī)支撐到2000qps也開始容易報警了。
所以要是你有個系統(tǒng),高峰期一秒鐘過來的請求有1萬,那一個mysql單機(jī)絕對會死掉。你這個時候就只能上緩存,把很多數(shù)據(jù)放緩存,別放mysql。緩存功能簡單,說白了就是key-value式操作,單機(jī)支撐的并發(fā)量輕松一秒幾萬十幾萬,支撐高并發(fā)so easy。單機(jī)承載并發(fā)量是mysql單機(jī)的幾十倍。

所以你要結(jié)合這倆場景考慮一下,你為啥要用緩存?
一般很多同學(xué)項(xiàng)目里沒啥高并發(fā)場景,那就別折騰了,直接用高性能那個場景吧,就思考有沒有可以緩存結(jié)果的復(fù)雜查詢場景,后續(xù)可以大幅度提升性能,優(yōu)化用戶體驗(yàn),有,就說這個理由,沒有??那你也得編一個出來吧,不然你不是在搞笑么
(1) redis 和 memcached 的區(qū)別
redis支持更豐富的數(shù)據(jù)類型(支持更復(fù)雜的應(yīng)用場景):Redis不僅僅支持簡單的k/v類型的數(shù)據(jù),同時還提供list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲。memcache支持簡單的數(shù)據(jù)類型,String。
Redis支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟的時候可以再次加載進(jìn)行使用,而Memecache把數(shù)據(jù)全部存在內(nèi)存之中。
集群模式:memcached沒有原生的集群模式,需要依靠客戶端來實(shí)現(xiàn)往集群中分片寫入數(shù)據(jù);但是 redis 目前是原生支持 cluster 模式的.
Memcached是多線程,非阻塞IO復(fù)用的網(wǎng)絡(luò)模型;Redis使用單線程的多路 IO 復(fù)用模型
(2)redis的線程模型

1)文件事件處理器
redis基于reactor模式開發(fā)了網(wǎng)絡(luò)事件處理器,這個處理器叫做文件事件處理器,file event handler。這個文件事件處理器,是單線程的,redis才叫做單線程的模型,采用IO多路復(fù)用機(jī)制同時監(jiān)聽多個socket,根據(jù)socket上的事件來選擇對應(yīng)的事件處理器來處理這個事件。
如果被監(jiān)聽的socket準(zhǔn)備好執(zhí)行accept、read、write、close等操作的時候,跟操作對應(yīng)的文件事件就會產(chǎn)生,這個時候文件事件處理器就會調(diào)用之前關(guān)聯(lián)好的事件處理器來處理這個事件。
文件事件處理器是單線程模式運(yùn)行的,但是通過IO多路復(fù)用機(jī)制監(jiān)聽多個socket,可以實(shí)現(xiàn)高性能的網(wǎng)絡(luò)通信模型,又可以跟內(nèi)部其他單線程的模塊進(jìn)行對接,保證了redis內(nèi)部的線程模型的簡單性。
文件事件處理器的結(jié)構(gòu)包含4個部分:多個socket,IO多路復(fù)用程序,文件事件分派器,事件處理器(命令請求處理器、命令回復(fù)處理器、連接應(yīng)答處理器,等等)。
多個socket可能并發(fā)的產(chǎn)生不同的操作,每個操作對應(yīng)不同的文件事件,但是IO多路復(fù)用程序會監(jiān)聽多個socket,但是會將socket放入一個隊(duì)列中排隊(duì),每次從隊(duì)列中取出一個socket給事件分派器,事件分派器把socket給對應(yīng)的事件處理器。
然后一個socket的事件處理完之后,IO多路復(fù)用程序才會將隊(duì)列中的下一個socket給事件分派器。文件事件分派器會根據(jù)每個socket當(dāng)前產(chǎn)生的事件,來選擇對應(yīng)的事件處理器來處理。
2)文件事件
當(dāng)socket變得可讀時(比如客戶端對redis執(zhí)行write操作,或者close操作),或者有新的可以應(yīng)答的sccket出現(xiàn)時(客戶端對redis執(zhí)行connect操作),socket就會產(chǎn)生一個AE_READABLE事件。
當(dāng)socket變得可寫的時候(客戶端對redis執(zhí)行read操作),socket會產(chǎn)生一個AE_WRITABLE事件。
IO多路復(fù)用程序可以同時監(jiān)聽AE_REABLE和AE_WRITABLE兩種事件,要是一個socket同時產(chǎn)生了AE_READABLE和AE_WRITABLE兩種事件,那么文件事件分派器優(yōu)先處理AE_REABLE事件,然后才是AE_WRITABLE事件。
3)文件事件處理器
如果是客戶端要連接redis,那么會為socket關(guān)聯(lián)連接應(yīng)答處理器
如果是客戶端要寫數(shù)據(jù)到redis,那么會為socket關(guān)聯(lián)命令請求處理器
如果是客戶端要從redis讀數(shù)據(jù),那么會為socket關(guān)聯(lián)命令回復(fù)處理器
4)客戶端與redis通信的一次流程
在redis啟動初始化的時候,redis會將連接應(yīng)答處理器跟AE_READABLE事件關(guān)聯(lián)起來,接著如果一個客戶端跟redis發(fā)起連接,此時會產(chǎn)生一個AE_READABLE事件,然后由連接應(yīng)答處理器來處理跟客戶端建立連接,創(chuàng)建客戶端對應(yīng)的socket,同時將這個socket的AE_READABLE事件跟命令請求處理器關(guān)聯(lián)起來。
當(dāng)客戶端向redis發(fā)起請求的時候(不管是讀請求還是寫請求,都一樣),首先就會在socket產(chǎn)生一個AE_READABLE事件,然后由對應(yīng)的命令請求處理器來處理。這個命令請求處理器就會從socket中讀取請求相關(guān)數(shù)據(jù),然后進(jìn)行執(zhí)行和處理。
接著redis這邊準(zhǔn)備好了給客戶端的響應(yīng)數(shù)據(jù)之后,就會將socket的AE_WRITABLE事件跟命令回復(fù)處理器關(guān)聯(lián)起來,當(dāng)客戶端這邊準(zhǔn)備好讀取響應(yīng)數(shù)據(jù)時,就會在socket上產(chǎn)生一個AE_WRITABLE事件,會由對應(yīng)的命令回復(fù)處理器來處理,就是將準(zhǔn)備好的響應(yīng)數(shù)據(jù)寫入socket,供客戶端來讀取。
命令回復(fù)處理器寫完之后,就會刪除這個socket的AE_WRITABLE事件和命令回復(fù)處理器的關(guān)聯(lián)關(guān)系。
(3)為啥redis單線程模型也能效率這么高?
1)純內(nèi)存操作
2)核心是基于非阻塞的IO多路復(fù)用機(jī)制
3)單線程反而避免了多線程的頻繁上下文切換問題(百度)
redis都有哪些數(shù)據(jù)類型?分別在哪些場景下使用比較合適?
(1)string
這是最基本的類型了,沒啥可說的,就是普通的set和get,做簡單的kv緩存
(2)hash
這個是類似map的一種結(jié)構(gòu),這個一般就是可以將結(jié)構(gòu)化的數(shù)據(jù),比如一個對象(前提是這個對象沒嵌套其他的對象)給緩存在redis里,然后每次讀寫緩存的時候,可以就操作hash里的某個字段。
key=150
value={
??“id”: 150,
??“name”: “zhangsan”,
??“age”: 20
}
hash類的數(shù)據(jù)結(jié)構(gòu),主要是用來存放一些對象,把一些簡單的對象給緩存起來,后續(xù)操作的時候,你可以直接僅僅修改這個對象中的某個字段的值
value={
??“id”: 150,
??“name”: “zhangsan”,
??“age”: 21
}
(3)list
有序列表,這個是可以玩兒出很多花樣的
微博,某個大v的粉絲,就可以以list的格式放在redis里去緩存
key=某大v
value=[zhangsan, lisi, wangwu]
比如可以通過list存儲一些列表型的數(shù)據(jù)結(jié)構(gòu),類似粉絲列表了、文章的評論列表了之類的東西
比如可以通過lrange命令,就是從某個元素開始讀取多少個元素,可以基于list實(shí)現(xiàn)分頁查詢,這個很棒的一個功能,基于redis實(shí)現(xiàn)簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西,性能高,就一頁一頁走
比如可以搞個簡單的消息隊(duì)列,從list頭懟進(jìn)去,從list尾巴那里弄出來
(4)set
無序集合,自動去重
直接基于set將系統(tǒng)里需要去重的數(shù)據(jù)扔進(jìn)去,自動就給去重了,如果你需要對一些數(shù)據(jù)進(jìn)行快速的全局去重,你當(dāng)然也可以基于jvm內(nèi)存里的HashSet進(jìn)行去重,但是如果你的某個系統(tǒng)部署在多臺機(jī)器上呢?
得基于redis進(jìn)行全局的set去重
可以基于set玩兒交集、并集、差集的操作,比如交集吧,可以把兩個人的粉絲列表整一個交集,看看倆人的共同好友是誰?對吧
把兩個大v的粉絲都放在兩個set中,對兩個set做交集
(5)sorted set
排序的set,去重但是可以排序,寫進(jìn)去的時候給一個分?jǐn)?shù),自動根據(jù)分?jǐn)?shù)排序,這個可以玩兒很多的花樣,最大的特點(diǎn)是有個分?jǐn)?shù)可以自定義排序規(guī)則
比如說你要是想根據(jù)時間對數(shù)據(jù)排序,那么可以寫入進(jìn)去的時候用某個時間作為分?jǐn)?shù),人家自動給你按照時間排序了
排行榜:將每個用戶以及其對應(yīng)的什么分?jǐn)?shù)寫入進(jìn)去,zadd board score username,接著zrevrange board 0 99,就可以獲取排名前100的用戶;zrank board username,可以看到用戶在排行榜里的排名
redis的過期策略都有哪些?內(nèi)存淘汰機(jī)制都有哪些?手寫一下LRU代碼實(shí)現(xiàn)?
定期刪除+惰性刪除
所謂定期刪除,指的是redis默認(rèn)是每隔100ms就隨機(jī)抽取一些設(shè)置了過期時間的key,檢查其是否過期,如果過期就刪除。假設(shè)redis里放了10萬個key,都設(shè)置了過期時間,你每隔幾百毫秒,就檢查10萬個key,那redis基本上就死了,cpu負(fù)載會很高的,消耗在你的檢查過期key上了。注意,這里可不是每隔100ms就遍歷所有的設(shè)置過期時間的key,那樣就是一場性能上的災(zāi)難。實(shí)際上redis是每隔100ms隨機(jī)抽取一些key來檢查和刪除的。
但是問題是,定期刪除可能會導(dǎo)致很多過期key到了時間并沒有被刪除掉,那咋整呢?所以就是惰性刪除了。這就是說,在你獲取某個key的時候,redis會檢查一下 ,這個key如果設(shè)置了過期時間那么是否過期了?如果過期了此時就會刪除,不會給你返回任何東西。
并不是key到時間就被刪除掉,而是你查詢這個key的時候,redis再懶惰的檢查一下
通過上述兩種手段結(jié)合起來,保證過期的key一定會被干掉。
很簡單,就是說,你的過期key,靠定期刪除沒有被刪除掉,還停留在內(nèi)存里,占用著你的內(nèi)存呢,除非你的系統(tǒng)去查一下那個key,才會被redis給刪除掉。
但是實(shí)際上這還是有問題的,如果定期刪除漏掉了很多過期key,然后你也沒及時去查,也就沒走惰性刪除,此時會怎么樣?如果大量過期key堆積在內(nèi)存里,導(dǎo)致redis內(nèi)存塊耗盡了,咋整?
答案是:走內(nèi)存淘汰機(jī)制。
(2)內(nèi)存淘汰
如果redis的內(nèi)存占用過多的時候,此時會進(jìn)行內(nèi)存淘汰,有如下一些策略:
redis 10個key,現(xiàn)在已經(jīng)滿了,redis需要刪除掉5個key
1個key,最近1分鐘被查詢了100次
1個key,最近10分鐘被查詢了50次
1個key,最近1個小時倍查詢了1次
1)noeviction:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯,這個一般沒人用吧,實(shí)在是太惡心了
2)allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,移除最近最少使用的key(這個是最常用的)
3)allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,隨機(jī)移除某個key,這個一般沒人用吧,為啥要隨機(jī),肯定是把最近最少使用的key給干掉啊
4)volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中,移除最近最少使用的key(這個一般不太合適)
5)volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中,隨機(jī)移除某個key
6)volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中,有更早過期時間的key優(yōu)先移除
要不你手寫一個LRU算法?
我確實(shí)有時會問這個,因?yàn)橛行┖蜻x人如果確實(shí)過五關(guān)斬六將,前面的問題都答的很好,那么其實(shí)讓他寫一下LRU算法,可以考察一下編碼功底
你可以現(xiàn)場手寫最原始的LRU算法,那個代碼量太大了,我覺得不太現(xiàn)實(shí)
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
//這里就是傳遞進(jìn)來最多能緩存多少數(shù)據(jù)
????public LRUCache(int cacheSize) {
????????super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);//這塊就是設(shè)置一個hashmap的初始大小,同時最后一個true指的是讓linkedhashmap按照訪問順序來進(jìn)行排序,最近訪問的放在頭,最老訪問的就在尾
????????CACHE_SIZE = cacheSize;
????}
????@Override
????protected boolean removeEldestEntry(Map.Entry eldest) {
????????return size() > CACHE_SIZE;//這個意思就是說當(dāng)map中的數(shù)據(jù)量大于指定的緩存?zhèn)€數(shù)的時候,就自動刪除最老的數(shù)據(jù)
????}
}
我給你看上面的代碼,是告訴你最起碼你也得寫出來上面那種代碼,不求自己純手工從底層開始打造出自己的LRU,但是起碼知道如何利用已有的jdk數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)一個java版的LRU