美團四面:如何保障 MySQL 和 Redis 的資料一致性?

gongmeng發表於2023-02-21

文章轉載於田螺大哥的這篇文章mp.weixin.qq.com/s/aHZWwoWIUFz0hzb...

大家好,我是田螺!

之前也看了很多相關的文章,但是感覺講的都不好,很多文章都會去講各種策略,比如(旁路快取)策略、(讀穿 / 寫穿)策略和(寫回)策略等,感覺意義真的不大,然後有的文章也只講了部分情況,也沒有告訴最優解。

我直接先拋一下結論:在滿足實時性的條件下,不存在兩者完全儲存一致的方案,只有最終一致性方案。 根據網上的眾多解決方案,總結出 6 種,直接看目錄

美團四面:如何保障 MySQL 和 Redis 的資料一致性?

1. 先寫 MySQL,再寫 Redis

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
圖解說明:

  • 這是一副時序圖,描述請求的先後呼叫順序;
  • 橘黃色的線是請求 A,黑色的線是請求 B;
  • 橘黃色的文字,是 MySQL 和 Redis 最終不一致的資料;
  • 資料是從 10 更新為 11;
  • 後面所有的圖,都是這個含義,不再贅述。

請求 A、B 都是先寫 MySQL,然後再寫 Redis,在高併發情況下,如果請求 A 在寫 Redis 時卡了一會,請求 B 已經依次完成資料的更新,就會出現圖中的問題。

這個圖已經畫的很清晰了,我就不用再去囉嗦了吧,不過這裡有個前提,就是對於讀請求,先去讀 Redis,如果沒有,再去讀 DB,但是讀請求不會再回寫 Redis。 大白話說一下,就是讀請求不會更新 Redis。

2. 先寫 Redis,再寫 MySQL

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
同“先寫 MySQL,再寫 Redis”,看圖可秒懂。

3. 先刪除 Redis,再寫 MySQL

這幅圖和上面有些不一樣,前面的請求 A 和 B 都是更新請求,這裡的請求 A 是更新請求,但是請求 B 是讀請求,且請求 B 的讀請求會回寫 Redis。

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
請求 A 先刪除快取,可能因為卡頓,資料一直沒有更新到 MySQL,導致兩者資料不一致。

這種情況出現的機率比較大,因為請求 A 更新 MySQL 可能耗時會比較長,而請求 B 的前兩步都是查詢,會非常快。

4. 先刪除 Redis,再寫 MySQL,再刪除 Redis

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
為了便於大家看圖,對於藍色的文字,“刪除快取 10”必須在“回寫快取10”後面,那如何才能保證一定是在後面呢?網上給出的第一個方案是,讓請求 A 的最後一次刪除,等待 500ms。

對於這種方案,看看就行,反正我是不會用,太 Low 了,風險也不可控。

那有沒有更好的方案呢,我建議非同步序列化刪除,即刪除請求入佇列

美團四面:如何保障 MySQL 和 Redis 的資料一致性?

非同步刪除對線上業務無影響,序列化處理保障併發情況下正確刪除。

如果雙刪失敗怎麼辦,網上有給 Redis 加一個快取過期時間的方案,這個不敢苟同。個人建議整個重試機制,可以藉助訊息佇列的重試機制,也可以自己整個表,記錄重試次數,方法很多。

簡單小結一下:

  • “快取雙刪”不要用無腦的 sleep 500 ms;
  • 透過訊息佇列的非同步&序列,實現最後一次快取刪除;
  • 快取刪除失敗,增加重試機制。

5. 先寫 MySQL,再刪除 Redis

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
對於上面這種情況,對於第一次查詢,請求 B 查詢的資料是 10,但是 MySQL 的資料是 11,只存在這一次不一致的情況,對於不是強一致性要求的業務,可以容忍。(那什麼情況下不能容忍呢,比如秒殺業務、庫存服務等。)
當請求 B 進行第二次查詢時,因為沒有命中 Redis,會重新查一次 DB,然後再回寫到 Reids。

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
這裡需要滿足 2 個條件:

  • 快取剛好自動失效;

  • 請求 B 從資料庫查出 10,回寫快取的耗時,比請求 A 寫資料庫,並且刪除快取的還長。

對於第二個條件,我們都知道更新 DB 肯定比查詢耗時要長,所以出現這個情況的機率很小,同時滿足上述條件的情況更小。

6. 先寫 MySQL,透過 Binlog,非同步更新 Redis

這種方案,主要是監聽 MySQL 的 Binlog,然後透過非同步的方式,將資料更新到 Redis,這種方案有個前提,查詢的請求,不會回寫 Redis。

美團四面:如何保障 MySQL 和 Redis 的資料一致性?
這個方案,會保證 MySQL 和 Redis 的最終一致性,但是如果中途請求 B 需要查詢資料,如果快取無資料,就直接查 DB;如果快取有資料,查詢的資料也會存在不一致的情況。

所以這個方案,是實現最終一致性的終極解決方案,但是不能保證實時性。

我們對比上面討論的 6 種方案:

  1. 先寫 Redis,再寫 MySQL
    • 這種方案,我肯定不會用,萬一 DB 掛了,你把資料寫到快取,DB 無資料,這個是災難性的;
    • 我之前也見同學這麼用過,如果寫 DB 失敗,對 Redis 進行逆操作,那如果逆操作失敗呢,是不是還要搞個重試?
  2. 先寫 MySQL,再寫 Redis
    • 對於併發量、一致性要求不高的專案,很多就是這麼用的,我之前也經常這麼搞,但是不建議這麼做;
    • 當 Redis 瞬間不可用的情況,需要報警出來,然後線下處理。
  3. 先刪除 Redis,再寫 MySQL
    • 這種方式,我還真沒用過,直接忽略吧。
  4. 先刪除 Redis,再寫 MySQL,再刪除 Redis
    • 這種方式雖然可行,但是感覺好複雜,還要搞個訊息佇列去非同步刪除 Redis。
  5. 先寫 MySQL,再刪除 Redis
    • 比較推薦這種方式,刪除 Redis 如果失敗,可以再多重試幾次,否則報警出來;
    • 這個方案,是實時性中最好的方案,在一些高併發場景中,推薦這種。
  6. 先寫 MySQL,透過 Binlog,非同步更新 Redis
    • 對於異地容災、資料彙總等,建議會用這種方式,比如 binlog + kafka,資料的一致性也可以達到秒級;
    • 純粹的高併發場景,不建議用這種方案,比如搶購、秒殺等。

個人結論:

  • 實時一致性方案:採用“先寫 MySQL,再刪除 Redis”的策略,這種情況雖然也會存在兩者不一致,但是需要滿足的條件有點苛刻,所以是滿足實時性條件下,能儘量滿足一致性的最優解。

  • 最終一致性方案:採用“先寫 MySQL,透過 Binlog,非同步更新 Redis”,可以透過 Binlog,結合訊息佇列非同步更新 Redis,是最終一致性的最優解。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章