天天看點

種草社群緩存設計

本文來自網易雲社群

作者:劉魏威

引言

從資料模型來看,種草社群 = 内容 + 關系 + 計數。在業務上,具體展開就是,

  • 内容:即提問、回答、心得、使用者、消息
  • 關系:即提問->回答, 使用者->回答, 使用者->消息
  • 計數:即點贊數、粉絲數、關注數、回答數等各種計數

如何高效處理這幾個主要元素,決定了社群系統的使用者體驗和服務容量。

最初項目為了盡快上線,這些資料都是直接到db裡查詢,前期通路量小,沒什麼關系。但到了後面,一旦通路量放大,db的資源瓶頸就會凸顯。後來的壓測結果也的确反應了這一點,在通路量稍微增長,資料庫qps上升時,ddb的響應時間明顯變長,平均響應時間有40~50ms。是以急需對這些資料進行緩存,以抵擋直接通路db的大部分流量。

實作這些資料的緩存做法很簡單,但若是要把他做好,且各個業務模型能高效率的接入使用,則需要好好考量下。

内容緩存

内容緩存,這裡特指DAO緩存,以資料庫主鍵為查詢key,資料庫行記錄為value。

DAO緩存的實作有一些開源架構可以直接拿來用,如spring->緩存key的設計

緩存key需要包含哪些元素?先來列舉下之前遇到過的問題:

  • 線上環境和預發環境共用一套緩存,測試時修改預釋出環境緩存會有風險
  • 緩存Value變更,比如緩存對象增加一個業務相關的字段,新老緩存可能同時存在,無法做到無縫釋出,對業務無影響
  • key字首分布較随意,在代碼裡沒有一個集中管理的地方,不同的業務有可能會沖突
  • 批量删除或遷移緩存

解決上述問題,key就需要包含:

  • 環境資訊,隔離環境,防止互相影響
  • 版本,增加資料庫字段時,可以修改版本,使業務讀不到緩存進而強制重新整理緩存
  • 集中的字首管理,簡單實作就是一個枚舉常量,所有業務DAO字首定義放在一起
緩存未命中如何處理

緩存miss,需要知道是資料真的不存在,還是僅僅緩存過期了。有些黑客可能會惡意構造資料,導緻緩存無限擊穿。是以需要設計一個辨別不存在的對象,從緩存裡取出資料時做下判斷,如果是特定的空對象,則不需要再去db擷取了。

緩存未命中時讀資料庫,如果是單條資料,則同步設定到緩存,如果是多條,則異步設定到緩存。

關系緩存

關系資料通常用于清單場景,批量取符合條件的資料,然後按指定字段排序,分頁展示。

這塊比較難處理的就是過濾+排序+分頁。業務體量小時可以不使用緩存,建立專門的索引表,把需要作為過濾條件的字段包含到索引表裡,利用資料庫去處理排序、過濾。然而通路量大了之後,資料庫就不适合幹這個事情了。為了解決這類問題,種草社群實作了一套基于redis sorted set的通用關系緩存API。大緻的接口如下:

/**
 * 以索引為邊界批量擷取有序集合中的資料
 *
 * @param keys 鍵名清單
 * @param begin 偏移量開始
 * @param end 偏移量結束
 * @param orderType 排序類型
 * @param relationCacheFilter 過濾器
 * @return
 */
Map<K, Set<V>> multiGetByIndex(final List<K> keys, final long begin, final long end, final OrderType orderType,
        RelationCacheFilter<V> relationCacheFilter);
        
/**
 * 以分數為邊界批量擷取有序集合中的資料
 *
 * @param keys 鍵名清單
 * @param min 最小分數
 * @param max 最大分數
 * @param offset 偏移量
 * @param limit 條數
 * @param orderType 排序類型
 * @param relationCacheFilter 過濾器
 * @return
 */
Map<K, Set<V>> multiGetByScore(final List<K> keys, final double min, final double max, final long offset,
        final long limit, final OrderType orderType, RelationCacheFilter<V> relationCacheFilter);
        
/**
 * 建構緩存key
 * 
 * @param k 鍵名
 * @return 緩存key
 */
String buildCacheKey(K k);

/**
 * 傳回分區
 * 
 * @return
 */
String getRegion();

/**
 * 擷取過期時間,機關秒
 */
Long getExpireSeconds();

/**
 * 擷取全量初始化任務線程
 * 
 * @return
 */
RelationCacheInitRunnable getCacheInitRunnable(K k);      

實作要點:

  • Multiget通過redis pipeline實作,節省網絡開銷,但使用時需要注意數量限制,畢竟是批量操作
  • 資料過濾,業務方提供一個回調函數,回調函數裡可實作複雜的業務邏輯
  • 資料分頁,提供兩種方式,基于偏移量和基于score分值,以滿足比較常見的場景,如取前n條心得,取時間段範圍内的limit條資料。
  • 緩存未命中,分兩種情況處理:批量擷取,異步初始化;單條擷取,同步初始化;業務方提供緩存初始化線程。單條處理同步初始化是考慮到如個人首頁可能會刷不到資料,體驗較差。而批量擷取異步初始化,某一個使用者的内容沒拉取到,關系不大。真是比較重要的場景,可考慮定時刷緩存。

計數緩存

計數主要面臨的問題有幾點:

  • 高頻率的讀寫,如何解決性能問題
  • 有限的記憶體存儲,緩存過期初始化導緻db壓力大問題
  • 高并發更新及系統故障帶來的資料一緻性問題

種草社群的計數緩存基于redis,資料結構上主要用到了:

  • 普通的string/value,存儲單個計數
  • hash表,存儲業務上有關聯的一組計數,便于批量讀取

大緻的資料流如下圖: 

種草社群緩存設計

之前專門寫過一遍文章,詳細可點選此處檢視。

網易雲大禮包:https://www.163yun.com/gift

本文來自網易雲社群,經作者劉魏威授權釋出

相關文章:

【推薦】 Question | 網站被黑客掃描撞庫該怎麼應對防範?

【推薦】 網際網路金融中的資料挖掘技術應用

【推薦】 nej+regular環境使用es6的低成本方案