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