【解決方案】項目重構(gòu)之如何使用 MySQL 替換原來的 MongoDB
前言
在筆者 Java 后端開發(fā)的項目經(jīng)歷中,MySQL 和 MongoDB 都有使用過作為后端的數(shù)據(jù)庫來對業(yè)務(wù)數(shù)據(jù)進行持久化,兩者沒有孰優(yōu)孰劣之分,都可以在合適的場景下發(fā)揮出它們的優(yōu)勢。
今天要分享的是一個項目重構(gòu)過程中如何將數(shù)據(jù)庫選型由原來的 MongoDB 改為 MySQL 的思考,涉及到業(yè)務(wù)當(dāng)前的痛點、選型分析、解決的核心思路,最后會給出簡單的 demo。
本篇文章側(cè)重在于兩者在表設(shè)計思維上的轉(zhuǎn)換,而業(yè)務(wù)數(shù)據(jù)遷移同步的方案,下一篇文章將給出。
一、痛點所在
該項目是一個【PC端管理后臺】+【移動端h5頁面】為主業(yè)務(wù)框架的系統(tǒng),原來的預(yù)期是:在后臺配置好活動所需的參數(shù),h5 既可以放在 app 客戶端打開,也可以作為url 鏈接的形式直接在瀏覽器打開。項目一期的時候,業(yè)務(wù)方認為這樣的運營活動會帶來不少的流量和用戶。但是到后來業(yè)務(wù)重心有所調(diào)整,引流的方式發(fā)生變化,最終導(dǎo)致了項目的一個重構(gòu)。
主要的原因有以下幾點:
-
總體的數(shù)據(jù)量沒有預(yù)想的那么大
活動參與人數(shù)前期預(yù)估為30w+,經(jīng)歷過2個線上活動后的實際總參與人數(shù)為5w+,客戶端注冊用戶數(shù)為3w+,占全部參與人數(shù)的65%左右,遠不及預(yù)期規(guī)模;
-
核心接口的并發(fā)也沒有預(yù)想的高
h5 端的大約 5-8 個的核心接口在實際線上活動進行的最高 QPS 只達到 200-300 左右,CPU 與 內(nèi)存占用率也未達到設(shè)置的告警線(60%);
-
MySQL 在硬件資源成本上性價比更高
以阿里云的 RDS for MySQL 與 云數(shù)據(jù)庫 MongoDB 做對比,都是集群部署 + 8核16GB + 100GB 存儲 + 1年時長的規(guī)格下,前者會比后者便宜7w+RMB;
-
MySQL 的動態(tài)數(shù)據(jù)源切換方案更成熟
當(dāng)時后端的項目已經(jīng)被全部要求接入多租戶改造,市面上開源的、成熟的動態(tài)數(shù)據(jù)源切換方案并不多,而完全專門支持 MongoDB 的是少之又少。
綜合以上幾點原因,完全放棄該項目是沒必要的,但也需要適應(yīng)當(dāng)前業(yè)務(wù)的變化和成本控制,預(yù)計花費30人/天,即 2 個后端開發(fā)在 2-3 周內(nèi)完成對該系統(tǒng)的重構(gòu),接口和前端頁面基本無需調(diào)整。
二、選型分析
這里就正式進入技術(shù)部分了,首要對比的是兩者各自的特點以及適用的場景,這對于把握整個項目的走向是至為關(guān)鍵的。
2.1特點對比
| 對比項 | MySQL | MongoDB |
|---|---|---|
| 數(shù)據(jù)模型 | 關(guān)系型數(shù)據(jù)庫,采用表格(table)的形式存儲數(shù)據(jù),每一行是一條記錄 | 非關(guān)系型(NoSQL)、文檔型數(shù)據(jù)庫,數(shù)據(jù)以文檔(document)的非結(jié)構(gòu)化形式存儲 |
| 查詢方式 | 使用標準的 SQL 進行查詢,提供了豐富的查詢條件、連接(join)、排序、分頁等功能 | 使用基于 JSON 結(jié)構(gòu)特點的的查詢語句,支持大量數(shù)據(jù)的聚合、統(tǒng)計、分析 |
| 事務(wù)支持 | 支持 ACID 事務(wù),確保在多條操作組成的事務(wù)中數(shù)據(jù)的一致性和可靠性。特別是在InnoDB引擎中,提供了完整的事務(wù)支持 | 4.0 版本開始引入了多文檔事務(wù)支持,可以保證在一定范圍內(nèi)的讀寫操作具備ACID特性。但對于需要嚴格事務(wù)特性的復(fù)雜業(yè)務(wù)場景不及 MySQL 成熟 |
| 數(shù)據(jù)處理 | 在處理復(fù)雜查詢和高并發(fā)寫入時,需要依賴索引來優(yōu)化性能,或者通過分區(qū)、分片等手段進行水平擴展 | 在水平擴展和實時數(shù)據(jù)處理方面優(yōu)勢很大,通過分片(sharding)技術(shù)可以輕松應(yīng)對海量數(shù)據(jù)存儲和高并發(fā)讀寫 |
| 空間占用 | 由于數(shù)據(jù)結(jié)構(gòu)緊湊,對數(shù)據(jù)的存儲通常更為節(jié)省空間,特別是對于簡單數(shù)據(jù)結(jié)構(gòu)和關(guān)系清晰的數(shù)據(jù)集 | 由于文檔存儲的靈活性和包含元數(shù)據(jù)等因素,通常占用空間較大 |
| 項目集成 | 已經(jīng)有成熟的第三方 ORM 框架支持,如:Mybatis、Mybatis Plus、io.mybatis、tk.mybatis等 | 目前集成在 Spring Boot 項目里的增刪改查都是基于 MongoRepository 和 MongoTemplate 來實現(xiàn)的 |
2.2場景對比
- MySQL
- Web 應(yīng)用程序:如常見的 xx 管理后臺、xx 管理系統(tǒng),電商 web 網(wǎng)站,包括一些移動端 h5 的頁面等;
- 企業(yè)級應(yīng)用:如常見的客戶關(guān)系管理系統(tǒng)(CRM)、人力資源管理系統(tǒng)(HRM)和供應(yīng)鏈管理系統(tǒng)(SCM)等,MySQL 提供了強大的事務(wù)支持;
- 嵌入式開發(fā):需要輕量級數(shù)據(jù)庫的軟件、硬件和設(shè)備,MySQL 可以作為一個嵌入式數(shù)據(jù)庫引擎集成到各種應(yīng)用程序中,提高應(yīng)用程序的可移植性;
- 云計算和大數(shù)據(jù):MySQL 在云數(shù)據(jù)庫服務(wù)中被廣泛使用,支持云原生應(yīng)用程序和分布式數(shù)據(jù)處理框架,如 Hadoop 和 Spark 等。
- MongoDB
- 處理實時數(shù)據(jù):非常適合處理移動互聯(lián)網(wǎng)應(yīng)用常見的大部分場景,如用戶活動、社交互動、在線購物等;
- 內(nèi)容管理系統(tǒng)(CMS):用于處理文章、稿件、評論、圖片、視頻等富媒體內(nèi)容的存儲和增刪改查,支持全文搜索和實時更新;
- 數(shù)據(jù)聚合倉庫:存儲原始或半處理的業(yè)務(wù)數(shù)據(jù),利用聚合框架進行實時數(shù)據(jù)聚合、統(tǒng)計分析和數(shù)據(jù)可視化;
- 游戲數(shù)據(jù)管理:存儲玩家賬戶信息、游戲進度、成就、虛擬物品、社交關(guān)系等,快速計算和更新游戲排行榜數(shù)據(jù),支持實時查詢等。
三、核心思路
我們知道,在 MongoDB 中,一條數(shù)據(jù)的記錄(文檔)格式是 json 的 格式,即強調(diào) key-value 的關(guān)系。
對于一個 MongoDB 的文檔來說,里面可以包含很多這個集合的屬性,就像一篇文章里面有很多章節(jié)一樣。
以下面這個圖2-1為例子,activity 是一個完整的集合,里面包含了很多屬性,id、name、status等基本屬性,還有 button 和 share 等額外屬性,這些屬性共同構(gòu)成了這個集合。
但這樣的結(jié)構(gòu)在 MySQL 里是不能實現(xiàn)的,理由很簡單,MySQL 強調(diào)關(guān)系,1:1 和 1:N 是十分常見的關(guān)系。可以看到,下面將基本屬性放在 activity 作為主表,而其它額外屬性分別放在了 button 表和 share 表里,同時將主表的主鍵 id 作為了關(guān)聯(lián)表的 ac_id 外鍵。
那要怎么替換才能實現(xiàn)呢?MongoDB 改成 MySQL 的核心在于:原有的集合關(guān)系以及嵌套關(guān)系,需要拆表成1 : N 的范式關(guān)系,用主鍵-外鍵的方式做關(guān)聯(lián)查詢,同時避免 join 連接查詢。
四、demo 示例
下面首先分別給出實際的表設(shè)計與實體映射,包括 MongoDB 和 MySQL 的,然后再通過簡單的查詢代碼來體現(xiàn)兩者的區(qū)別。
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查詢代碼
下面就根據(jù)主鍵 id 和狀態(tài)這兩個條件進行活動詳情的查詢。
4.2.1MongoDB 查詢
/**
* @apiNote 通過主鍵id和活動狀態(tài)查詢活動
* @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));
//查兩個關(guān)聯(lián)表
vo.setShare(this.activityShareService.getShare(activity.getId()));
vo.setButton(this.activityButtonService.getButton(activity.getId()));
return vo;
}
五、文章小結(jié)
使用 MySQL 替換 MongoDB 的小結(jié)如下:
- 做技術(shù)選型時要充分考慮對比兩者的特點以及應(yīng)用場景,選擇最合適的
- 如非必要,那么還是繼續(xù)沿用原來的設(shè)計;一旦選擇重構(gòu),那么就要考慮成本
- 原有的集合關(guān)系以及嵌套關(guān)系,需要拆表成1 : N 的范式關(guān)系,用主鍵-外鍵的方式做關(guān)聯(lián)
最后,如有不足和錯誤,還請大家指正?;蛘吣阌衅渌胝f的,也歡迎大家在評論區(qū)交流!

筆者今天要分享的是一個項目重構(gòu)過程中如何將數(shù)據(jù)庫選型由原來的 MongoDB 改為 MySQL 的思考,涉及到業(yè)務(wù)當(dāng)前的痛點、選型分析、解決的核心思路,最后會給出簡單的 demo。
浙公網(wǎng)安備 33010602011771號