上千萬數據查詢解決方案

業務場景
  • 集團下有多個業務團隊,例如:團隊A,團隊B
  • 通過賬號註冊用戶後,記錄來自的團隊,加入用戶來源於團隊A(A客戶池),用戶在團隊A下單後,可以推薦團隊B的業務給用戶,如果用戶在團隊B(B客戶池)成交訂單,則團隊A與團隊B可以對用戶在團隊B下單的金額分紅
  • 數據記錄:註冊時記錄團隊A,用戶在團隊B下單,則記錄B
  • 如果用戶在集團下多個團隊有下單,則記錄爲多個團隊的客戶池
  • 集團用戶在2500w左右,最後所有的私域客戶池記錄在5kw左右,每天幾萬增長
  • 客戶池又分有無行業場景
  • 使用MySQL
第一版上線(虛擬機部署)
  • 當前需求涉及權限,接入內部登錄授權等
  • 考慮到現有類似項目,則使用現有項目開發
  • 這版代碼完全沒有緩存,考慮到同一個表中,建立關鍵查詢索引(區分業務團隊字段,例如: type)
  • 上線後發現,即使type建立索引,也沒有增加查詢效率,由於區分度太低導致(一般區分度在80%以上才建議建立索引)
  • 客戶池最多的用戶排在第一個,導致幾乎所有用戶進入,就顯示服務異常,或者15s纔有數據返回(數據量在3kw左右,默認時間爲最近一週)
  • 根據業務需要,調整客戶池顯示順序,數據渲染有所緩解
  • 虛擬機配置爲1C2G,一會兒運維就找我,反饋CPU被打爆了,然後運維重啓後,依然被打爆
第二版上線(Docker部署 2C4G)
  • 切換Docker部署
  • 調整默認時間爲過去一天的記錄(原七天)
  • 由於是歷史項目,切換過程也是一波三折,配置不規範,導致重重受阻,此處就不細說了
  • 上線後,數據渲染效果有所緩解,CPU沒有佔滿
  • 也對每個客戶池的第一頁做緩存,但是是第一個用戶訪問時緩存
  • 這樣也會存在問題,第一個用戶始終數據渲染始終慢
  • 底層使用 Spring Data JPA(有一半字段前端不使用,會節省轉換記錄佔用內存),在多條件查詢時,findAll(Specification specification, Pageable pageable) 不支持指定返回參數
// 過濾字段不生效 升級Spring Data JPA版本也不行
List<Selection<?>> selections = Lists.newArrayList(root.get("userId"), root.get("name"), root.get("industry"), root.get("labels"), 
                root.get("provinceName"), root.get("cityName"));
query = query.multiselect(selections);
  • 使用這種方式可以,但是存在兩個問題,多條件拼裝,對應查詢條件總量(前端分頁使用)
/**
 * https://stackoverflow.com/questions/9191551/jpa-multiselect
 * 使用如下方式可以實現指定字段返回,但是不支持分頁
 **/
criteriaBuilder.multiselect(select);
TypedQuery<Tuple> q = entityManager.createQuery(criteriaBuilder);
  • 網上說還有其他解決方案,但是經過測試不行,如果您需要嘗試,可以搜索來看下
  • 這裏還有個小插曲,第一版上線時數據不正確,例如:客戶企業名稱: “XXX公司”,客戶代表爲空,但是頁面顯示是 客戶企業名稱: ‘’ 客戶代表: XXX公司
  • 我以爲是程序從數據庫查詢出來後處理導致的問題,果斷將處理過程放在查詢語句中
  • 類似於客戶所在地需要處理,將數據中的 省、市、區/縣使用"-"展示,例如: 四川-成都-青羊,但是市和區都可能爲空
/**
 * 至於爲什麼返回 Object[](使用原生SQL查詢),請自行搜索
 **/
@Query(value = "select user_id userId, name, '' industry, labels, representative, \n" +
        "concat(province_name, \n" +
        "case city_name when '' then '' ELSE CONCAT('-', city_name) end, \n" +
        "case district_name when '' then '' ELSE CONCAT('-', district_name) end) region, date_format(str_to_date(CONCAT('',month_day), '%Y%m%d'), '%Y-%m-%d') as createTime\n" +
        "from table_name\n" +
        "where type = ?1 and user_id in (?2)", nativeQuery = true)
List<Object[]> getCustomerDetailList(Integer type, List<Integer> userList);
  • 然後使用笨方法,將數組一個個元素取出,塞進構造方法即可
第三版上線(定時任務加緩存)
  • 接入公司調度平臺(定時任務即可)
  • 每天上班前調用,寫入每個團隊第一頁緩存數據(與產品確認,用戶一般操作場景)
  • 查詢條件可第二版代碼相同,但是將 findAll() 返回的 Page<?> 拆分,緩存中記錄 total, List idList, sid(用戶記錄生成緩存是否有問題)
  • 緩存對象定義
/**
 * 區別生成緩存時操作ID和用戶訪問緩存時數據關聯(可以省略)
 **/
private String sid;

/**
 * 默認查詢條件總記錄數
 **/
private long total;

/**
 * 主鍵ID(也可以用用戶ID,但是使用用戶ID需要和type查詢)
 **/
private List<Integer> idList;
  • 用戶操作時,通過對應頁碼和查詢條件,獲取緩存中idList,這個可以快速查詢到記錄
  • 取出緩存中 total 記錄數,封裝page對象返回皆可
  • 可以設計寬鬆些,對不同頁碼,每頁展示數,團隊類型可用動態配置,數據量少的團隊完全可以不走緩存
  • 我們在分析問題時,如果一個方法不能處理或者解決,可以嘗試拆分成幾步,分別處理即可
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章