偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

緩存一致:讀多寫少時,如何解決數(shù)據(jù)更新緩存不同步?

開發(fā) 前端
臨時緩存是有 TTL 的,如果 60 秒內(nèi)修改了用戶的昵稱,緩存是不會馬上更新的。最糟糕的情況是在 60 秒后才會刷新這個用戶的昵稱緩存,顯然這會給系統(tǒng)帶來一些不必要的麻煩。其實對于這種緩存數(shù)據(jù)刷新,可以分成幾種情況,不同情況的刷新方式有所不同,接下來我給你分別講講。

我們之前提到過,互聯(lián)網(wǎng)大多數(shù)業(yè)務(wù)場景的數(shù)據(jù)都屬于讀多寫少,在請求的讀寫比例中,寫的比例會達(dá)到百分之一,甚至千分之一。而對于用戶中心的業(yè)務(wù)來說,這個比例會更大一些,畢竟用戶不會頻繁地更新自己的信息和密碼,所以這種讀多寫少的場景特別適合做讀取緩存。通過緩存可以大大降低系統(tǒng)數(shù)據(jù)層的查詢壓力,擁有更好的并發(fā)查詢性能。但是,使用緩存后往往會碰到更新不同步的問題,下面我們具體看一看。

緩存性價比

是的,緩存的確有可能被濫用,特別是在像用戶中心這樣對數(shù)據(jù)準(zhǔn)確性要求很高的場景中。你提到在對用戶中心進行優(yōu)化時,首要想到的就是將用戶信息放入緩存,以提高性能。這確實是一個常見的優(yōu)化思路,因為緩存能夠顯著減少數(shù)據(jù)庫的訪問頻率,提升系統(tǒng)響應(yīng)速度。

# 表結(jié)構(gòu)
CREATE TABLE `accounts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`account` varchar(15) NOT NULL DEFAULT '',
`password` char(32) NOT NULL,
`salt` char(16) NOT NULL,
`status` tinyint(3) NOT NULL DEFAULT '0'
`update_time` int(10) NOT NULL DEFAULT '0',
`create_time` int(10) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


# 登錄查詢
select id, account, update_time from accounts 
where account = 'user1'
and password = '6b9260b1e02041a665d4e4a5117cfe16'
and status = 1

確實,這是一個簡單的查詢需求。乍一看,似乎將 2000 萬條用戶數(shù)據(jù)都放入緩存可以極大地提升性能,但實際上并不完全如此。雖然緩存能提供高性能的服務(wù),但其性價比并不一定高。這個表主要用于賬號登錄的查詢,而登錄操作本身即使頻繁,也不會對系統(tǒng)帶來巨大的流量壓力。因此,即便將所有用戶數(shù)據(jù)放入緩存,大部分時間這些數(shù)據(jù)都處于閑置狀態(tài)。這樣一來,緩存資源反而被浪費,我們也不必要將并發(fā)量不高的數(shù)據(jù)緩存起來,從而增加預(yù)算開銷。

這就引出一個核心問題:緩存的使用需要考慮性價比。如果花費大量時間和資源將某些數(shù)據(jù)放入緩存,但對系統(tǒng)性能并沒有顯著的提升,甚至增加了額外的成本,那么這樣的緩存策略就是不合理的。緩存的效果需要經(jīng)過評估,通常來說,只有熱點數(shù)據(jù)才值得放入緩存。

臨時熱緩存

在推翻了將所有賬號信息都放入緩存的方案后,我們將目標(biāo)轉(zhuǎn)向那些被頻繁查詢的信息上,比如用戶信息。用戶信息的使用頻率非常高,尤其是在論壇等場景中,常常需要頻繁展示,例如用戶的頭像、昵稱和性別等。不過,由于這些數(shù)據(jù)量較大,全部緩存起來不僅浪費空間,還不具備性價比。

針對這種情況,我們可以考慮使用一種臨時緩存的策略:當(dāng)某個用戶信息首次被訪問時,將其存入緩存;在短時間內(nèi),若有類似查詢請求,就可以直接從緩存中獲取。這樣既可以有效地降低數(shù)據(jù)庫查詢壓力,又不會占用過多的緩存空間。以下是一個常用的實現(xiàn)臨時緩存的代碼示例:

# 示例代碼
def get_user_info(user_id):
# 首先嘗試從緩存中獲取用戶信息
    user_info = cache.get(user_id)
if user_info:
return user_info


# 如果緩存中沒有,查詢數(shù)據(jù)庫
    user_info = db.query_user_info(user_id)


# 將查詢到的信息存入緩存,并設(shè)置一個合理的過期時間
    cache.set(user_id, user_info, timeout=300)  # 緩存五分鐘
return user_info

正如我們看到的,這種策略將數(shù)據(jù)臨時放入緩存,在 60 秒過期后自動淘汰。如果在這段時間內(nèi)再次查詢相同數(shù)據(jù),我們的代碼會重新將數(shù)據(jù)填入緩存,繼續(xù)提供使用。這種臨時緩存策略非常適合數(shù)據(jù)量大但熱點數(shù)據(jù)較少的場景,有助于緩解數(shù)據(jù)庫的查詢壓力。

設(shè)置緩存的 TTL(Time-to-Live)是為了更有效地利用內(nèi)存資源。當(dāng)數(shù)據(jù)在指定時間內(nèi)未被再次訪問,就會被自動清除,這樣我們就能避免購買過多內(nèi)存。通過這種方式,可以在節(jié)省成本的同時,提高緩存的性價比,且實現(xiàn)起來簡單,維護也方便,是一種很常用的策略

緩存更新不及時問題

臨時緩存是有 TTL 的,如果 60 秒內(nèi)修改了用戶的昵稱,緩存是不會馬上更新的。最糟糕的情況是在 60 秒后才會刷新這個用戶的昵稱緩存,顯然這會給系統(tǒng)帶來一些不必要的麻煩。其實對于這種緩存數(shù)據(jù)刷新,可以分成幾種情況,不同情況的刷新方式有所不同,接下來我給你分別講講。

1. 單條實體數(shù)據(jù)緩存刷新

單條實體數(shù)據(jù)緩存更新是最簡單的一個方式,比如我們緩存了 9527 這個用戶的 info 信息,當(dāng)我們對這條數(shù)據(jù)做了修改,我們就可以在數(shù)據(jù)更新時同步更新對應(yīng)的數(shù)據(jù)緩存:

Type UserInfo struct {
  Id         int    `gorm:"column:id;type:int(11);primary_key;AUTO_INCREMENT" json:"id"`
  Uid        int    `gorm:"column:uid;type:int(4);NOT NULL" json:"uid"`
  NickName   string `gorm:"column:nickname;type:varchar(32) unsigned;NOT NULL" json:"nickname"`
  Status     int16  `gorm:"column:status;type:tinyint(4);default:1;NOT NULL" json:"status"`
  CreateTime int64  `gorm:"column:create_time;type:bigint(11);NOT NULL" json:"create_time"`
  UpdateTime int64  `gorm:"column:update_time;type:bigint(11);NOT NULL" json:"update_time"`
}


//更新用戶昵稱
func (m *UserInfo)UpdateUserNickname(ctx context.Context, name string, uid int) (bool, int64, error) {
//先更新數(shù)據(jù)庫
  ret, err := m.db.UpdateUserNickNameById(ctx, uid, name)
if ret {
//然后清理緩存,讓下次讀取時刷新緩存,防止并發(fā)修改導(dǎo)致臨時數(shù)據(jù)進入緩存
//這個方式刷新較快,使用很方便,維護成本低
    Redis.Del("user_info_" + strconv.Itoa(uid))
  }
return ret, count, err
}

總體來說,我們可以先識別出被修改的數(shù)據(jù) ID,然后根據(jù)這些 ID 刪除相應(yīng)的數(shù)據(jù)緩存。在下次請求到來時,系統(tǒng)會重新獲取最新的數(shù)據(jù)并更新到緩存中,這樣可以有效減少并發(fā)操作將臟數(shù)據(jù)寫入緩存的可能性。

除了這種方法,我們還可以向隊列發(fā)送更新消息,讓子系統(tǒng)處理更新,或者開發(fā)中間件,將數(shù)據(jù)操作發(fā)送到子系統(tǒng),讓其自行決定需要更新的數(shù)據(jù)范圍。然而,通過隊列更新消息時,我們可能會遇到一個問題——條件批量更新時,可能無法直接確定具體有多少個 ID 發(fā)生了變化。常見的解決方法是:首先按照相同的條件查詢出所有受影響的 ID,然后執(zhí)行更新操作,最后使用這些相關(guān)的 ID 更新具體的緩存。

2. 關(guān)系型和統(tǒng)計型數(shù)據(jù)緩存刷新

首先,有一種人工維護緩存的方式。眾所周知,關(guān)系型數(shù)據(jù)或統(tǒng)計結(jié)果的緩存刷新具有一定的難度,主要原因在于這些統(tǒng)計數(shù)據(jù)通常是基于多條數(shù)據(jù)計算得出的。當(dāng)我們需要刷新這類數(shù)據(jù)的緩存時,很難準(zhǔn)確識別出需要更新的關(guān)聯(lián)緩存。

為了解決這個問題,可以通過人工方式,在集中管理的地方記錄或定義特定的刷新邏輯,以實現(xiàn)關(guān)聯(lián)緩存的更新。

圖片圖片

不過這種方式比較精細(xì),如果刷新緩存很多,那么緩存更新會比較慢,并且存在延遲。而且人工書寫還需要考慮如何查找到新增數(shù)據(jù)關(guān)聯(lián)的所有 ID,因為新增數(shù)據(jù)沒有登記在 ID 內(nèi),人工編碼維護會很麻煩。除了人工維護緩存外,還有一種方式就是通過訂閱數(shù)據(jù)庫來找到 ID 數(shù)據(jù)變化。如下圖,我們可以使用 Maxwell 或 Canal,對 MySQL 的更新進行監(jiān)控。

圖片圖片

在這種方案中,變更信息會被推送到 Kafka。我們可以根據(jù)表名和具體的 SQL 確認(rèn)哪些數(shù)據(jù) ID 發(fā)生了更新,然后依據(jù)腳本中設(shè)定的邏輯,對相關(guān)緩存 key 進行更新。比如,當(dāng)用戶更新了昵稱,緩存更新服務(wù)就能夠識別需要更新 user_info_9527 這個緩存,同時根據(jù)配置找到并刪除其他相關(guān)的緩存。這種方法的優(yōu)勢在于,可以快速地更新簡單的緩存,并且核心系統(tǒng)可以向子系統(tǒng)廣播數(shù)據(jù)變更信息,代碼實現(xiàn)也相對簡單。不過,對于復(fù)雜的關(guān)聯(lián)關(guān)系刷新,仍然需要人工書寫邏輯來實現(xiàn)。

如果表內(nèi)數(shù)據(jù)更新較少,還可以考慮使用版本號緩存策略。這種方法比較直接:一旦有任何更新,表中所有數(shù)據(jù)緩存都會過期。例如,可以為 user_info 表設(shè)置一個版本號 key,比如 user_info_version。當(dāng)表數(shù)據(jù)發(fā)生更新時,直接將 user_info_version 自增 1。寫入緩存時,同時記錄當(dāng)前版本號;讀取時,業(yè)務(wù)邏輯會檢查緩存版本號與表版本號是否一致。如果不一致,就更新緩存數(shù)據(jù)。需要注意的是,如果版本號頻繁更新,緩存命中率會大幅下降,因此該方法更適合數(shù)據(jù)更新不頻繁的表

當(dāng)然,我們還可以對這個表做一個范圍拆分,比如按 ID 范圍分塊拆分出多個 version,通過這樣的方式來減少緩存刷新的范圍和頻率。

圖片圖片

此外,關(guān)聯(lián)型數(shù)據(jù)更新還可以通過識別主要實體 ID 來刷新緩存。這要保證其他緩存保存的 key 也是主要實體 ID,這樣當(dāng)某一條關(guān)聯(lián)數(shù)據(jù)發(fā)生變化時,就可以根據(jù)主要實體 ID 對所有緩存進行刷新。這個方式的缺點是,我們的緩存要能夠根據(jù)修改的數(shù)據(jù)反向找到它關(guān)聯(lián)的主體 ID 才行。

圖片圖片

最后,還有一種方法是通過異步腳本遍歷數(shù)據(jù)庫來刷新所有相關(guān)緩存。這種方式適用于在兩個系統(tǒng)之間進行數(shù)據(jù)同步,能夠減少系統(tǒng)之間的接口交互頻率。其缺點是,在數(shù)據(jù)被刪除后,還需要手動刪除相應(yīng)的緩存,因此更新存在一定延遲。不過,如果結(jié)合訂閱更新消息廣播機制,這種方案可以實現(xiàn)近乎同步的數(shù)據(jù)更新。

長期熱數(shù)據(jù)緩存

回過頭來看之前提到的臨時緩存方案,雖然它能解決大部分問題,但有個潛在風(fēng)險需要考慮:當(dāng) TTL 到期時,如果有大量緩存請求未命中,透傳的流量可能會給數(shù)據(jù)庫帶來巨大的壓力,甚至可能導(dǎo)致數(shù)據(jù)庫崩潰。這就是業(yè)內(nèi)常說的緩存穿透問題。如果發(fā)生大規(guī)模的并發(fā)穿透,服務(wù)可能宕機。因此,如果數(shù)據(jù)庫無法承受日常流量,就不能依賴臨時緩存方案來設(shè)計緩存系統(tǒng),而應(yīng)該采用長期緩存的方式來實現(xiàn)熱點緩存,以避免緩存穿透對數(shù)據(jù)庫的影響。

要實現(xiàn)長期緩存,需要更多的人工操作來保證緩存與數(shù)據(jù)表的一致性。長期緩存的普及主要得益于 NoSQL 技術(shù)的發(fā)展,它與臨時緩存不同,需要業(yè)務(wù)幾乎不依賴數(shù)據(jù)庫,所有在服務(wù)運行期間所需的數(shù)據(jù)都必須在緩存中可用,并確保緩存不會在使用期間丟失。這帶來的挑戰(zhàn)是,我們需要精確知道緩存中的數(shù)據(jù),并提前對這些數(shù)據(jù)進行預(yù)熱。如果數(shù)據(jù)規(guī)模較小,還可以考慮將所有數(shù)據(jù)緩存起來,這樣的實現(xiàn)會相對簡單一些。

總結(jié)

并不是所有數(shù)據(jù)放入緩存都會帶來良好的收益,因此我們需要從數(shù)據(jù)量、使用頻率和緩存命中率三個方面進行分析。對于讀多寫少的數(shù)據(jù),雖然將其緩存能夠降低數(shù)據(jù)層的壓力,但仍需根據(jù)一致性需求來更新緩存中的數(shù)據(jù)。

在這方面,單條實體數(shù)據(jù)的緩存更新相對容易實現(xiàn),但對于需要條件查詢的統(tǒng)計結(jié)果,實時更新則較為困難。因此,在設(shè)計緩存策略時,需綜合考慮這些因素,以確保緩存的有效性和數(shù)據(jù)的一致。

圖片 圖片

責(zé)任編輯:武曉燕 來源: 二進制跳動
相關(guān)推薦

2020-06-01 22:09:48

緩存緩存同步緩存誤用

2021-04-18 15:01:56

緩存系統(tǒng)數(shù)據(jù)

2020-05-12 10:43:22

Redis緩存數(shù)據(jù)庫

2022-09-06 15:30:20

緩存一致性

2022-03-31 08:21:14

數(shù)據(jù)庫緩存雙寫數(shù)據(jù)一致性

2021-06-11 09:21:58

緩存數(shù)據(jù)庫Redis

2024-12-26 15:01:29

2022-12-14 08:23:30

2020-09-03 09:45:38

緩存數(shù)據(jù)庫分布式

2021-01-19 10:39:03

Redis緩存數(shù)據(jù)

2015-11-25 11:20:23

WindowsUbuntu時間同步

2022-03-29 10:39:10

緩存數(shù)據(jù)庫數(shù)據(jù)

2024-10-28 12:41:25

2018-07-15 08:18:44

緩存數(shù)據(jù)庫數(shù)據(jù)

2023-04-13 08:15:47

Redis緩存一致性

2024-04-11 13:45:14

Redis數(shù)據(jù)庫緩存

2022-07-25 09:48:22

緩存數(shù)據(jù)服務(wù)

2022-04-01 16:55:22

數(shù)據(jù)庫緩存日志

2023-05-09 10:59:33

緩存技術(shù)派MySQL

2018-12-13 12:43:07

Redis緩存穿透
點贊
收藏

51CTO技術(shù)棧公眾號