這篇文章主要講解了“如何解決MongoDB深分頁的問題”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“如何解決mongodb深分頁的問題”吧!
成都創(chuàng)新互聯(lián)公司提供成都做網(wǎng)站、成都網(wǎng)站設(shè)計、成都外貿(mào)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計,品牌網(wǎng)站設(shè)計,廣告投放等致力于企業(yè)網(wǎng)站建設(shè)與公司網(wǎng)站制作,10年的網(wǎng)站開發(fā)和建站經(jīng)驗,助力企業(yè)信息化建設(shè),成功案例突破超過千家,是您實現(xiàn)網(wǎng)站建設(shè)的好選擇.
突然有一天,有用戶反饋,保存的個人模板全都不見了,當(dāng)聽到這個消息的時候,第一個想法就是懷疑是用戶自己刪除了,因為有日常的開發(fā)任務(wù),當(dāng)時并沒有在意,自此每隔三五天就有用戶反饋同類的問題,這時候下意識的想,之前用戶沒反饋,現(xiàn)在反饋的多了起來,是不是最近上線的程序有bug,做了刪除操作,緊急的查看了一下代碼上線記錄,發(fā)現(xiàn)并沒有進(jìn)行刪除模板操作,同時反饋的還有模板加載響應(yīng)很慢,偶爾接口返回500,測試同事試了一下,加載數(shù)據(jù)是正常的,抱著質(zhì)疑的態(tài)度開始深入了分析了這部分業(yè)務(wù)邏輯和程序的編寫,發(fā)現(xiàn)了一些端倪:
新用戶另存為我的模板會創(chuàng)建一個 “個人模板” 的場景,場景id存儲在用戶的數(shù)據(jù)表中;
保存的模板頁和場景頁存儲在mongodb的同一個表中,數(shù)據(jù)表的體量有十億多條數(shù)據(jù);
查詢功能有分頁,mongodb數(shù)據(jù)比較大時,對于深分頁性能相對較差,分頁代碼
query = new Query(Criteria.where("sceneId").is(sceneId)).with(new Sort(Sort.Direction.DESC, "id")) .skip((page.getPageNo() - 1) * page.getPageSize()).limit(page.getPageSize());
查詢方式如下圖:
那么顯而易見的模板丟失的原因就分析出來了,是用戶把 “個人模板” 場景給刪除了,從而導(dǎo)致場景下的模板頁隨之被刪除,分析出原因之后,想到了以下的優(yōu)化方案:
場景列表查詢不顯示 “個人模板” 場景數(shù)據(jù);
優(yōu)化mongodb分頁,優(yōu)化mongodb分頁,優(yōu)化如下
if (tplId == null){ // 分頁獲取 query = new Query(Criteria.where("sceneId").is(sceneId)).with(new Sort(Sort.Direction.DESC, "id")) .skip((page.getPageNo() - 1) * page.getPageSize()).limit(page.getPageSize()); }else{ query = new Query(Criteria.where("sceneId").is(sceneId).lte(tplId)).with(new Sort(Sort.Direction.DESC, "id")).limit(page.getPageSize()); }
優(yōu)化方案定好之后,很快程序就在預(yù)發(fā)布部署測試并上線,果不其然,用戶在列表看不到 “個人模板” 場景之后,反饋模板丟失的問題沒了,但是 個人模板列表加載無響應(yīng)的問題還在持續(xù)存在,難道是mongodb分頁優(yōu)化沒起作用?其實并不是,因為表的數(shù)據(jù)體量已經(jīng)達(dá)到十億級別,接口的響應(yīng)最大時間設(shè)置為2秒,超過了最大響應(yīng)時間就返回500,那么mongodb分頁優(yōu)化并不能從根本解決加載慢的問題,新的優(yōu)化方案隨即產(chǎn)生:
場景頁和模板頁分開存儲,統(tǒng)計了一下,模板頁的總數(shù)一共七百多萬,其余的都是場景頁數(shù)據(jù);
新用戶另存為個人模板不產(chǎn)生 “個人模板” 場景,用戶表中存儲場景ID;
在MySQL庫中建立userId和pageId的關(guān)系表;
優(yōu)化后的查詢?nèi)缦聢D:
那么有兩個問題,這么大的體量數(shù)據(jù),怎么遷移模板頁數(shù)據(jù)呢? 新產(chǎn)生的模板頁數(shù)據(jù)怎么進(jìn)行存儲呢? 想了一下解決方案:
模板頁數(shù)據(jù)雙寫,新插入的存儲在不同的mongodb表中,并在MySQL中建立 UID和Tpl的關(guān)系;
開發(fā)模板遷移程序,從用戶角度出發(fā),輪訓(xùn)每一位有模板標(biāo)識的用戶獲取sceneId,查詢出模板頁,隨之保存到新表,建立UID和Tpl的關(guān)系
接下來就開始去按照這個優(yōu)化方案去執(zhí)行:
第一步:在MySQL中建立UID和Tpl的關(guān)系表,先進(jìn)行數(shù)據(jù)雙寫 ,數(shù)據(jù)關(guān)系表如下圖
2、 第二步,開發(fā)模板遷移程序,遷移的辦法有好幾種,第一種:使用ETL工具進(jìn)行數(shù)據(jù)遷移、 第二種:查出歷史數(shù)據(jù),發(fā)送到MQ中,設(shè)置一定數(shù)量的消費者使用多線程方式去消費執(zhí)行,最終我覺得最優(yōu)方案是第二種,如下圖:
流程定義好了,為了不影響業(yè)務(wù)的正常執(zhí)行,一般遷移數(shù)據(jù)這樣子的工作都是從數(shù)據(jù)庫的從庫獲取數(shù)據(jù), 接下來就開發(fā)遷移程序,首先建立兩個項目,data-provider,data-consumer,data-provider 查詢用戶,把另存為模板的場景ID發(fā)送到mq,data-consumer接受場景ID,去查詢page,并分別保存到模板頁新表和MySQL庫的UID和Tpl的關(guān)系表中
data-provider代碼如下:
@Component public class TaskTplSyncRunner implements ApplicationRunner { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private TaskService taskService; @Override public void run(ApplicationArguments args) throws Exception { AtomicInteger total = new AtomicInteger(0); AtomicInteger count = new AtomicInteger(1); // 開始sceneId Long start = null; if (!CollectionUtils.isEmpty(args.getOptionValues("start"))) { start = args.getOptionValues("start").get(0) == null ? 1 : Long.valueOf(args.getOptionValues("start").get(0)); } // 最后sceneId Long end = null; if (!CollectionUtils.isEmpty(args.getOptionValues("end"))) { end = args.getOptionValues("end").get(0) == null ? 1000 : Long.valueOf(args.getOptionValues("end").get(0)); } // 每一次的執(zhí)行跨度 Integer pageSize = null; if (!CollectionUtils.isEmpty(args.getOptionValues("pageSize"))) { pageSize = args.getOptionValues("pageSize").get(0) == null ? 2000 : Integer.valueOf(args.getOptionValues("pageSize").get(0)); } logger.info("init start value is ={},end value is={}", start, end); while (true) { MapobjectMap = taskService.sendTplMq(start, end); if (objectMap.containsKey("endSceneId")){ // 得到下一次循環(huán)的最后一個id end = Long.valueOf(objectMap.get("endSceneId").toString()); start = end - pageSize; } count.getAndIncrement(); if (objectMap.containsKey("total")) { total.addAndGet(Integer.valueOf(objectMap.get("total") + "")); } // 是最后一個用戶直接跳出 if (start < 1101) { break; } } logger.info("execute personage tpl sync success,total count {} ", total.intValue()); logger.info("execute personage tpl sync task end。。。。。。。。。。"); } }
@Async public MapsendTplMq(Long start, Long end) { Map paramMap = new HashMap<>(); paramMap.put("start",start); paramMap.put("end",end); List sceneList = sceneDao.findSceneList(paramMap); Map resultMap = new HashMap<>(); sceneList.stream().forEach(scene -> { amqpTemplate.convertAndSend("exchange.sync.tpl","scene.tpl.data.sync.test", JsonMapper.getInstance().toJson(scene)); }); resultMap.put("total",sceneList.size()); resultMap.put("resultFlag",false); resultMap.put("endSceneId",start); return resultMap; }
從ApplicationArguments中獲取start,end,pageSize的值的原因是防止程序執(zhí)行中斷,自己設(shè)置 VM options
data-consumer 代碼如下:
@Component public class Receiver { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private ScenePageDoc scenePageDoc; @RabbitListener(queues = "queue.scene.tpl.data.sync.test", containerFactory = "containerFactory") public void receiveTplMessage(String message) { Long pageId = null; try { HashMaphashMap = JsonMapper.getInstance().fromJson(message, HashMap.class); pageId = Long.valueOf(String.valueOf(hashMap.get("pageId"))); // 查詢scene_page表中的page信息 ScenePage scenePage = scenePageDoc.findPageById(pageId); if (scenePage != null){ //查詢是否已經(jīng)同步過 ScenePageTpl scenePageTpl = scenePageDoc.findPageTplById(pageId); if (scenePageTpl == null){ scenePageDoc.savePageTpl(scenePage); logger.info("execute sync success pageId value is={}",pageId); // 建立UID 和 tpl 的關(guān)系 } // 刪除eqs_scene_page表的頁面數(shù)據(jù) scenePageDoc.removeScenePageById(pageId); } }catch (Exception e){ logger.error("執(zhí)行同步程序出現(xiàn)異常,error param is ={}", message); scenePageDoc.saveSyncError(pageId); e.printStackTrace(); } } }
mq優(yōu)化
@Configuration public class MqConfig { @Bean public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,ConnectionFactory connectionFactory){ SimpleRabbitListenerContainerFactory factory=new SimpleRabbitListenerContainerFactory(); // 設(shè)置線程池 ExecutorService service = new ThreadPoolExecutor(60,90,60, TimeUnit.SECONDS,new LinkedBlockingQueue(),new ThreadPoolExecutor.CallerRunsPolicy()); factory.setTaskExecutor(service); //設(shè)置consumer個數(shù) factory.setConcurrentConsumers(60); // 關(guān)閉ack factory.setAcknowledgeMode(AcknowledgeMode.NONE); configurer.configure(factory,connectionFactory); return factory; } }
下一步部署程序
nohup java -jar -Djava.security.egd=file:/dev/./urandom eqxiu-data-provider-0.0.5.jar --start=81947540 --end=81950540 --pageSize=3000 > /data/logs/tomcat/data-provider/spring.log & nohup java -jar eqxiu-data-consumer-0.0.5.jar > /data/logs/tomcat/data-consumer/spring.log &
但是執(zhí)行發(fā)現(xiàn),consumer的利用率并不高,如下圖:
查了下資料,consumer utilisation 低的原因有三點
1、消費者太少;
2、消費端的ack太慢;
3、消費者太多。
因為我設(shè)置了 factory.setAcknowledgeMode(AcknowledgeMode.NONE); 那么就不存在第二種原因,那么我就調(diào)整了一下vm option參數(shù),加大速度,很快consumer utilisation一直持續(xù)在96%以上,程序運行不到3個小時,數(shù)據(jù)都已經(jīng)遷移完畢;
優(yōu)化后的查詢速度如下圖:
感謝各位的閱讀,以上就是“如何解決mongodb深分頁的問題”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對如何解決mongodb深分頁的問題這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!