后台查询接口影响响应时间最大的因素:用空间换时间的优缺点及解决方案

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 1.当数据库的一个表记录很多显然查询数据很慢。2.当数据库的一个表记录不大,但是数据很大也可能很慢。我们的一个用户表中一个building很大,当查询100条数据就会把服务器的内存搞爆掉。当然查询时要查询筛选有用字段,不可以直接把记录的所有字段都查拆来。这样能减少内存消耗和提高查询速度。3.在经常查询字段上建立索引。据说oracle上用索查询和不用索引查询在超多记录的情况下相差1000倍。4.若出现嵌套查询显然会大大增加相应查询时间。要先预处理用管道操作把能合并的查询合并到一个查询中,然后生成map,然后再处理。这是标准的用空间换时间的方案。

后台查询接口影响响应时间最大的因素:
1.记录数据条数;
2.记录数据大小;
3.是否在索引上查找;
4.查询数据次数。

分析:
1.当数据库的一个表记录很多显然查询数据很慢。
2.当数据库的一个表记录不大,但是数据很大也可能很慢。
我们的一个用户表中一个building很大,当查询100条数据就会把服务器的内存搞爆掉。
当然查询时要查询筛选有用字段,不可以直接把记录的所有字段都查拆来。这样能减少内存消耗和提高查询速度。
3.在经常查询字段上建立索引。据说oracle上用索查询和不用索引查询在超多记录的情况下相差1000倍。
4.若出现嵌套查询显然会大大增加相应查询时间。你想想一个查询按30毫秒计算,查询1000次要多少时间。要先预处理用管道操作把能合并的查询合并到一个查询中,然后生成map,然后再处理。这是标准的用空间换时间的方案。
当然当出现了大字段的情况,把嵌套查询转换成管道查询反而会出现内存爆掉导致无法实现的问题。这种情况通常出现在后台报表查询接口中。最佳的解决方案是部分采用管道,部分采用嵌套查询。这样既兼顾了嵌套查询的内存利用率低和减少查询次数的目的。若这样优化还是出现查询超时的问题,可以把get请求换成post请求。因为get请求都有默认超时时间60秒,而post通常没有默认超时时间,除非你专门设置。
第一个例子是采用嵌套查询导致响应时间增加的部分代码:

 let results=await WpUModel.find({
   
            user_type: '经纪人',
            user_name: new RegExp(searchName)
        },'-_id id user_name user_type').lean().exec();

        let res=[]
        for (const item of results) {
   
            let checkDetails = await WpUCModel.find({
   
                user_id:item.id
            },'-_id user_id create_time check_status check_details').sort({
   create_time:1}).lean().exec();

            // 同一个人可能有多次审核
            for (const checkDetail of checkDetails) {
   
                let {
   create_time,check_status,check_details}=checkDetail;

                let refer={
   
                    'PASS':'通过',
                    "REFUSE":'未通过',
                    "WAIT":'审核中'
                }
                check_status= refer[check_status];
                let userID=JSON.parse(check_details).userID;
                let introducer;
                if(userID){
   
                    introducer=await WpUModel.findOne({
   
                        id: userID,
                    },'-_id id user_name user_type').lean().exec();
                }
                res.push({
   
                    ...item,
                    create_time,
                    check_status,
                    introducer,
                })
            }


        }

显然签到查询用到的内存要小,代码简单,写的快。但是缺点很显然:查询数据库次数很多,影响响应时间,随着记录数的增加影响越大。
第二个例子,下面是使用管道预先处理优化后的代码:

let results = await WpUModel.find(
            {
   
                user_type: '经纪人',
                user_name: new RegExp(searchName),
            },
            '-_id id user_name user_type employee_num'
        ).lean().exec();

        let idList = results.map(x2 => x2.id);
        let res = [];
        let checkDetailsList = await WpUCModel.aggregate([
            // 第一阶段:匹配 user_id 在 idList 中的文档
            {
    $match: {
    user_id: {
   $in: idList } } },
            // 第二阶段:按 user_id 分组,并将每个分组的内容放入一个数组中
            {
   
                $group: {
   
                    _id: '$user_id', // 按 user_id 分组
                    checkDetails: {
   
                        $push: {
   
                            user_id: '$user_id',
                            create_time: '$create_time',
                            check_status: '$check_status',
                            check_details: '$check_details'
                        }
                    }
                }
            },
            // 第三阶段:重新投影,只包含需要的字段,并重命名 _id 为 user_id
            {
   
                $project: {
   
                    _id: 0, // 排除 _id 字段
                    user_id: '$_id', // 重命名 _id 为 user_id
                    checkDetails: 1 // 包含 details 字段
                }
            }
        ]);
        // logger.debug('checkDetailsList:', checkDetailsList);
        const filteredDataMap = new Map(checkDetailsList.map(item => [item.user_id, item.checkDetails]));
        let subIdList = [];
        checkDetailsList.forEach(item =>{
   
            if(apiCommonToolUtil.checkNotEmptArray(item.checkDetails)){
   
                item.checkDetails.forEach(x =>{
   
                    if(x && x.check_details){
   
                        let y = JSON.parse(x.check_details);
                        if(y && y.userID){
   
                            subIdList.push(y.userID);
                        }
                    }
                });
            }
        });
        let subUserList = await WpUModel.find(
            {
   
                id: {
   $in:subIdList}
            },
            '-_id id user_name user_type'
        ).lean().exec();
        const subUserMap = new Map(subUserList.map(item => [item.id, item]));
        // logger.debug('subUserMap:', subUserMap);
        for (const item of results) {
   
            let checkDetails = filteredDataMap.get(item.id);
            if(apiCommonToolUtil.checkNotEmptArray(checkDetails)){
   
                checkDetails.forEach(x =>{
   
                    if(x){
   
                        let create_time = x.create_time, check_status = x.check_status, check_details = x.check_details;
                        // logger.debug('create_time:', create_time, 'check_status:', check_status, 'check_details:', check_details);
                        let refer = {
   
                            PASS: '通过',
                            REFUSE: '未通过',
                            WAIT: '审核中',
                        };
                        check_status = refer[check_status];
                        let userID = JSON.parse(check_details).userID;
                        let introducer;
                        if(userID){
   
                            introducer = subUserMap.get(userID);  
                        }
                        // logger.debug('x:', x, 'userID:', userID, 'introducer:', introducer);
                        res.push({
   
                            ...item,
                            create_time,
                            check_status,
                            introducer,
                        });
                    }
                });
            }

        }

可以看到只进行三次数据查询就实现了第一个嵌套查询几十上百的查询。当然它也是有代价的,就是原来只需要查询一个记录现在需要一次性把成百上千的记录查询出来,这就需要用到更大的内存。代码复杂度也比第一个方案复杂的多,写代码和调试代码的时间也比第一方案要费时间。当然使用者只对响应时间敏感,开发者投入的时间是固定的一次性,使用者使用需要的时间是重复的。所以让开发者投入适当多的时间改善响应时间是值得,有利于提高效率和用户体验。有的公司只追求开发效率不讲用户体验,很容易引导开发者大量采用第一方案来提高开发效率,一旦养成了代码风气。开发的软件用户体验和竞争力可想而知。放眼都是嵌套查询和查询所有字段的代码,新来的显然也会抄来抄去。大部分开发人员在做代码搬运工,为了快速完成任务和提高每天的所谓效率,只会害了一个项目。只要你全部精力投入开发,没有无缘无故的慢也没有无缘无故的快。开发效率和软件质量同样重要,人不可能不吃不喝一直写代码,若一天投入开发的时间过多,反而早晨开发效率的下降。开发低质量的软件比开发得慢危害更大。
第三个例子:下面是一个请求耗时60秒以上的查询操作的部分代码:

let allData = {
   range: '全部'};
        let newRealEstateData = {
   range: '新房'};
        let secRealEstateData = {
   range: '二手房'};
        // 将结果转换成 Map 对象
        let realEstateMap = new Map(allRealEstate.map(item => [item.id, item.type]));
        // vr映射
        let vrMap = {
   };
        let hpMap = {
   };
        let vrs =  await WpVModel.find({
   }, '-_id type estate_id estates').lean().exec();
        let hpNewCount = 0, hpOldCount = 0;
        let vrNewHouseCount = 0, vrOldHouseCount = 0;
        vrs.forEach(vr => {
   
            if (vr.type === '航拍') {
   
                hpMap[vr.estate_id] = 1;
                if (vr.estates) {
   
                    vr.estates.forEach(x => {
   
                        hpMap[x] = 1;
                    });
                }
                if(vr.estate_id && realEstateMap){
   
                    let type = realEstateMap.get(vr.estate_id);
                    if(type){
   
                        if(type === '新房'){
   
                            hpNewCount = hpNewCount + 1;
                        }else{
   
                            hpOldCount = hpOldCount + 1;
                        }
                    }
                }
            } else if (vr.type === '样板房') {
   
                if(vr.estate_id && realEstateMap){
   
                    let type = realEstateMap.get(vr.estate_id);
                    if(type){
   
                        if(type === '新房'){
   
                            vrNewHouseCount = vrNewHouseCount + 1;
                        }else{
   
                            vrOldHouseCount = vrOldHouseCount + 1;
                        }
                    }
                }
                vrMap[vr.estate_id] = 1;
            }
        });
        vrs = null;
        for(let item of allRealEstate) {
   
            // 样板房
            if(vrMap[item.id]) {
   
                allData['样板房vr'] = (allData['样板房vr'] || 0) + 1;
                if(item.type === '新房') {
   
                    newRealEstateData['样板房vr'] = (newRealEstateData['样板房vr'] || 0) + 1;
                } else {
   
                    secRealEstateData['样板房vr'] = (secRealEstateData['样板房vr'] || 0) + 1;
                }
            }
            // 航拍
            if(hpMap[item.id]) {
   
                allData['航拍'] = (allData['航拍'] || 0) + 1;
                if(item.type === '新房') {
   
                    newRealEstateData['航拍'] = (newRealEstateData['航拍'] || 0) + 1;
                } else {
   
                    secRealEstateData['航拍'] = (secRealEstateData['航拍'] || 0) + 1;
                }
            }

            let {
   building, apartment} = await WpREModel.findOne({
   
                id: item.id
            }, '-_id building apartment').lean().exec();
            let kmap = {
   
                'hasSun': '日照',
                'hasScenery': '景观',
                'hasNoise': '噪声',
                'hasPrice': '一房一价',
                'east': '楼栋',
            };
            if(building) {
   
                let buildingArr = JSON.parse(building);
                let fmap = {
   };
                buildingArr.forEach(value => {
   
                    for (let key in kmap) {
   
                        if (value[key] && !fmap[key]) {
   
                            fmap[key] = 1;
                            let mk = kmap[key];
                            allData[mk] = (allData[mk] || 0) + 1;
                            if(item.type === '新房') {
   
                                newRealEstateData[mk] = (newRealEstateData[mk] || 0) + 1;
                            } else {
   
                                secRealEstateData[mk] = (secRealEstateData[mk] || 0) + 1;
                            }
                        }
                    }
                });
            }
            // 户型
            if(apartment) {
   
                let imgList = JSON.parse(apartment).imgList;
                imgList.forEach(async x => {
   
                    if(x.canvasImage) {
   
                        allData['户型'] = (allData['户型'] || 0) + 1;
                        if(item.type === '新房') {
   
                            newRealEstateData['户型'] = (newRealEstateData['户型'] || 0) + 1;
                        } else {
   
                            secRealEstateData['户型'] = (secRealEstateData['户型'] || 0) + 1;
                        }
                    }else{
   
                        allData['户型(无评测)'] = (allData['户型(无评测)'] || 0) + 1;
                        if(item.type === '新房') {
   
                            newRealEstateData['户型(无评测)'] = (newRealEstateData['户型(无评测)'] || 0) + 1;
                        } else {
   
                            secRealEstateData['户型(无评测)'] = (secRealEstateData['户型(无评测)'] || 0) + 1;
                        }
                    }
                });
            } 

        }

分析:这行代码超级耗时:

let vrs =  await WpVModel.find({
   }, '-_id type estate_id estates').lean().exec();

它耗时特别大,因为记录量太大基本无法优化,查询出来几万条。
后面的嵌套查询耗时是可以优化的。只是由于building超级大,所以不能按照第二个例子那样全部用管道处理一次性查询出来,不然会内存爆掉。
第四个例子:下面是部分采用管道来减少查询数组库的次数来达到提高查询处理速度:

let allData = {
   range: '全部'};
        let newRealEstateData = {
   range: '新房'};
        let secRealEstateData = {
   range: '二手房'};
        // 将结果转换成 Map 对象
        let realEstateMap = new Map(allRealEstate.map(item => [item.id, item.type]));
        // vr映射
        let vrMap = {
   };
        let hpMap = {
   };
        let vrs =  await WpVRModel.find({
   }, '-_id type estate_id estates').lean().exec();
        let hpNewCount = 0, hpOldCount = 0;
        let vrNewHouseCount = 0, vrOldHouseCount = 0;

        vrs.forEach(vr => {
   
            if (vr.type === '航拍') {
   
                hpMap[vr.estate_id] = 1;
                if (vr.estates) {
   
                    vr.estates.forEach(x => {
   
                        hpMap[x] = 1;
                    });
                }
                if(vr.estate_id && realEstateMap){
   
                    let type = realEstateMap.get(vr.estate_id);
                    if(type){
   
                        if(type === '新房'){
   
                            hpNewCount = hpNewCount + 1;
                        }else{
   
                            hpOldCount = hpOldCount + 1;
                        }
                    }
                }
            } else if (vr.type === '样板房') {
   
                if(vr.estate_id && realEstateMap){
   
                    let type = realEstateMap.get(vr.estate_id);
                    if(type){
   
                        if(type === '新房'){
   
                            vrNewHouseCount = vrNewHouseCount + 1;
                        }else{
   
                            vrOldHouseCount = vrOldHouseCount + 1;
                        }
                    }
                }
                vrMap[vr.estate_id] = 1;
            }
        });
        vrs = null;
        let maxIndex = 0;
        let length = allRealEstate.length;
        for(let i = 0; i < length; ){
   
            let list = [];
            if(i + 30 < length){
   
                maxIndex = i + 30;
                list = allRealEstate.slice(i, maxIndex);
            }else{
   
                maxIndex = length;
                list = allRealEstate.slice(i, maxIndex);
            }
            i = maxIndex;
            // logger.debug('i:', i, 'list.length:', list.length);
            let idList = list.map(x2 => x2.id);
            let buildingApartmentList = await WpREModel.find({
   
                id: {
   
                    $in: idList
                }
            }, '-_id id building apartment').lean().exec();
            // 将结果转换成 Map 对象
            let buildingApartmentMap = new Map(buildingApartmentList.map(item => [item.id, item]));
            list.forEach(item => {
   
                // let item = allRealEstate[i];
                // 样板房
                if(vrMap[item.id]) {
   
                    allData['样板房vr'] = (allData['样板房vr'] || 0) + 1;
                    if(item.type === '新房') {
   
                        newRealEstateData['样板房vr'] = (newRealEstateData['样板房vr'] || 0) + 1;
                    } else {
   
                        secRealEstateData['样板房vr'] = (secRealEstateData['样板房vr'] || 0) + 1;
                    }
                }
                // 航拍
                if(hpMap[item.id]) {
   
                    allData['航拍'] = (allData['航拍'] || 0) + 1;
                    if(item.type === '新房') {
   
                        newRealEstateData['航拍'] = (newRealEstateData['航拍'] || 0) + 1;
                    } else {
   
                        secRealEstateData['航拍'] = (secRealEstateData['航拍'] || 0) + 1;
                    }
                }

                let buildingApartmentEntity = buildingApartmentMap.get(item.id);
                let kmap = {
   
                    'hasSun': '日照',
                    'hasScenery': '景观',
                    'hasNoise': '噪声',
                    'hasPrice': '一房一价',
                    'east': '楼栋',
                };
                if(buildingApartmentEntity && buildingApartmentEntity.building) {
   
                    let buildingArr = JSON.parse(buildingApartmentEntity.building);
                    let fmap = {
   };
                    buildingArr.forEach(value => {
   
                        for (let key in kmap) {
   
                            if (value[key] && !fmap[key]) {
   
                                fmap[key] = 1;
                                let mk = kmap[key];
                                allData[mk] = (allData[mk] || 0) + 1;
                                if(item.type === '新房') {
   
                                    newRealEstateData[mk] = (newRealEstateData[mk] || 0) + 1;
                                } else {
   
                                    secRealEstateData[mk] = (secRealEstateData[mk] || 0) + 1;
                                }
                            }
                        }
                    });
                }
                // 户型
                if(buildingApartmentEntity && buildingApartmentEntity.apartment) {
   
                    let imgList = JSON.parse(buildingApartmentEntity.apartment).imgList;
                    imgList.forEach(async x => {
   
                        if(x.canvasImage) {
   
                            allData['户型'] = (allData['户型'] || 0) + 1;
                            if(item.type === '新房') {
   
                                newRealEstateData['户型'] = (newRealEstateData['户型'] || 0) + 1;
                            } else {
   
                                secRealEstateData['户型'] = (secRealEstateData['户型'] || 0) + 1;
                            }
                        }else{
   
                            allData['户型(无评测)'] = (allData['户型(无评测)'] || 0) + 1;
                            if(item.type === '新房') {
   
                                newRealEstateData['户型(无评测)'] = (newRealEstateData['户型(无评测)'] || 0) + 1;
                            } else {
   
                                secRealEstateData['户型(无评测)'] = (secRealEstateData['户型(无评测)'] || 0) + 1;
                            }
                        }
                    });
                } 
            });
        }

可以看到原来7826次的查询只需要261次查询做到了。就是每次只查30个记录,来避免内存爆掉,同时大大减少了查询次数,来减少查询响应时间。

目录
相关文章
|
4月前
|
存储 算法 大数据
指标类需求问题之在商品开发和运营过程中,减少指标计算以节省人效要怎么操作
指标类需求问题之在商品开发和运营过程中,减少指标计算以节省人效要怎么操作
|
4月前
|
Docker 容器
典型热点应用问题之追求60秒构建时间目标的问题如何解决
典型热点应用问题之追求60秒构建时间目标的问题如何解决
|
6月前
|
运维 Serverless KVM
函数计算产品使用问题之如何处理冷启动时间过长的问题
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
7月前
|
分布式计算 Java 数据库连接
回答粉丝疑问:Spark为什么调优需要降低过多小任务,降低单条记录的资源开销?
回答粉丝疑问:Spark为什么调优需要降低过多小任务,降低单条记录的资源开销?
61 1
|
5月前
|
缓存 自然语言处理 Java
浅析JAVA日志中的性能实践与原理解释问题之减少看得见的业务开销问题如何解决
浅析JAVA日志中的性能实践与原理解释问题之减少看得见的业务开销问题如何解决
|
缓存 算法 Cloud Native
面试技巧:如何在有限时间内优化代码性能
面试技巧:如何在有限时间内优化代码性能
73 0
|
缓存 测试技术 数据库
软件测试面试题:假设在测试过程中某些事务的响应时间过长,但分析应用服务、数据库以及网络都属于正常现象,问题可能出现的原因有哪些?
软件测试面试题:假设在测试过程中某些事务的响应时间过长,但分析应用服务、数据库以及网络都属于正常现象,问题可能出现的原因有哪些?
369 0
|
存储 缓存 Dart
如何处理直播实时在线人数显示并且最小化性能和资源消耗?
直播技术成为一种极为流行的交流方式。而直播平台的核心指标之一就是实时在线人数,准确地显示该指标对于用户和运营商来说都具有重要意义。然而,直播实时在线人数的显示也面临着性能和资源消耗的挑战。本文将介绍如何利用Flutter和Dart开发技术栈来优化直播实时在线人数的显示,以达到最小化性能和资源消耗的目标。 作者:狗头大军之江苏分军 链接:https://juejin.cn/spost/7255473856234913852 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如何处理直播实时在线人数显示并且最小化性能和资源消耗?
|
存储 开发框架 负载均衡
限流的非常规用途 - 缓解抢购压力
限流的非常规用途 - 缓解抢购压力
117 0
|
SQL 消息中间件 JavaScript
效率加倍,高并发场景下的接口请求合并方案
效率加倍,高并发场景下的接口请求合并方案
下一篇
无影云桌面