數據庫分庫分表架構選型

隨着用戶量的增加和歷史數據的不斷積累,導致公司系統越來越卡,稍微複雜的查詢都是分鐘級,甚至有前端請求超時報錯的情況(2分鐘),所以這段時間一直在研究公司的數據庫架構。
我是一個地道的java程序員,由於我們公司沒有DBA,所以只能我來研究,這也是公司交給我的一個重要的任務,我利用做完手頭項目的空餘時間分析並研究了目前市場上很多的數據庫架構,進行一次總結、體會。
請謹記:
沒有最好的數據庫架構,只有適不適合的數據庫架構。


0 數據庫架構調整背景:

1、sql已經無法繼續優化
2、數據庫表結構設計已經無法繼續優化
3、已經做了讀寫分離,但是性能還是低
4、單日數據量50W左右,不超過100W(超過100W不建議使用本文的做法,後面會講到)
5、讀壓力遠大於寫壓力
6、對最近的1到2個月的數據操作頻繁、對最近半年的較頻繁
7、偶爾會對很久的歷史數據進行查詢(歷史數據不能刪)
8、無法避免會進行關聯查詢
9、分頁、排序等功能都必須正常使用
10、沒有專門的DBA,或者公司不想運維過於複雜的數據庫架構

如果你滿足以上需要,那恭喜你,這篇文章應該值得你參考。
爲了不浪費大家寶貴的時間,我這篇文章採用倒敘的方法,第一章直接介紹架構調整後的終極版本,第二章開始介紹有哪些其他的架構都被pass掉了,讓大家更加認同“終極版本”。當然如果大家有其他想法或者意見都歡迎評論留言。

1 終極版本

終極版本
首先,解釋下上圖的含義:

  • 圖中的master是mysql的寫庫、slave是mysql的讀庫;cti是數據庫的名字,fact_call表存最近2個月的數據,fact_call_6存最近6個月的數據,fact_call_all表存所有數據。
  • fact_call_6和fact_call_all的數據每天從fact_call表同步過來,同步完畢後需要刪除fact_call表中超過2個月的數據,還需要刪除fact_call_6表中超過6個月的數據.
  • 注意:需要限制查詢最近的超過2個月的數據(如果要查詢超過2個月的數據,則不能查今天的數據,因爲今天的需要到晚上才能同步到fact_call_6表中)。
  • 當然也可以設置爲1個月的、2個月的、6個月的、永久的。主要思路是:不分表不分庫,做表的冗餘存儲

其次,mysql的腳本:

  • mysql的存儲過程腳本
-- 創建同步的存儲過程
DELIMITER //
USE `cti`//
DROP PROCEDURE IF EXISTS pro_syn_data//
CREATE PROCEDURE pro_syn_data ()
BEGIN
INSERT INTO `fact_call_6` SELECT * FROM `fact_call` WHERE DATE(report_time) >= DATE( DATE_SUB(NOW(), INTERVAL 1 DAY) ) ;
INSERT INTO `fact_call_all` SELECT * FROM `fact_call` WHERE DATE(report_time) >= DATE( DATE_SUB(NOW(), INTERVAL 1 DAY) ) ;
END//
DELIMITER ;

-- 創建刪除的存儲過程
DELIMITER //
USE `cti`//
DROP PROCEDURE IF EXISTS pro_clear_data//
CREATE PROCEDURE pro_clear_data ()
BEGIN
DELETE FROM `fact_call` WHERE DATE(report_time) <= DATE( DATE_SUB(NOW(), INTERVAL 3 MONTH) ) ;
DELETE FROM `fact_call_6` WHERE DATE(report_time) <= DATE( DATE_SUB(NOW(), INTERVAL 6 MONTH) ) ;
END//
DELIMITER ;
  • mysql的定時器腳本
-- 查詢mysql事件是否開啓
show variables like 'event_scheduler';
select @@event_scheduler;

-- 開啓mysql事件
SET GLOBAL event_scheduler = 1;

-- 創建定時同步的事件
DROP EVENT IF EXISTS `e_pro_syn_data`;
CREATE EVENT `e_pro_syn_data` 
ON SCHEDULE EVERY 1 DAY STARTS '2018-11-12 00:00:01' 
ON COMPLETION NOT PRESERVE ENABLE DO CALL pro_syn_data ();
-- 創建定時刪除的事件
DROP EVENT IF EXISTS `e_pro_clear_data`;
CREATE EVENT `e_pro_clear_data` 
ON SCHEDULE EVERY 1 DAY STARTS '2018-11-12 02:00:00' 
ON COMPLETION NOT PRESERVE ENABLE DO CALL pro_clear_data ();

再次,讀取分表數據的java的示例代碼:
主要思路:在查詢fact_call等表前判斷應該查哪個表查。
核心代碼:
(1)通過時間範圍確定表名

	/**
	 * 確定從哪張表中讀取數據
	 * @param decisionTime 這是sql中最小的的report_time
	 *                     例如:select * from fact_call where report_time > '2018-10-06 17:32:59' and report_time < '2018-11-06 17:32:59'
	 *                     或者:select * from fact_call where report_time between '2018-10-06 17:32:59' and '2018-11-06 17:32:59'
	 *                     那麼decisionTime應該是其中較小的值'2018-10-06 17:32:59'
	 *                     注意:必須限制一次查詢的最大時間跨度不超過3個月
	 */
	public String decisionTableName(String decisionTime) {
		try {
			if (null!=decisionTime && !"".equals(decisionTime)) {
				long decision = sdf.parse(decisionTime).getTime();
				Calendar calendar = Calendar.getInstance();
				calendar.add(Calendar.MONTH, -3);
				long before_3 = calendar.getTimeInMillis();
				calendar.add(Calendar.MONTH, -3);
				long before_6 = calendar.getTimeInMillis();
				if (decision > before_3) {
					return "fact_call";
				} else if (decision > before_6) {
					return "fact_call_6";
				} else {
					return "fact_call_all";
				}
			}
		}catch (Exception e) {
			e.printStackTrace();
		}
		return "fact_call_all";
	}

(2)mybatis的映射文件,對錶名做判斷:

    <select id="findAll" resultMap="base_result_map" parameterType="java.util.Map">
    SELECT
        *
    FROM
        <choose>
            <when test="tableName=='fact_call'">
                fact_call f
            </when>
            <when test="tableName=='fact_call_6'">
                fact_call_6 f
            </when>
            <otherwise>
                fact_call_all f
            </otherwise>
        </choose>
    WHERE
        1=1
        <!-- and f.report_time &lt; #{begin_time} and f.report_time &gt; #{end_time}-->
        and f.report_time between #{begin_time} and #{end_time}
        limit #{index} , #{size}
    </select>

最後,解釋爲什麼這麼做:
優點:
1、不需要分庫(後面會介紹分庫的架構)
2、不依賴第三方程序(後面會介紹數據庫中間件的架構)
3、數據冗餘儘量少(後面會介紹一主多從的架構)
4、可靠性更高(後面會介紹使用mysql觸發器做實時同步的架構)
缺點:
1、數據冗餘爲8個月數據(以空間換時間)
2、需要開啓mysql定時器功能(影響的性能很小,可忽略)
3、對程序員不透明(但是程序員自己代碼判斷去哪個表中查,也很簡單)
4、單臺數據庫存在服務器io限制(我們公司的數據庫查詢慢的問題不在於服務器,在於單表過大)

最終完美上線

2 爲什麼不使用mycat、Kingshard、Sharding-JDBC

1、爲什麼使用mycat
mycat
(注:上圖來源網絡)
mycat功能很強大,即支持分庫也支持分表。支持取模、hash等不同的劃分策略。但是存在三點問題:

  • 集羣搭建過於複雜,運維成本高,如果不搭建mycat集羣又會帶來單點故障問題;
  • 我的需求對於最近數據和歷史數據是不同的,歷史數據可能1年也就查幾次,慢點無所謂
  • 我必須要使用分頁、排序等功能

注:
但是大後期,也就是過了很多年後,當我們公司有了自己的DBA,當我們的日數據量超過100W,應該還是會採用分庫分表的辦法。(爲什麼是100W呢?因爲mysql innodb處理5000W數據量的單錶速度勉強可以接受,5000W/60天,約等於100W/天)。我們公司還處於早期階段,沒有DBA,公司想盡快提高數據庫性能,又不想搞得太複雜。

至於不使用Kingshard、Sharding-JDBC原因很多,一方面太麻煩了,Kingshard使用Go語言開發,難以維護。Sharding-JDBC對程序員不透明,我每個程序都一遍複雜的分庫分表,主要是複雜。
另外,從我需求的角度分析,我已經手動分表了,也不需要使用第三方來做讀寫分離。完全就沒必要使用數據庫中間件!

3 爲什麼不使用一主多從

一主多從的架構,如下圖:
在這裏插入圖片描述
(上圖的最近3月,改爲最近2月。)
上圖是分庫的,之前準備使用mybatis的多源數據庫自動切換的辦法,這樣可以避免使用數據庫中間件,也蠻簡單的,在每次查詢之前根據分庫的key來切換數據源即可(感興趣的可以自己搜索,我是已經實現過)。上圖的好處是可以把數據庫放在不同的服務器上,但是缺點是違反了主從原則,運維管理也很麻煩。後來被pass掉了。

3 爲什麼不使用mysql觸發器做實時同步

mysql觸發器實時同步的版本:
在這裏插入圖片描述
大家應該看出來了,終極版本中“需要限制查詢最近的超過2個月的數據(如果要查詢超過2個月的數據,則不能查今天的數據,因爲今天的需要到晚上才能同步到fact_call_6表中)”,這個原因是因爲,我做的不是實時同步,那麼爲什麼不使用mysql觸發器做實時同步,原因很簡單,當數據庫操作過於頻繁時,mysql觸發器不可靠!

經過很久的while(true){分析、開會、討論},最終得出來了我們的“終極版本”。最終版本雖說看起來很簡單,也沒有使用什麼複雜的技術,但是能滿足我們的需求,最快的提升數據庫性能。就像:

以前:
客戶:我想查下我今天的數據,爲什麼查不出來數據了,一查就報錯。
產品經理:沒辦法數據庫量太大了。
客戶:什麼?那也就是說我以後每天的數據都不能查了?
產品經理說:那我把兩年前的數據刪了吧?
客戶:刪數據?那怎麼行,萬一我們領導哪天要檢查、覈實、取證怎麼辦,不能刪(其實他們永遠不會去查很久以前的數據,他們只是一聽到刪他們的數據他們就很慌)。

上線完畢,改動相對較小。只是多加了兩張表、對於要查fact_call表的程序稍微做了修改。

現在:
客戶:我今天的數據可以查,但是查以前的數據很慢,爲什麼?
產品經理:誰叫你查以前的數據,查很久以前的數據,本來就慢…
客戶:哦,好吧…
我:(哈哈哈哈)

本文地址:https://blog.csdn.net/tiandixuanwuliang/article/details/84650061

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章