Redis和數據庫 數據同步問題的解決

緩存充當數據庫

比如說Session這種訪問非常頻繁的數據,就適合采用這種方案;當然瞭,既然沒有涉及到數據庫,那麼也就不會存在一致性問題;

緩存充當數據庫熱點緩存

讀操作

目前的讀操作有個固定的套路,如下:

客戶端請求服務器的時候,發現如果服務器的緩存中存在,則直接取服務器的;

如果緩存中不存在,則去請求數據庫,並且將數據庫計算出來的數據回填給緩存;

返回數據給客戶端;

寫操作

各種情況會導致數據庫和緩存出現不一致的情況,這就是緩存和數據庫的雙寫一致性問題;

目前緩存存在三種策略,分別是

Cache Aside 更新策略:同時更新緩存和數據庫;

Read/Write Through 更新策略:先更新緩存,緩存負責同步更新數據庫;

Write Behind Caching 更新策略:先更新緩存,緩存定時異步更新數據庫;

三種策略各有優缺點,可以根據業務場景使用;

Cache Aside 更新策略

該策略大概的流程就是請求過來時先從緩存中取,如果命中緩存的話,則直接返回讀取的數據;相反如果沒有命中的話,接著會從數據庫中成功獲取到數據後,再去清除緩存中的數據;具體流程圖如下:

但是以上在某些特殊的情況下是存在問題:

問題1:先更新數據庫,後更新緩存

兩個線程在高並發的情況下就會可能出現數據臟讀的情況:

線程A執行寫操作,成功更新數據庫;

線程B同樣執行和線程A一樣的操作,但是在線程A執行更新緩存的過程中,線程B更新瞭新的數據庫數據到緩存中;

線程A在線程B全部操作完成以後才將相對老的數據又更新到瞭緩存中;

問題2:先刪除緩存,後更新數據庫

同樣的,在高並發場景下同樣會出現臟讀的情況:

線程A成功刪除瞭緩存,等待更新數據庫;

線程B進行讀操作,由於此時緩存已經被刪除瞭,因此線程B重新從數據庫中獲取老的數據並且更新到瞭緩存中;

線程A在線程B完成瞭整個的讀操作以後,才更新數據庫,此時緩存中的數據依舊是老的數據;

問題3:先更新數據庫,後刪除緩存

目前這是比較普遍的操作,即使它還是有可能會出現臟讀的情況:

線程A進行讀操作,此時正好沒有命中緩存,接著請求數據庫;

線程B進行寫操作,在線程A沒有從數據庫中獲取到數據之前,把數據寫入到數據庫中,並且還成功刪除瞭緩存;

線程A在線程B完成瞭整個的寫操作以後,才將相對老的數據更新到緩存中;

但是以上的情況比較不會出現,這是因為上述情況需要滿足線程A的讀操作要慢於線程B的寫操作,但是在現實過程中,讀操作通常都是要快於寫操作得多的,但是為瞭避免發生以上的情況,通常都是要給緩存加上一個過期的時間;

但是設想一下,如果上面的刪除緩存失敗瞭怎麼辦呢,這樣顯然會導致數據臟讀的情況,我覺得方案如下:

設置緩存的過期時間(必須要做);

提供一個保障重試機制,將哪些刪除失敗的key提供給消息隊列去消費;

從消息隊列取出這些key再次進行刪除,失敗再次加入到消息隊列中,超過一定次數以上則人工介入;

但是以上情況需要在業務代碼中進行操作,顯然得需要進行解耦;

目前我們公司就是使用該方案,具體過程為在更新數據庫數據的時候,數據庫會以binlog日志的形式保存下來,通過canal開源軟件將binlog解析成程序語言可以解析的地步,接著訂閱程序獲取到這些數據以後,嘗試刪除緩存操作,如果操作失敗的話,則將其加入到消息隊列中,重復消費,當刪除操作的失敗次數到達一定的次數以後,還是得人工介入。

Read/Write Through 更新策略

該模式下,程序隻需要維護緩存即可,數據庫的同步工作交由緩存來同步更新;

該策略具體又分為兩種:

Read Through:在查詢的過程中更新緩存;

Write Through:在寫操作的過程中如果命中緩存,則直接更新緩存,數據庫則由緩存自己同步去更新;

Write Behind Caching 更新策略

該策略隻更新緩存,不會立馬更新數據庫,隻會在一定的時間異步的批量去操作數據庫;這樣的好處在於直接操作緩存,效率極高,並且操作數據是異步的,還可以將多次的操作數據庫語句合並到一個事務中一起提交,因此效率很客觀;

但是,該策略沒有辦法做到數據強一致性,並且實現邏輯相對是比較復雜的,因為它需要確認哪些是需要更新到數據庫的,哪些是僅僅想要存儲在緩存中的;

比較

目前通常使用的是第一種策略中的先更新數據庫,後更新緩存;其他的相較比起來實現都比較復雜;

最後想說的是,緩存本來就是為瞭犧牲強一致性來提高性能的,所以肯定會存在一定的延遲時間,我們隻需要保證最終的數據一致性即可;

補充:redis數據的同步問題

修改redis.conf配置文件

vi redis.conf

在編輯模式下 輸入 /slaveof 來搜索

將slaveof啟用 即 將#刪除

依次配置所有 slave 並將進程 kill 掉 重啟

查看主從信息

redis 集群主從同步的簡單原理

Redis的復制功能是基於內存快照的持久化策略基礎上的,也就是說無論你的持久化策略選擇的是什麼,隻要用到瞭Redis的復制功能,就一定會有內存快照發生。

當Slave啟動並連接到Master之後,它將主動發送一個SYNC命令( 首先Master會啟動一個後臺進程,將數據快照保存到文件中[rdb文件] Master 會給Slave 發送一個

Ping命令來判斷Slave的存活狀態 當存活時 Master會將數據文件發送給Slave 並將所有寫命令發送到Slave )。

Slave首先會將數據文件保存到本地 之後再將 數據 加載到內存中。當第一次鏈接 或者是 故障後 重新連接 都會先判斷Slave的存活狀態 在做全部數據的同步 , 之後隻會同步Master的寫操作(將命令發送給Slave)

問題:

當 Master 同步數據時 若數據量較大 而Master本身隻會啟用一個後臺進程 來對多個Slave進行同步 , 這樣Master就會壓力過大 , 而且Slave 恢復的時間也會很慢!

redis 主從復制的優點:

(1)在一個Redis集群中,master負責寫請求,slave負責讀請求,這麼做一方面通過將讀請求分散到其他機器從而大大減少瞭master服務器的壓力,另一方面slave專註於提供讀服務從而提高瞭響應和讀取速度。

(2)在一個Redis集群中,如果master宕機,slave可以介入並取代master的位置,因此對於整個Redis服務來說不至於提供不瞭服務,這樣使得整個Redis服務足夠安全。

(3)水平增加Slave機器可以提高性能

Slave 默認是隻讀的更改:

Master 可以 讀寫(Write and Read) 而 Slave隻可以讀(read only默認情況)也可以更改 {但是開啟後Slave數據不會向上同步}

Redis的主從架構的兩種方式:

1.主從架構:

2.主從從架構:

備註:

因為Slave斷連,重連後仍然會全部同步數據,所以redis2.8版本後,增加瞭增量復制來解決宕機後重新鏈接仍然會全部同步!

Master會維護一個環形隊列:

隊列內存儲:

1》:slave連接master的id值 2》:slave上一次同步的最後一個命令這樣當斷開重連後就不會全部同步,而隻會在最後一個命令同步數據!

當你看到這些感到redis很好,有一點你要你記住,redis是基於內存的,內存是很珍貴的,公司不會花費大量的資源隻為瞭讓你玩這個架構,同時推薦memcached,這個成本就比較低瞭,因為它是基於磁盤的,當然效率就會比基於內存的redis低,同時也有和redis同樣設計風格的非關系型數據庫SSDB就比較友善瞭。 

SSDB和Redis的優缺點比較:

redis是內存數據庫,ssdb是面向硬盤的存儲,二者在存儲格式和讀寫方式上有著根本的不同。前面回答裡提到的zrevrange 和 zrevrangebyscore慢,而zrange 和 zrangebyscore 還能接受,其實就是說逆序遍歷比順序遍歷慢得多,其根本原因就在於逆序遍歷的時候,會多一個“記錄頭部”定位的過程,需要不斷嘗試去定位到兩條記錄的“分界點”,而順序遍歷的時候則不需要,因為讀完一條記錄直接就到瞭下一條記錄的“分界點”,並且像rocksdb之類的存儲引擎都會把數據長度保存在記錄的元信息裡,隻需要按長度讀取數據就可以瞭。

redis則不存在類似問題,因為它是完全基於指針和偏移量在內存中進行尋址來讀取數據的,尋址效率高瞭好多個數量級。

ssdb貌似就是一個個人項目,但代碼質量還是不錯的,整個設計思想比較簡潔。ssdb的主從復制效率很低。

binlog和數據是分開存儲的,日志冗餘較多,由於ssdb本身要在多線程條件下才能發揮出更好的性能,為瞭使多個線程在寫入binlog時能保證操作順序和原子性,ssdb的binlog數據結構上用瞭一把全局鎖,可想而知,這裡的鎖競爭會很影響性能。另外,ssdb默認也沒有集群管理的支持。

ssdb的好處,和swapdb一樣,都可以省錢。如果有需要,可以嘗試swapdb,它結合瞭redis和ssdb的優點,實現瞭基於LFU的熱度統計和冷熱交換,做到瞭低成本和高性能的高平衡。redis的好處,那就多瞭。

缺點就是純內存,比用SSD花錢。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。

推薦閱讀: