- 前言
- 一、痛點所在
- 二、選型分析
- 2.1特點對比
- 2.2場景對比
- 三、核心思路
- 四、demo 示例
- 4.1實體對映
- 4.1.1MongoDB 實體
- 4.1.2MySQL 實體
- 4.2查詢程式碼
- 4.2.1MongoDB 查詢
- 4.2.2MySQL 查詢
- 4.1實體對映
- 五、文章小結
前言
在筆者 Java 後端開發的專案經歷中,MySQL 和 MongoDB 都有使用過作為後端的資料庫來對業務資料進行持久化,兩者沒有孰優孰劣之分,都可以在合適的場景下發揮出它們的優勢。
今天要分享的是一個專案重構過程中如何將資料庫選型由原來的 MongoDB 改為 MySQL 的思考,涉及到業務當前的痛點、選型分析、解決的核心思路,最後會給出簡單的 demo。
本篇文章側重在於兩者在表設計思維上的轉換,而業務資料遷移同步的方案,下一篇文章將給出。
一、痛點所在
該專案是一個【PC端管理後臺】+【移動端h5頁面】為主業務框架的系統,原來的預期是:在後臺配置好活動所需的引數,h5 既可以放在 app 客戶端開啟,也可以作為url 連結的形式直接在瀏覽器開啟。專案一期的時候,業務方認為這樣的運營活動會帶來不少的流量和使用者。但是到後來業務重心有所調整,引流的方式發生變化,最終導致了專案的一個重構。
主要的原因有以下幾點:
-
總體的資料量沒有預想的那麼大
活動參與人數前期預估為30w+,經歷過2個線上活動後的實際總參與人數為5w+,客戶端註冊使用者數為3w+,佔全部參與人數的65%左右,遠不及預期規模;
-
核心介面的併發也沒有預想的高
h5 端的大約 5-8 個的核心介面在實際線上活動進行的最高 QPS 只達到 200-300 左右,CPU 與 記憶體佔用率也未達到設定的告警線(60%);
-
MySQL 在硬體資源成本上價效比更高
以阿里雲的 RDS for MySQL 與 雲資料庫 MongoDB 做對比,都是叢集部署 + 8核16GB + 100GB 儲存 + 1年時長的規格下,前者會比後者便宜7w+RMB;
-
MySQL 的動態資料來源切換方案更成熟
當時後端的專案已經被全部要求接入多租戶改造,市面上開源的、成熟的動態資料來源切換方案並不多,而完全專門支援 MongoDB 的是少之又少。
綜合以上幾點原因,完全放棄該專案是沒必要的,但也需要適應當前業務的變化和成本控制,預計花費30人/天,即 2 個後端開發在 2-3 周內完成對該系統的重構,介面和前端頁面基本無需調整。
二、選型分析
這裡就正式進入技術部分了,首要對比的是兩者各自的特點以及適用的場景,這對於把握整個專案的走向是至為關鍵的。
2.1特點對比
對比項 | MySQL | MongoDB |
---|---|---|
資料模型 | 關係型資料庫,採用表格(table)的形式儲存資料,每一行是一條記錄 | 非關係型(NoSQL)、文件型資料庫,資料以文件(document)的非結構化形式儲存 |
查詢方式 | 使用標準的 SQL 進行查詢,提供了豐富的查詢條件、連線(join)、排序、分頁等功能 | 使用基於 JSON 結構特點的的查詢語句,支援大量資料的聚合、統計、分析 |
事務支援 | 支援 ACID 事務,確保在多條操作組成的事務中資料的一致性和可靠性。特別是在InnoDB引擎中,提供了完整的事務支援 | 4.0 版本開始引入了多文件事務支援,可以保證在一定範圍內的讀寫操作具備ACID特性。但對於需要嚴格事務特性的複雜業務場景不及 MySQL 成熟 |
資料處理 | 在處理複雜查詢和高併發寫入時,需要依賴索引來最佳化效能,或者透過分割槽、分片等手段進行水平擴充套件 | 在水平擴充套件和實時資料處理方面優勢很大,透過分片(sharding)技術可以輕鬆應對海量資料儲存和高併發讀寫 |
空間佔用 | 由於資料結構緊湊,對資料的儲存通常更為節省空間,特別是對於簡單資料結構和關係清晰的資料集 | 由於文件儲存的靈活性和包含後設資料等因素,通常佔用空間較大 |
專案整合 | 已經有成熟的第三方 ORM 框架支援,如:Mybatis、Mybatis Plus、io.mybatis、tk.mybatis等 | 目前整合在 Spring Boot 專案裡的增刪改查都是基於 MongoRepository 和 MongoTemplate 來實現的 |
2.2場景對比
- MySQL
- Web 應用程式:如常見的 xx 管理後臺、xx 管理系統,電商 web 網站,包括一些移動端 h5 的頁面等;
- 企業級應用:如常見的客戶關係管理系統(CRM)、人力資源管理系統(HRM)和供應鏈管理系統(SCM)等,MySQL 提供了強大的事務支援;
- 嵌入式開發:需要輕量級資料庫的軟體、硬體和裝置,MySQL 可以作為一個嵌入式資料庫引擎整合到各種應用程式中,提高應用程式的可移植性;
- 雲端計算和大資料:MySQL 在雲資料庫服務中被廣泛使用,支援雲原生應用程式和分散式資料處理框架,如 Hadoop 和 Spark 等。
- MongoDB
- 處理實時資料:非常適合處理移動網際網路應用常見的大部分場景,如使用者活動、社互動動、線上購物等;
- 內容管理系統(CMS):用於處理文章、稿件、評論、圖片、影片等富媒體內容的儲存和增刪改查,支援全文搜尋和實時更新;
- 資料聚合倉庫:儲存原始或半處理的業務資料,利用聚合框架進行實時資料聚合、統計分析和資料視覺化;
- 遊戲資料管理:儲存玩家賬戶資訊、遊戲進度、成就、虛擬物品、社交關係等,快速計算和更新遊戲排行榜資料,支援實時查詢等。
三、核心思路
我們知道,在 MongoDB 中,一條資料的記錄(文件)格式是 json 的 格式,即強調 key-value 的關係。
對於一個 MongoDB 的文件來說,裡面可以包含很多這個集合的屬性,就像一篇文章裡面有很多章節一樣。
以下面這個圖2-1為例子,activity 是一個完整的集合,裡面包含了很多屬性,id、name、status等基本屬性,還有 button 和 share 等額外屬性,這些屬性共同構成了這個集合。
但這樣的結構在 MySQL 裡是不能實現的,理由很簡單,MySQL 強調關係,1:1 和 1:N 是十分常見的關係。可以看到,下面將基本屬性放在 activity 作為主表,而其它額外屬性分別放在了 button 表和 share 表裡,同時將主表的主鍵 id 作為了關聯表的 ac_id 外來鍵。
那要怎麼替換才能實現呢?MongoDB 改成 MySQL 的核心在於:原有的集合關係以及巢狀關係,需要拆表成1 : N 的正規化關係,用主鍵-外來鍵的方式做關聯查詢,同時避免 join 連線查詢。
四、demo 示例
下面首先分別給出實際的表設計與實體對映,包括 MongoDB 和 MySQL 的,然後再透過簡單的查詢程式碼來體現兩者的區別。
4.1實體對映
4.1.1MongoDB 實體
@EqualsAndHashCode(callSuper = true)
@Data
public class Activity extends BaseEntity {
@Id
private String id;
private String name;
private ActivityStatusEnum status;
private ReviewStatusEnum review;
private ActivityTypeEnum type;
private ActivityButton button;
private ActivityShare share;
}
4.1.2MySQL 實體
@Data
public class Activity extends BaseEntity {
@Id
private Integer id;
private String name;
private Integer status;
private Integer review;
private Integer type;
}
@Data
public class ActivityButton extends BaseEntity {
@Id
private Integer id;
private Integer acId;
private String signUp;
private Integer status;
private String desc;
}
@Data
public class ActivityShare extends BaseEntity {
@Id
private String id;
private Integer acId;
private String title;
private String iconUrl;
}
4.2查詢程式碼
下面就根據主鍵 id 和狀態這兩個條件進行活動詳情的查詢。
4.2.1MongoDB 查詢
/**
* @apiNote 透過主鍵id和活動狀態查詢活動
* @param id 主鍵id
* @return 實體
*/
@Override
public Avtivity getDetailById(String id) {
return this.repository.findById(id)
.filter(val -> ActivityStatusEnum.ON.equals(val.getStatus()))
.orElseThrow(() -> new RuntimeException("該活動不存在!"));
}
4.2.2MySQL 查詢
@Resource
private ActivityShareService activityShareService;
@Resource
private ActivityButtonService activityButtonService;
@Override
public ActivityVO detail(Integer id) {
ExampleWrapper<Activity, Serializable> wrapper = this.wrapper();
wrapper.eq(Activity::getid, id)
.eq(Activity::getStatus(), DataStatusEnum.NORMAL.getCode());
Activity activity = Optional.ofNullable(this.findOne(wrapper.example()))
.orElseThrow(() -> new RuntimeException("該活動不存在!"));
ActivityVO vo = new ActivityVO();
vo.setName(Optional.ofNullable(activity.getName()).orElse(StringUtils.EMPTY));
//查兩個關聯表
vo.setShare(this.activityShareService.getShare(activity.getId()));
vo.setButton(this.activityButtonService.getButton(activity.getId()));
return vo;
}
五、文章小結
使用 MySQL 替換 MongoDB 的小結如下:
- 做技術選型時要充分考慮對比兩者的特點以及應用場景,選擇最合適的
- 如非必要,那麼還是繼續沿用原來的設計;一旦選擇重構,那麼就要考慮成本
- 原有的集合關係以及巢狀關係,需要拆表成1 : N 的正規化關係,用主鍵-外來鍵的方式做關聯
最後,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!