NoSQL那些事--Redis
Redis是個流行的內(nèi)存數(shù)據(jù)庫(in-momery)。接口好用,性能也很強(qiáng),還支持多種數(shù)據(jù)結(jié)構(gòu),加上各種高可用性集群方案,實(shí)在是太太太好用了。
但是就是因?yàn)樘糜昧?#xff0c;好用到讓很多人都暈了腦子:
- 用Redis性能就大大提高了
- 用Redis可以保證原子性
- 用Redis可以實(shí)現(xiàn)事務(wù)
- 用Redis可以當(dāng)隊(duì)列
- ……
這就好像一個股民,在手機(jī)上操作買賣幾筆股票,賺了一些,然后感嘆道"股市就是為我發(fā)財而存在的啊"!!他的下場可想而知。
Redis的種種優(yōu)勢源自于他的設(shè)計——簡單直接的單線程內(nèi)存操作。但這些優(yōu)勢是有前提的。
Redis的性能高,嗎?
Redis的性能非常高。有些評測說用Redis可以達(dá)到幾十萬QPS(比如這里http://skipperkongen.dk/2013/08/27/how-many-requests-per-second-can-i-get-out-of-redis/)。大家可能在網(wǎng)文上記住了這個NB的數(shù)字,卻很少關(guān)心這個數(shù)值怎么來的。這就像是你買手機(jī)評測光看跑分一樣不靠譜。
Redis要達(dá)到高性能需要做到:
- Value盡可能的小。一般的測評都會用比較小的value,比如一個整數(shù)或者不長的字符串。但是如果用Redis做緩存,那么緩存的大小的可能偏離這個數(shù)字。比如一個頁面幾十KB;再比如,一個5年的市場價格序列數(shù)據(jù)可能高達(dá)幾MB。這么大的數(shù)據(jù)量在帶寬的限制下直接的效果就是QPS驟降,跌到幾百都毫不意外。畢竟Redis不是神仙,不能改變物理定律。此外,因?yàn)镽edis是單線程的,過大尺寸的數(shù)據(jù)訪問會block所有其他的操作。
- 使用Pipeline或Lua Script。Redis一般被用做網(wǎng)絡(luò)服務(wù)。所有的請求都是跨網(wǎng)絡(luò)進(jìn)行的。所以TCP Round Trip的長短對Redis的性能表現(xiàn)很重要。盡量減少Round Trip可以有效的提高吞吐。所以,通常的優(yōu)化方法是使用Pipeline,使得客戶端可以一次性把一組Redis命令發(fā)給Redis Server;或者預(yù)先在Redis Server中定義Lua Script,使用時直接調(diào)用。但無論是Pipeline還是Lua Script,都會受到業(yè)務(wù)需求的制約——不是所有業(yè)務(wù)都適合用Pipeline/Lua Script的。
- 使用快一些的網(wǎng)絡(luò)。很多Redis的測評為了彰顯其NB,都是在本地同時跑客戶端和服務(wù)器的。也就是說,它們要么使用了loopback網(wǎng)絡(luò)(localhost),要么使用了Unix Socket。這根本就不能反映一般分布式的網(wǎng)絡(luò)場景下的情況。同時,一些Redis的HA/Sharding方案會選擇用Twemproxy這樣的代理來實(shí)現(xiàn)。代理的加入會讓性能進(jìn)一步的打折扣。
- 不開啟RDB或者AOF。RDB和AOF是Redis的持久化方案。開啟他們會對Redis的性能表現(xiàn)有損耗。比如RDB在開始執(zhí)行時,會fork一個新的用于寫入rdb文件的進(jìn)程。這個fork的過程和內(nèi)存空間的復(fù)制會讓Redis卡頓一下;AOF每次sync數(shù)據(jù)到磁盤,也會block一小會。如果為了確保數(shù)據(jù)嚴(yán)格持久化,開啟了AOF的appendfsync=everysec設(shè)置,使得每個寫入指令都要立刻sync到磁盤,就會打破Redis快的前提——內(nèi)存數(shù)據(jù)操作。簡單來說,開啟任何一種持久化方案都會影響Redis的性能表現(xiàn)。
所以,如果想真實(shí)評價Redis的性能,一定要把你的場景設(shè)計好,然后用Redis自帶Redis-benchmark(見https://redis.io/topics/benchmarks),設(shè)定value的尺寸、要測試的Redis命令、和Pipeline的開啟情況,再把Redis Server按照生產(chǎn)環(huán)境的樣子配置好。然后跑一下壓測,看看Redis的實(shí)際表現(xiàn)到底是怎樣的。
Redis可以保證原子性,嗎?
我們先定義一下什么是原子性:
- 一般編程語言這么定義:原子性是指一組操作在執(zhí)行過程中,不受其他并發(fā)操作的干擾。這樣進(jìn)行的數(shù)據(jù)操作的值不會被相互覆蓋。
- 數(shù)據(jù)庫事務(wù)中ACID的A這么定義:原子性是指一組操作,要不完成,要不沒做,不存在改了一半的狀態(tài)。沒完成的操作可以回滾。
很顯然,Redis并不支持回滾,所以第二條肯定沒戲。
那么第一條呢?
Redis是單線程執(zhí)行的。在完成一個操作之前,不會有其他的操作被執(zhí)行。這的確是真的。但是,在業(yè)務(wù)開發(fā)中,需要的不是一個簡單操作的原子性,而需要實(shí)現(xiàn)一個臨界區(qū)的原子性。
業(yè)務(wù)中對數(shù)據(jù)的操作往往都不是簡單的一個set,一個incr就可以搞定的。一個復(fù)雜的業(yè)務(wù)邏輯,往往需要多個帶有邏輯判斷的寫入指令。業(yè)務(wù)中要保證的是這一組指令是原子的。比如下面的邏輯,希望一個value只能越設(shè)置越大。
(async function setBiggerV(v) { let currentV = parseInt(await redis.get('key')); if (currentV < v) { await redis.set('key', v); } })();這其實(shí)是有bug的,考慮到如下執(zhí)行序列(假設(shè)v一開始是5):
| 讀取key,得到5 | ? |
| ? | 讀取key,得到5 |
| ? | 設(shè)置key,為8 |
| 設(shè)置key,為7 | ? |
最終,Redis中v的值被設(shè)置為7,這就違反了這段邏輯的設(shè)計。如果這個機(jī)制被應(yīng)用于協(xié)調(diào)一個分布式系統(tǒng),那么整個系統(tǒng)就會因此掛掉。set這個命令是不是原子并不能讓這段業(yè)務(wù)代碼變成原子的。我們需要的是讓get和set這個整體原子。
在Redis中,可以用Redis事務(wù)或者Lua Script來實(shí)現(xiàn)原子性。Redis事務(wù)和Lua Script都可以保證一組指令執(zhí)行不受其他指令的打擾。比如上面的例子,用Lua Script實(shí)現(xiàn),就可以正確運(yùn)行。
但如果業(yè)務(wù)邏輯涉及到其他存儲,Redis事務(wù)和Lua Script就幫不上忙。比如,在Redis中放一個庫存的數(shù)字。用戶下單時,要在Redis中扣減庫存,并且在另外一個數(shù)據(jù)庫中INSERT一條交易記錄。這段邏輯是沒法做到原子的——除非你自行實(shí)現(xiàn)了某種分布式事務(wù)的機(jī)制。而分布式事務(wù)的實(shí)現(xiàn)復(fù)雜度往往會超過Redis帶來的好處。
用Redis可以實(shí)現(xiàn)事務(wù),嗎?
我們一般場景下說的事務(wù)的意思往往指的是數(shù)據(jù)庫系統(tǒng)中的”ACID事務(wù)“。(見https://www.jianshu.com/p/cb97f76a92fd)。ACID事務(wù)是計算機(jī)科學(xué)中一個非常重要的抽象。它極大地簡化了編寫業(yè)務(wù)代碼的難度。沒有ACID事務(wù),開發(fā)人員需要花大量精力處理由于并發(fā)和系統(tǒng)意外崩潰帶來的數(shù)據(jù)一致性問題。
Redis也有一個“事務(wù)”的概念。原文(見https://redis.io/topics/transactions)。大致含義是:Redis將MULTI指令和EXEC指令之間的多個指令視作一個事務(wù);一旦Redis看到了EXEC就開始執(zhí)行這一組指令,并保證執(zhí)行過程中不被打斷——除非Redis本身或者所在機(jī)器crash掉。如果發(fā)生了,就可能出現(xiàn)只有部分指令被執(zhí)行的情況。
所以,Redis事務(wù)與ACID事務(wù)是完全不同的!
Redis的事務(wù)只支持Isolation,不支持ACD。
有人說,AOF的appendfsync=everysec是可以持久化的。但這種持久化只在單機(jī)情況下有效。多機(jī)情況下,Redis是沒有一個機(jī)制能夠?qū)?shù)據(jù)修改同步sync到其他節(jié)點(diǎn)的,即便是Redis Cluster的WAIT指令也不行。
在這種限制下,在Redis中實(shí)現(xiàn)業(yè)務(wù)邏輯差不多就只有兩種可能:
- 不在意ACID事務(wù)——數(shù)據(jù)丟了沒事,改錯了也沒大關(guān)系
- 基于Redis的接口實(shí)現(xiàn)自己的ACID,或者ACID的某種子集
緩存屬于第一個場景。數(shù)據(jù)丟了沒事,從數(shù)據(jù)庫里重新加載就行了。
但如果是第二種場景,你要自己搞一個ACID。不是不可能,但要反復(fù)確認(rèn)這樣做的必要性。你是否具有專業(yè)的存儲開發(fā)技能,你能投入多少精力在ACID上,你的公司能給你多少資源做開發(fā)測試,這些都需要仔細(xì)考慮。
用Redis可以當(dāng)隊(duì)列,嗎?
Redis實(shí)現(xiàn)了一個List的數(shù)據(jù)結(jié)構(gòu)。借助它,可以實(shí)現(xiàn)出隊(duì),入隊(duì)的功能。實(shí)際上很多人早就熟練使用Redis做隊(duì)列。比如Sidekiq就是使用Redis作為異步j(luò)ob隊(duì)列的存儲。然而,這樣靠譜嗎?
靠譜不靠譜,得看你怎么定義“隊(duì)列”的要求:
- 隊(duì)列可不可能丟東西?比如,如果隊(duì)列短時間掛掉。此時,producer是必須停止服務(wù),還是繼續(xù)服務(wù)但不再插入隊(duì)列(這樣就會丟東西),或者說producer有某種機(jī)制可以在本地先暫時堆積一下,直到隊(duì)列恢復(fù)工作?
- 隊(duì)列的consumer是否需要一個“commit”的語義,表示處理完了一個事件?還是說,只要從隊(duì)列里取出來就可以了,萬一沒處理也沒所謂?
- 是否有事件重放的需要?比如上線了一個版本的consumer然后發(fā)現(xiàn)有bug,處理錯了3個小時的數(shù)據(jù)。修復(fù)后,希望能重新處理一遍之前出錯的數(shù)據(jù),那么這個隊(duì)列能不能做事件的”重放“?
- 如果consumer處理失敗怎么處理?是直接丟棄,還是重新插入到隊(duì)列中?
- 隊(duì)列是不是需要有最大的長度限制?如果到了最大長度,說明Consumer跟不上Producer的速度;此時,需要卡住Producer嗎?
- ……
Redis的List基本上對于所有這些問題都是完全不管的。也就是說,它不能給你任何的保證。更嚴(yán)重的是,就算你能接受一定程度的數(shù)據(jù)丟失,但是Redis無法告訴你他丟了多少東西,并且找不回來(MySQL還能翻翻binlog)!到最后,到底丟了多少,造成多少損失,是無法監(jiān)控,是無法衡量的。
在業(yè)務(wù)上,“保證”一個事情能夠發(fā)生相當(dāng)重要。試想一下,你的界面允許用戶下一筆訂單,用戶已經(jīng)看到了“成功下單”的界面,結(jié)果之后卻發(fā)現(xiàn)什么訂單也沒有。用戶是不是有一句MMP不知道當(dāng)講不當(dāng)講。
也許,你會說,"我的場景不需要這么嚴(yán)格的一致性,數(shù)據(jù)丟了沒所謂,也不需要事件重放,數(shù)據(jù)處理錯了就錯了"。這個Redis的確可以辦到,而且可以做得很好。但我建議你和你的產(chǎn)品經(jīng)理聊一下,看看需求是不是真的這樣。也許他會有不同的意見 ; - )
一般來講,一個技術(shù)公司需要兩大類“隊(duì)列”。一種是業(yè)務(wù)事件隊(duì)列。這種隊(duì)列絕對不能丟東西,而且可能需要exactly once語義,需要高可用。為了保證可用性,多節(jié)點(diǎn)的部署是必須的。而引入了多節(jié)點(diǎn),就必須解決復(fù)制的問題和分布式一致的問題,主從切換的問題,分片的問題等。這種隊(duì)列的典型代表是Rabbit MQ和Kafka。
另外一種隊(duì)列是收集服務(wù)前后端業(yè)務(wù)事件的隊(duì)列(比如登陸、注冊、下單成功、下單失敗……)。通過隊(duì)列,這些事件會被收集到數(shù)據(jù)分析中心,支持錯誤分析、客服、數(shù)據(jù)分析等功能。這種隊(duì)列可以容忍一些數(shù)據(jù)丟失,也能容忍數(shù)據(jù)延遲性比較大,但要求吞吐巨大。這種隊(duì)列的典型代表是Fluentd和Logstash。
也許你一開始在用Redis的List做隊(duì)列,但是如果這個業(yè)務(wù)是認(rèn)真的,你的系統(tǒng)一定會逐漸演進(jìn)到這二者之一。
Redis 4.2計劃引入Disque作為新的隊(duì)列實(shí)現(xiàn)。也許能夠扭轉(zhuǎn)這個情況。但4.2離發(fā)布還要很久,并且成熟到可以在生產(chǎn)使用,也至少要到4.4版本——大概在2019年甚至更晚。所以目前觀望一下就好,不必特別在意。
更新一下:Redis 5.0beta引入了Stream Date Type。實(shí)現(xiàn)了類似于Kafka的append only數(shù)據(jù)結(jié)構(gòu)和API。不過很可能要到5.2才能在生產(chǎn)中使用(2019年年底)。見https://redis.io/topics/streams-intro
Redis適合用來做什么?
在我看來,Redis適合以下場景:
- 共享Cache ,不怕丟數(shù)據(jù),丟了可以從DB中reload;
- 共享Session ,不怕丟數(shù)據(jù),丟了可以重新登錄;
- batch job的中間結(jié)果。不怕丟數(shù)據(jù),丟了重新跑job就可以了;
- 一些簡單數(shù)據(jù)的存儲,低頻改動,但是會被頻繁讀取。比如首頁推薦的產(chǎn)品列表。但此時必須增加HA的防護(hù),sentinel、cluster或者自定義的機(jī)制都可以;
- 一些更加復(fù)雜存儲的building block,比如分布式鎖,此時需要多節(jié)點(diǎn)來實(shí)現(xiàn)一個簡單的quorum
其他場景,往往有更好的、更成熟的方案。
特別注意,不要用Redis存儲任何需要“認(rèn)真對待”的數(shù)據(jù),請用支持ACID事務(wù)的數(shù)據(jù)庫。
Redis是非常優(yōu)秀的工具,但非是銀彈。只有認(rèn)真的了解業(yè)務(wù)對“保證”的要求,認(rèn)真的了解所用工具的工作原理,才能做出正確的設(shè)計決策。
轉(zhuǎn)載:https://www.jianshu.com/p/9cecff6042de
轉(zhuǎn)載于:https://www.cnblogs.com/boxy/p/11533553.html
總結(jié)
以上是生活随笔為你收集整理的NoSQL那些事--Redis的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: day52 Django全流程
- 下一篇: (附源码gitHub下载地址)sprin