-? ? ? 01、不一樣的Redis? ? -
提到Redis,大家一定會想到的幾個點是什么呢?
高并發(fā)、KV存儲、內(nèi)存數(shù)據(jù)庫、豐富的數(shù)據(jù)結(jié)構(gòu)、單線程(版本6之前)等。
那么,接下來,上面提到的這些,都會一一給大家解答,帶大家系統(tǒng)剖析一下Redis的架構(gòu)設(shè)計魅力!
-? ? ? 02、為什么會出現(xiàn)緩存?? ? -
一般情況下,數(shù)據(jù)都是在數(shù)據(jù)庫中,應(yīng)用系統(tǒng)直接操作數(shù)據(jù)庫。當(dāng)訪問量上萬,數(shù)據(jù)庫壓力增大,這個時候,怎么辦呢?
有小伙伴會說,分庫分表、讀寫分離。的確,這些確實是解決比較高的訪問量的解決辦法,但是,如果訪問量更大,10萬,100萬呢?怎么分似乎都不解決問題吧,所以我們需要用到其他辦法,來解決高并發(fā)帶來的數(shù)據(jù)庫壓力。
這個時候,緩存出現(xiàn)了,顧名思義,就是先把數(shù)據(jù)緩存在內(nèi)存中一份,當(dāng)訪問的時候,我們會先訪問內(nèi)存的數(shù)據(jù),如果內(nèi)存中的數(shù)據(jù)不存在,這個時候,我們再去讀取數(shù)據(jù)庫,之后把數(shù)據(jù)庫中的數(shù)據(jù)再備份一份到內(nèi)存中,這樣下次讀請求過來的時候,還是會直接先從內(nèi)存中訪問,訪問到內(nèi)存的數(shù)據(jù)了之后就直接返回了。這樣做就完美的降低了數(shù)據(jù)庫的壓力,可能十萬個請求進來,全部都訪問了內(nèi)存中備份的數(shù)據(jù),而沒有去訪問數(shù)據(jù)庫,或者說只有少量的請求訪問到了數(shù)據(jù)庫,這樣真的是大大降低了數(shù)據(jù)庫的壓力,而且這樣做也提高了系統(tǒng)響應(yīng),大家想一下,內(nèi)存的讀寫速度是遠(yuǎn)遠(yuǎn)大于硬盤的讀寫速度的,一個請求進來讀取的內(nèi)存可以比讀取硬盤快很多很多,用戶的體驗也會很高。
-? ? ? 03、什么是緩存?? ? -
緩存原指CPU上的一種高速存儲器,它先于內(nèi)存與CPU交換數(shù)據(jù),速度很快。
現(xiàn)在泛指存儲在計算機上的原始數(shù)據(jù)的復(fù)制集,便于快速訪問。
在互聯(lián)網(wǎng)技術(shù)中,緩存是系統(tǒng)快速響應(yīng)的關(guān)鍵技術(shù)之一。
-? ? ? 04、緩存的三種讀寫模式? ? -
1、Cache Aside Pattern(常用)
Cache Aside Pattern(旁路緩存),是最經(jīng)典的緩存+數(shù)據(jù)庫讀寫模式。
讀的時候,先讀緩存,緩存沒有的話,就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)。
更新的時候,先更新數(shù)據(jù)庫,然后再刪除緩存。
為什么是刪除緩存,而不是更新緩存呢?
1、緩存的值是一個結(jié)構(gòu),hash、list等更新數(shù)據(jù)需要遍歷;
2、懶加載,使用的時候才更新緩存,也可以采用異步的方式填充緩存。
高并發(fā)臟讀的三種情況:
1、先更新數(shù)據(jù)庫,在更新緩存;
update與commit之間,更新緩存,commit失敗,則DB與緩存數(shù)據(jù)不一致。
2、先刪除緩存,再更新數(shù)據(jù)庫
update與commit之間,有新的讀,緩存空,讀DB數(shù)據(jù)到緩存,數(shù)據(jù)是舊的數(shù)據(jù);
commit后DB為新的數(shù)據(jù);
則DB與緩存數(shù)據(jù)不一致。
3、先更新數(shù)據(jù)庫,再刪除緩存(推薦)
update與commit之間,有新的讀,緩存空,讀DB數(shù)據(jù)到緩存,數(shù)據(jù)是舊的數(shù)據(jù);
commit后DB為新的數(shù)據(jù);
則DB與緩存數(shù)據(jù)不一致;
采用延時雙刪策略。
2、Read/Write Through Pattern
應(yīng)用程序只操作緩存,緩存操作數(shù)據(jù)庫;
Read-Through(穿透讀模式/直讀模式):應(yīng)用程序讀緩存,緩存沒有,由緩存回源到數(shù)據(jù)庫,并寫入緩存;
Write-Through(穿透寫模式/直寫模式):應(yīng)用程序?qū)懢彺?,緩存寫?shù)據(jù)庫。該種模式需要提供數(shù)據(jù)庫的handler,開發(fā)較為復(fù)雜。
3、Write Behind Caching Pattern
應(yīng)用程序只更新緩存;
緩存通過異步的方式將數(shù)據(jù)批量或合并后更新到DB中,不能時時同步,甚至?xí)G數(shù)據(jù)。
-? ? ? 05、Redis又是什么?? ? -
Redis是一個高性能的開源的,C語言寫的NoSQL(非關(guān)系型數(shù)據(jù)庫)也叫做緩存數(shù)據(jù)庫,數(shù)據(jù)保存在內(nèi)存中。Redis是以key-value形式存儲,和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫不一樣。不一定遵循傳統(tǒng)數(shù)據(jù)庫的那些基本要求。比如,不遵循SQL標(biāo)準(zhǔn)、事務(wù)、表結(jié)構(gòu)等。Redis有非常豐富的數(shù)據(jù)類型,比如String,list,set,zset,hash等。
-? ? ? 06、Redis可以做什么?? ? -
1、減輕數(shù)據(jù)庫壓力,提高并發(fā)量,提高系統(tǒng)響應(yīng)時間
2、做Session分離
傳統(tǒng)的Session是由自己的tomcat進行維護和管理的,在集群和分布式情況下,不同的tomcat要管理不同的session,只能在各個tomcat之間,通過網(wǎng)絡(luò)和IO進行session復(fù)制,極大的影響了系統(tǒng)的性能。
Redis解決了這一個問題,將登陸成功后的session信息,存放在Redis中,這樣多個tomcat就可以共享Session信息。
3、做分布式鎖
一般Java中的鎖都是多線程鎖,是在一個進程中的,多個進程在并發(fā)的時候也會產(chǎn)生問題,也要控制時序性,這個時候Redis可以用來做分布式鎖,使用Redis的setnx命令來實現(xiàn)。
4、電商購物車:
1、以用戶id為key
2、商品id為field
3、商品數(shù)量為value
電商購物車操作:
1、添加商品:hset cart:1001 10088 1
2、增加數(shù)量:hincrby cart:1001 10088 1
3、商品總數(shù):hlen cart:1001
4、刪除商品:hdel cart:1001 10088
5、獲取購物車所有商品:hgetall cart:1001
5、zset集合操作實現(xiàn)排行榜
1、點擊新聞
ZINCRBY hotNews:20190819 1 守護香港
2、展示當(dāng)日排行前十
ZREVRANGE hotNews:20190819 0 9 WITHSCORES
3、七日搜索榜單計算
ZUNIONSTORE hotNews:20190813-20190819 7
hotNews:20190813 hotNews:20190814... hotNews:20190819
4、展示七日排行前十
ZREVRANGE hotNews:20190813-201908109 0 9 WITHSCORES
用Redis做緩存,有這么有多優(yōu)點,那么,缺點是不是也會對應(yīng)的有很多呢?
1、額外的硬件支出
緩存是一種軟件系統(tǒng)中以空間換時間的技術(shù),需要額外的磁盤空間和內(nèi)存空間來存儲數(shù)據(jù)。
2、高并發(fā)緩存失效
在高并發(fā)的情況下,會出現(xiàn)緩存失效(緩存穿透,緩存雪崩,緩存擊穿等問題)造成瞬間數(shù)據(jù)庫訪問量增大,甚至崩潰,所以這些問題是一定要去解決。
3、緩存與數(shù)據(jù)庫數(shù)據(jù)同步
緩存與數(shù)據(jù)庫無法做到數(shù)據(jù)的實時同步。
4、緩存并發(fā)競爭
多個Redis客戶端同時對一個key進行set值的時候由于執(zhí)行順序引起的并發(fā)的問題。
-? ? ? 07、Redis高性能設(shè)計? ? -
1、Redis是單線程的么?
Redis的單線程主要是指Redis的網(wǎng)絡(luò)IO和鍵值對讀寫是由一個線程來完成的,這也是Redis對外提供鍵值存儲服務(wù)的主要流程。但Redis的其他功能,比如持久化,異步刪除,集群數(shù)據(jù)同步等,都是由額外的線程執(zhí)行的。
2、Redis單線程為什么還能這么快?
這里我們在本地測試一下Redis支持的并發(fā)。
執(zhí)行這條命令:./redis-benchmark get
結(jié)果:
============ get ==========
100000 requests completed in 1.02 seconds
50 parallel clients
3 bytes payload
keep alive: 1
host configuration "save": 900 1 300 10 60 10000
host configuration "appendonly": nomulti-thread: no
0.00% <= 0.1 milliseconds
13.00% <= 0.2 milliseconds
13.00% <= 0.2 milliseconds
55.85% <= 0.3 milliseconds
80.60% <= 0.4 milliseconds
92.57% <= 0.5 milliseconds
97.12% <= 0.6 milliseconds
99.06% <= 0.7 milliseconds
99.68% <= 0.8 milliseconds
99.86% <= 0.9 milliseconds
99.90% <= 1.0 milliseconds
99.90% <= 1.1 milliseconds
55.85% <= 0.3 milliseconds
80.60% <= 0.4 milliseconds
92.57% <= 0.5 milliseconds
97.12% <= 0.6 milliseconds
99.06% <= 0.7 milliseconds
99.68% <= 0.8 milliseconds
99.86% <= 0.9 milliseconds
99.90% <= 1.0 milliseconds
99.90% <= 1.1 milliseconds
99.90% <= 1.2 milliseconds
99.91% <= 1.3 milliseconds
99.93% <= 1.4 milliseconds
99.95% <= 1.5 milliseconds
99.97% <= 1.6 milliseconds
99.98% <= 1.7 milliseconds
99.99% <= 1.8 milliseconds
99.99% <= 1.9 milliseconds
100.00% <= 2 milliseconds
100.00% <= 2 milliseconds
98328.42 requests per second
這里我們可以看到,每秒的話,差不多可以支持小10萬的并發(fā),這已經(jīng)是一個很恐怖的數(shù)據(jù)了。
因為它的所有數(shù)據(jù)都在內(nèi)存中,所有的運算都是內(nèi)存級別的運算,而且單線程避免了多線程的切換性能消耗問題。正因為Redis是單線程的,所以要小心使用Redis命令,對于那些耗時的指令(比如keys),一定要謹(jǐn)慎使用,一不小心就可能導(dǎo)致Redis卡頓。
Redis單線程如何處理那么多并發(fā)客戶端連接?
Redis的IO多路復(fù)用:Redis利用epoll來實現(xiàn)IO多路復(fù)用,將連接信息和事件放到隊列中,一次放到文件事件分派器,事件分派器將事件分發(fā)給事件處理器。
-? ? ? 08、Redis核心設(shè)計原理? ? -
Redis作為key-value存儲系統(tǒng),數(shù)據(jù)結(jié)構(gòu)如下:
一個Redis實例對應(yīng)多個DB,一個DB對應(yīng)多個key,key一般都是string的,后面的value叫做RedisObject,不是說value就是string,list,map這些,而是說這些所有的類型,都被Redis封裝成了一個叫RedisObjcet,具體是哪個類型呢?這里是用指針的方式來指向具體是哪個類型。
為什么要這么做,主要是為了提高Redis的性能。
PS:這里插一句,為什么使用指針的方式要比使用對象本身的方式性能更好呢?
這里有兩點:
第一點是動態(tài)分配;第二是指針一大特點在于你只需要在前面聲明一下指針指向的類型(而如果要使用實際的對象,你還需要定義一下)。這樣你就能降低你的編譯單元之間的耦合性從而減少編譯時間。
1、RedisDB結(jié)構(gòu)
Redis沒有表的概念,Redis實例所對應(yīng)的DB以編號區(qū)分,DB本身就是key的命名空間
比如:user:1000作為key的值,表示在user這個命名空間下id為1000的元素,類似于user表的id=1000的行。
2、SDS字符串
眾所周知,Redis是用C語言來實現(xiàn)的,在C語言中,String這個類型,其實就是一個char數(shù)組,比如char data[]="xxx\0",但是,客戶端往Redis發(fā)送set命令,是可以發(fā)任意的字符串的,是沒有校驗的,所以假如我們發(fā)了一個字符串xx\0xx,那么\0后面的xx是不會讀的,只會讀前面的xx(C語言中用"\0"表示字符串結(jié)束,如果字符串本身就有"\0"字符,字符串就會被截斷)。
所以Redis自實現(xiàn)了一個string叫sds,sds中記錄了一個len和一個char buf[],len用來記錄buf的長度,比如char buf[] = "xx\0xx",那么len就是5,sds中還有一個比較重要的屬性就是free,表示還剩余多少。
free是通過改變len來計算,比如"xxx1234" 改成 "xxx123456",那么會按照(len+addlen)*2=18 來擴容,這個時候len變成了9,free就是18-9也變成了9。
例如:
char buf[] = "xxx1234" 改成 "xxx123456" //這里的buf是柔性數(shù)組
free:12 變成free:10
len:8 變成len:10
Redis這樣設(shè)計SDS有什么好處:
1、二進制安全的數(shù)據(jù)結(jié)構(gòu);
2、提供了內(nèi)存預(yù)分配機制,避免了頻繁的內(nèi)存分配;
3、兼容C語言的函數(shù)庫;
4、有單獨的統(tǒng)計變量len和free,可以方便的得到字符串長度,這樣就避免了讀取不完整的風(fēng)險;
5、內(nèi)容存放在柔性數(shù)組buf中,SDS對上層暴露的指針不是指向結(jié)構(gòu)體SDS的指針,而是直接指向柔性數(shù)組buf的指針。上層可像讀取C字符串一樣讀取SDS的內(nèi)容,兼容C語言處理字符串的各種函數(shù)。
這里解釋一下什么叫柔型數(shù)組?
柔型數(shù)組即數(shù)組大小待定的數(shù)組,C語言中結(jié)構(gòu)體的最后一個元素可以是大小未知的數(shù)組,也就是所謂的0長度,所以我們可以用結(jié)構(gòu)體來創(chuàng)建柔性數(shù)組。柔性數(shù)組主要用途是為了滿足需要變長度的結(jié)構(gòu)體,為了解決使用數(shù)組時內(nèi)存的冗余和數(shù)組的越界問題
這也是Redis3.2之前所實現(xiàn)的。
未完待續(xù),敬請期待下篇分析。
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!