僅使用關係型數據庫就解決海量查詢的解決方案

一。當前所面臨的問題

隨着互聯網的發展,數據量越來越大,既使分庫分表後,單表數據量上千萬都是很正常,很多業務表在未歸檔前都是要到幾十億以上,多個這樣的業務表存在,到TB級非常正常。但業務的變化也是很快,後面的業務可能就會將分庫分表的核心字段丟棄,那就會面臨着,沒有分庫分表字段的存在,卻要在幾十億的分庫分表中查找,這些查找往往不是分庫分表字段,甚至索引列都不是。採用大數據或搜索引擎,甚至一些自己優化的方式,都面臨着各式各樣的問題,本文將談論現有的解決方案及優劣,然後給出一個新的方案,當然這個新方案也是有其侷限所在。

 

二。現有可行的解決方案的思考

1.大數據

大數據解決方案基本上是存在放在HDFS(Hadoop Distributed FileSystem)上面

問題1:當然現在也有人使用spark+nosql,但我們都知道NOSQL也其實要有4種不同的類型,總有得失,同時內存的容量也是有限,不適應於大表(比如表有分庫且庫的量大,總體上量大)

問題2:當然還有人說Presto,將數據讀到內存上,使用場景也受限於內存

總結:直接使用hadoop,適合報表相關的(T-1),使用spark或presto適合內存容器可控的計算場景

 

2.搜索引擎

搜索引擎基本原理是以空間換時間 ,這個空間換時間,就是在對應的分詞下建索引

問題1:搜索引擎需要數據來源,最優雅的方式就是從數據庫的BINLOG自動寫入搜索引擎,這個過程會有一定的延遲(有些資源和架構足夠好,可以減少到分鐘級以下)

問題2:索引,使用了搜索引擎,同樣也繞不開索引,如果沒有建立索引,同樣也會面臨着同樣問題,但搜索引擎基本上都支持動態引擎,但也這需要有運維動作才能(一般都不支持自動無腦創建,要不然磁盤空間會有很大的問題)

總結:適合對實時有要求,同時也有計劃且能夠按節奏進行的項目,並且有封裝好的搜索引擎組件的場景使用

 

3.分庫分表的組件個性化定製

這個要說到一個區間遍歷的概念了:比如說我有100萬數據,我要先知道指定字段的最小和最大數據,我按指定的字段>=? and <?的試試拆成多次,直到最大數據

大致市場上分爲二種通過proxy的方式(比如mycat)和內嵌在客戶端(比如:shardingJdbc)的二種方式,在此二種方式中可以做一些有序的取號服務的提前規劃,從而在分庫分表中有唯一主鍵,然後借用分庫分表的組件,並使用此唯一主鍵區間遍歷進行並行訪問(fork->join)

問題1:看到用了全局唯一取號的字段後,可能有些人會想爲什麼不用時間區間遍歷,因爲同一秒或甚至同一毫秒都有可能被刷成大量的數據,依然會造成數據庫不走索引的原理觸發,按照互聯網的小步快跑的思路,是不理想的,有風險。

問題2:因爲分庫分表的存在,全局唯一有序的取號實際上是每個表是跳號的,通過分庫分表的組件,一旦甚至沒有分庫分表字段,就會根據所配置的分庫分表的量去並行執行,這樣的後果是大量的SQL執行,會產生費很大力氣,纔得到與之不配對的結果,浪費了一些性能開銷,同時無腦的並行,在生產上也發現線程池裏的排隊情況,會引發新一輪的無效連接問題

問題3:全局唯一有序它不是聚合索引,所以它並不是最優的

總結:此方案與新的方案其實非常相似,這裏面是部門內多個同事在項目中的實踐中慢慢提煉出來的

 

三。新的方案

新的方案並不是代替上面所有的方案,只是提供新的一種選擇,此方案封裝的細節如下:

1.通過自已體系的代碼生成在DAO模板中增加API

2.結合第二點的第三點,我們採用了數據庫表的主鍵,然後通過主鍵的升降序分頁的第1條,輕易無傷的獲取整個表的最大和最小ID

3.我們通過count(*)的方式,按10000一個步長(目前測試來看10000的步長,比較中和)根據條件(此條件會涉及到一些自己系統的封裝,我們採用的類似於hiberate面對象的方式,可以很輕易在在對象轉成SQL時,植入id>=? and i<?,不同系統可以各自的方案去解決,但萬變不離其中,都是按ID的步長去查找數據

4.當我們查到數據,會將count(*)的總量與指定的量相對比,如果小於指定量,就添加指定量的分頁條件,僅僅獲取指定量的數據,如果沒有達到,繼續區間遍歷,積累到指定的量,再返回

總結一下:

上面的方案都是針對於單庫的,既然是分庫分表,爲什麼會涉及到單庫,因爲所有的優化到最後,基本上都是會有自己去控制每一步的消耗,使用方如果想並行,就自己啓線程並行fork->join,使用方主想控制主機實例或進程的CPU使用率,可以自己單庫循環,從封裝的角度上來講,不做過度封裝,交給使用方決定。

 

見代碼模板API(已體現出主要功能)

/**
	 * 根據condition按id區間循環查找(定製應用)
	 * @param condition
	 * @param dbNo
	 * @param minId
	 * @param maxId
	 * @param appointMaxSize
	 * @return
	 */
	public List<${beanName}> queryCirculationOfCustomByCondition(Condition condition, String dbNo, Long minId,
			Long maxId, int appointMaxSize) {

		String conditionWhere = ConditionParseUtil.getSqlWhere(condition);
		List<Object> conditionParams = ConditionParseUtil.getParams(condition);
		if (appointMaxSize > SqlDaoImpl.MAX_COUNT) {
			log.warn(LoggerFormatUtil
					.toStrByMsgParams("queryCirculationOfCustomByConditionException", "tableName", getTableName(),
							"dbNo", dbNo, "where", conditionWhere, "param", conditionParams, "appointMaxSize",
							appointMaxSize));
			throw new JobRuntimeException("指定數量超過" + SqlDaoImpl.MAX_COUNT + "量!");
		}

		List<${beanName}> allList = ListUtil.newArrayList();

		if (minId == null) {
			minId = sqlDao.queryMinIdBy(getTableName(), dbNo);
		}
		if (maxId == null) {
			maxId = sqlDao.queryMaxIdBy(getTableName(), dbNo);
		}

		int circulationCount = 0;
		List<${beanName}> resultList = null;
		boolean isEnd = false;

		log.info(LoggerFormatUtil
				.toStrByMsgParams("queryCirculationOfCustomByConditionStart", "tableName", getTableName(), "dbNo", dbNo,
						"where", conditionWhere, "param", conditionParams, "minId", minId, "maxId", maxId,
						"appointMaxSize", appointMaxSize));


		for (long j = minId; j < maxId; j += SqlDaoImpl.BATCH_COUNT) {

			circulationCount++;

			//copy condition
			Condition tempCondition = new Condition(condition);
			tempCondition.add(RestrictionUtil.ge(BaseDomainDefinition.id, j));
			tempCondition.add(RestrictionUtil.lt(BaseDomainDefinition.id, j + SqlDaoImpl.BATCH_COUNT));
			String whereStr = ConditionParseUtil.getSqlWhere(tempCondition);
			List<Object> params = ConditionParseUtil.getParams(tempCondition);

			int count = sqlDao.queryCountByTableAndWhereStr(getTableName(), dbNo, whereStr, params);
			if (count > 0) {
                if(count>appointMaxSize){
                    PageCondition tempPageCondition = new PageCondition(tempCondition,null,appointMaxSize);
                    resultList = queryByPageCondition(tempPageCondition,dbNo);
                }else{
                    resultList = queryByCondition(tempCondition, dbNo);
                }

				log.info(LoggerFormatUtil
						.toStrByMsgParams("queryCirculationOfCustomByConditionFoundData", "tableName", getTableName(),
								"dbNo", dbNo, "where", whereStr, "param", params, "minId", minId, "maxId", maxId,
								"foundSize", resultList.size(), "size", allList.size(), "appointMaxSize",
								appointMaxSize, "circulationCount", circulationCount));

				for (${beanName} obj : resultList) {
					allList.add(obj);
					if (allList.size() >= appointMaxSize) {
						isEnd = true;
						break;
					}
				}
			}

			if (isEnd) {
				break;
			}

		}

		log.info(LoggerFormatUtil
				.toStrByMsgParams("queryCirculationOfCustomByConditionEnd", "tableName", getTableName(), "dbNo", dbNo,
						"where", conditionWhere, "param", conditionParams, "minId", minId, "maxId", maxId, "size",
						allList.size(), "appointMaxSize", appointMaxSize, "circulationCount",
						circulationCount));

		return allList;
	}

 

四。性能測試

只談方案,不給出實際測試,這不是耍流氓,當然要給出來

場景  xxx單表數據量有441萬(它真實是拆成128個表5.6億數據)  查找的字段沒有索引 `process_flag` = ?: 指定查找1000條

最終用了241毫秒 查了數據庫442次,真正找到符合條件的數據是3次,獲取到 273筆數據

[2018-07-25 21:12:09.828] [INFO] [main] [c.v.xxx.yyy.zz.fix.main.quality.ZZTestQualityMain] >>> arIntRecInList1.size:1
[2018-07-25 21:12:09.829] [INFO] [main] [c.v.f.yyy.zz.intfc.dao.base.BaseZZIntRecInDaoImpl] >>> [queryCirculationOfCustomByConditionStart] json={} msg={"tableName":"xxx","dbNo":"127","where":" 1=1 and `source_name` = ? and `process_flag` = ?","param":["ORDER","N"],"minId":3028541,"maxId":7443503}
[2018-07-25 21:12:10.015] [INFO] [main] [c.v.f.yyy.zz.intfc.dao.base.BaseZZIntRecInDaoImpl] >>> [queryCirculationOfCustomByConditionFoundData] json={} msg={"tableName":"xxx","dbNo":"127","where":" 1=1 and `source_name` = ? and `process_flag` = ? and `id` >= ? and `id` < ?","param":["ORDER","N",6848541,6858541],"minId":3028541,"maxId":7443503,"foundSize":93,"size":0,"circulationCount":383}
[2018-07-25 21:12:10.023] [INFO] [main] [c.v.f.yyy.zz.intfc.dao.base.BaseZZIntRecInDaoImpl] >>> [queryCirculationOfCustomByConditionFoundData] json={} msg={"tableName":"xxx","dbNo":"127","where":" 1=1 and `source_name` = ? and `process_flag` = ? and `id` >= ? and `id` < ?","param":["ORDER","N",6858541,6868541],"minId":3028541,"maxId":7443503,"foundSize":15,"size":93,"circulationCount":384}
[2018-07-25 21:12:10.069] [INFO] [main] [c.v.f.yyy.zz.intfc.dao.base.BaseZZIntRecInDaoImpl] >>> [queryCirculationOfCustomByConditionFoundData] json={} msg={"tableName":"xxx","dbNo":"127","where":" 1=1 and `source_name` = ? and `process_flag` = ? and `id` >= ? and `id` < ?","param":["ORDER","N",7438541,7448541],"minId":3028541,"maxId":7443503,"foundSize":165,"size":108,"circulationCount":442}
[2018-07-25 21:12:10.069] [INFO] [main] [c.v.f.yyy.zz.intfc.dao.base.BaseZZIntRecInDaoImpl] >>> [queryCirculationOfCustomByConditionEnd] json={} msg={"tableName":"xxx","dbNo":"127","where":" 1=1 and `source_name` = ? and `process_flag` = ?","param":["ORDER","N"],"minId":3028541,"maxId":7443503,"size":273,"circulationCount":442}
[2018-07-25 21:12:10.069] [INFO] [main] [c.v.xxx.yyy.zz.fix.main.quality.ZZTestQualityMain] >>> xxx.size:273

 總結:此次測試雖然是單線程去做,但嘗試過50個線程去訪問MYSQL,MYSQL物理機各項指標均無明顯變化,這種對數據庫的傷害是非常小的

 

五。新方案的適用場景

1.整個條件中不含索引,且有索引的條件並沒有產生效果

2.數據量單表至少要超過100萬以上(小提示:對於百萬級以上的表,不走索引的SQL,絕對是分分鐘上數據庫服務器CPU報警的)

3.可以容忍幾百毫秒-5秒左右時間等待的使用

 

vipshop_ebs/朱傑

2018-12-28

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