Redis 和 MySQL 如何保證數(shù)據(jù)一致性?
我們都知道,在開發(fā)過程中,很多時候都會用到我們的緩存,而緩存的種類也是五花八門的,我們今天來了解的就是關(guān)于緩存中的一種,那就是 Redis。
Redis
redis是一個key-value存儲系統(tǒng)。
和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。
這些數(shù)據(jù)類型都支持push/pop、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。
在此基礎(chǔ)上,redis支持各種不同方式的排序。與memcached一樣,為了保證效率,數(shù)據(jù)都是緩存在內(nèi)存中。
區(qū)別的是redis會周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎(chǔ)上實(shí)現(xiàn)了master-slave(主從)同步。
今天我們不說這個 Redis 的主從同步,我們來說說我們最常使用 Redis 的時候,會導(dǎo)致的一些問題。
Redis和 Mysql 如何保持一致
說到這個一致性,了不起就得和大家說道說道了,為什么會出現(xiàn)這種情況呢?實(shí)際上就是和 Redis 的使用有很大的關(guān)系。
都知道,Redis 是一個 NoSQL 的數(shù)據(jù)庫,而且他還很快,所以很多數(shù)據(jù)都會從 Mysql 中把數(shù)據(jù)取出來,然后放到我們的緩存中,然后下次讀取數(shù)據(jù)的時候,從 Redis 中直接去讀取,這個時候,我們就會出現(xiàn)問題了。
什么問題呢?
在高并發(fā)的業(yè)務(wù)場景下,數(shù)據(jù)庫大多數(shù)情況都是用戶并發(fā)訪問最薄弱的環(huán)節(jié)。所以,就需要使用redis做一個緩沖操作,讓請求先訪問到Redis,而不是直接訪問MySQL等數(shù)據(jù)庫。
這個業(yè)務(wù)場景,主要是解決讀數(shù)據(jù)從Redis緩存,一般都是按照下圖的流程來進(jìn)行業(yè)務(wù)操作。
讀取緩存步驟一般沒有什么問題,但是一旦涉及到數(shù)據(jù)更新:數(shù)據(jù)庫和緩存更新,就容易出現(xiàn)緩存Redis和數(shù)據(jù)庫MySQL間的數(shù)據(jù)一致性問題。
不管是先寫 MySQL數(shù)據(jù)庫,再刪除Redis緩存;還是先刪除緩存,再寫庫,都有可能出現(xiàn)數(shù)據(jù)不一致的情況。
舉一個例子:
1.如果刪除了緩存 Redis,還沒有來得及寫庫 MySQL,另一個線程就來讀取,發(fā)現(xiàn)緩存為空,則到數(shù)據(jù)庫中讀取數(shù)據(jù),寫入緩存,此時緩存中為臟數(shù)據(jù)。
2.如果先寫了庫,在刪除緩存前,寫庫的線程宕機(jī)了,沒有刪除掉緩存,則也會出現(xiàn)數(shù)據(jù)不一致情況。
因?yàn)閷懞妥x是并發(fā)的,沒法保證順序,就會出現(xiàn)緩存和數(shù)據(jù)庫的數(shù)據(jù)不一致的問題。
其實(shí)解決方案也有不是少,今天了不起來給大家分析一下 Redis 和 Mysql 保證數(shù)據(jù)一致性的實(shí)現(xiàn)方案。
緩存和數(shù)據(jù)庫一致性解決方案
延時雙刪策略
延時雙刪策略步驟如下:
- 1.先刪除緩存
- 2.再寫數(shù)據(jù)庫
- 3.休眠N毫秒
- 4.再次刪除緩存
其實(shí)我們可以來想一下,如果有三個線程,分別是 線程1 ,線程2,線程3,三個線程,
其中,線程1先刪除緩存;
線程2讀取緩存為null,同步db數(shù)據(jù)到緩存中;
線程1更新db中的數(shù)據(jù);
線程3查詢緩存中數(shù)據(jù)是舊數(shù)據(jù);
這樣的話,就會出現(xiàn) Mysql 和 Redis 中的數(shù)據(jù)不一致,這時候采用延遲雙刪策略,去保證數(shù)據(jù)的一致性,
這時候就有人問了,為什么要休眠一段時間,然后再執(zhí)行呢?
假象一下,如果沒有第三步操作時,有很大概率,在兩次刪除Redis操作執(zhí)行完畢之后,數(shù)據(jù)庫的數(shù)據(jù)還沒有更新,此時若有請求訪問數(shù)據(jù),便會出現(xiàn)我們一開始提到的數(shù)據(jù)不一致的問題。
為什么還要再次刪除緩存呢?
如果我們沒有第二次刪除操作,此時有請求訪問數(shù)據(jù),有可能是訪問的之前未做修改的 Redis 數(shù)據(jù),刪除操作執(zhí)行后,Redis為空,有請求進(jìn)來時,便會去訪問數(shù)據(jù)庫,此時數(shù)據(jù)庫中的數(shù)據(jù)已是更新后的數(shù)據(jù),保證了數(shù)據(jù)的一致性。
因?yàn)楦杏X這種延遲雙刪除可靠性并沒有那么高,因?yàn)槲覀儾⒉荒鼙WC刪除 Redis 成功,也不能保證數(shù)據(jù)庫更新也是成功的,也就是我們所說的原子性,兩個組合起來只是在理想情況下。
比如雙刪失敗我們應(yīng)該怎么處理呢?
1、設(shè)置緩存過期時間
從理論上來說,給緩存設(shè)置過期時間,是保證最終一致性的解決方案。所有的寫操作以數(shù)據(jù)庫為準(zhǔn),只要到達(dá)緩存過期時間,則后面的讀請求自然會從數(shù)據(jù)庫中讀取新值然后回填緩存。
結(jié)合雙刪策略+緩存超時設(shè)置,這樣最差的情況就是在超時時間內(nèi)數(shù)據(jù)存在不一致。
2、重試方案
重試方案有兩種實(shí)現(xiàn),一種在業(yè)務(wù)層做,另外一種實(shí)現(xiàn)中間件負(fù)責(zé)處理。
然而,該方案有一個缺點(diǎn),對業(yè)務(wù)線代碼造成大量的侵入。
流程如下:
1.更新數(shù)據(jù)庫數(shù)據(jù);
2.緩存因?yàn)榉N種問題刪除失敗;
3.將消費(fèi)消息,獲得需要刪除的key;
4.自己消費(fèi)消息,獲得需要刪除的key;
5.重試刪除操作,直到成功。
而這個放在業(yè)務(wù)層去處理的話,侵入太高,所以一般是不太推薦使用來解決這個問題。
但是呢,還有一個就是使用中間件來進(jìn)行處理。
啟動一個訂閱程序去訂閱數(shù)據(jù)庫的binlog,獲得需要操作的數(shù)據(jù)。在應(yīng)用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進(jìn)行刪除緩存操作。
流程如下:
1.更新數(shù)據(jù)庫數(shù)據(jù);
2.數(shù)據(jù)庫會將操作信息寫入binlog日志當(dāng)中;
3.訂閱程序提取出所需要的數(shù)據(jù)以及key;
4.另起一段非業(yè)務(wù)代碼,獲得該信息;
5.嘗試刪除緩存操作,發(fā)現(xiàn)刪除失??;
6.將這些信息發(fā)送至消息隊列;
7.重新從消息隊列中獲得該數(shù)據(jù),重試操作。
關(guān)于延遲雙刪除策略,你學(xué)會了么?