MongoDB 查询偶发返回空数据问题

MongoDB 查询偶发返回空数据问题

问题描述

/api/orderUser 接口在正常查询时,偶发性返回空数据列表(list: []),但接口返回 200 OK 状态码,响应格式正常。

现象

  • 同样的查询条件,有时有数据,有时没数据

  • 接口不报错,返回格式正常

  • 数据库中确实存在匹配的数据

示例请求


GET /api/orderUser?pageSize=20&isOrder=&page=1&timeOut=1&sword=惠州市路普电子商务有限公&goods=66ba2fc26b857249c8c87c03&swordType=1

正常响应


{

  "list": [...],

  "count": 100,

  "hasCheck": true

}

异常响应


{

  "list": [],

  "count": 0,

  "hasCheck": true

}


根因分析

1. MongoDB 驱动行为

MongoDB Node.js 驱动在以下情况下,toArray() 可能返回空数组而非抛出错误:

| 场景 | 行为 |

|-----|------|

| 连接超时 | 返回 [] |

| 查询超时 | 返回 [] |

| 连接池耗尽 | 返回 [] 或异常 |

| 网络抖动 | 返回 [] |

关键代码位置:


// src/app/api/orderUser/route.ts:251

list = await collection("order").find(match).sort({ "add_time": -1 }).skip(skipNum).limit(pageSize).toArray()

2. 为什么 Next.js 不报错?


// 查询返回空数组,代码继续执行

count = await collection("order").countDocuments(match);



// 构造正常响应

return succ({

    list: [],  // 空数组

    count: 0,

    hasCheck,

    ...count_data

})

MongoDB 驱动没有抛出异常,Next.js 认为查询成功,正常返回 200。

3. 触发条件

  • 数据库并发查询过多

  • MongoDB 服务器负载高

  • 网络不稳定

  • 连接池配置不足


解决方案

方案 1:添加查询超时限制(推荐)

在查询时设置最大执行时间,超时时抛出错误而非返回空数组:


// src/app/api/orderUser/route.ts



// 修改前

list = await collection("order").find(match)

    .sort({ "add_time": -1 })

    .skip(skipNum)

    .limit(pageSize)

    .toArray()



// 修改后

list = await collection("order").find(match)

    .sort({ "add_time": -1 })

    .skip(skipNum)

    .limit(pageSize)

    .maxTimeMS(10000)  // 10秒超时

    .toArray()

方案 2:添加重试机制

当检测到异常空数据时,自动重试查询:


// src/app/api/orderUser/route.ts



const findWithRetry = async (match: any, skipNum: number, pageSize: number, retryCount = 3) => {

    for (let i = 0; i < retryCount; i++) {

        const list = await collection("order")

            .find(match)

            .sort({ "add_time": -1 })

            .skip(skipNum)

            .limit(pageSize)

            .maxTimeMS(10000)

            .toArray();

        

        // 如果查到数据,或者是最后一次重试,直接返回

        if (list.length > 0 || i === retryCount - 1) {

            return list;

        }

        

        // 等待后重试(指数退避)

        await new Promise(r => setTimeout(r, 100 * Math.pow(2, i)));

    }

    return [];

};



// 使用

list = await findWithRetry(match, skipNum, pageSize);

方案 3:添加异常检测日志

在查询后检测异常情况,便于排查:


// src/app/api/orderUser/route.ts



const startTime = Date.now();

list = await collection("order").find(match)

    .sort({ "add_time": -1 })

    .skip(skipNum)

    .limit(pageSize)

    .maxTimeMS(10000)

    .toArray();



const queryTime = Date.now() - startTime;



// 检测异常:count > 0 但 list 为空

if (list.length === 0 && count > 0) {

    console.error("[MongoDB异常] 查询返回空数据:", {

        queryTime,

        match: JSON.stringify(match),

        page,

        pageSize,

        skipNum,

        count,

        timestamp: new Date().toISOString()

    });

}

方案 4:优化 MongoDB 连接配置

在数据库连接层增加超时和重连配置:


// src/models/db.ts(假设的数据库连接文件)



const client = new MongoClient(uri, {

    maxPoolSize: 50,        // 连接池大小

    minPoolSize: 10,

    maxIdleTimeMS: 30000,   // 连接最大空闲时间

    waitQueueTimeoutMS: 5000,  // 等待连接超时

    serverSelectionTimeoutMS: 10000,  // 服务器选择超时

    socketTimeoutMS: 30000,  // Socket 超时

});


完整修改示例

修改文件:src/app/api/orderUser/route.ts


// 在 GET 函数中,替换原有的查询逻辑



export const GET = async (request: NextRequest) => {

    try {

        // ... 前面的代码 ...



        const skipNum = (page - 1) * pageSize;

        

        // 添加重试的查询函数

        const findWithRetry = async (retryCount = 3) => {

            for (let i = 0; i < retryCount; i++) {

                const result = await collection("order")

                    .find(match)

                    .sort({ "add_time": -1 })

                    .skip(skipNum)

                    .limit(pageSize)

                    .maxTimeMS(10000)  // 10秒超时

                    .toArray();

                

                if (result.length > 0 || i === retryCount - 1) {

                    return result;

                }

                

                // 指数退避重试

                await new Promise(r => setTimeout(r, 100 * Math.pow(2, i)));

            }

            return [];

        };



        const startTime = Date.now();

        let list: O_DB[] = [];

        let count = 0;

        

        if (!isEmailMarketing && !isWechatMarketing && !isPhoneMarketing && !isSmsMarketing && !isRenewal) {

            // 普通查询,使用重试机制

            list = await findWithRetry();

            count = await collection("order").countDocuments(match);

            

            // 异常检测

            if (list.length === 0 && count > 0) {

                console.error("[MongoDB异常] 查询返回空数据:", {

                    queryTime: Date.now() - startTime,

                    match: JSON.stringify(match),

                    page,

                    pageSize,

                    skipNum,

                    count

                });

            }

        }

        

        // ... 后续代码 ...

    } catch (err) {

        return error((err as any).message)

    }

}


验证方法

1. 查看日志

部署修改后,查看 Next.js 日志:


# 查看错误日志

tail -f logs/error.log | grep "MongoDB异常"

2. 手动测试

快速连续请求接口 10 次,观察是否还有空数据:


for i in {1..10}; do

    curl -s "http://localhost:3090/api/orderUser?pageSize=20&page=1&sword=test" | jq '.list | length'

done

3. 监控指标

  • 查询响应时间

  • MongoDB 连接池使用率

  • 错误日志频率


相关代码位置

| 文件 | 行号 | 说明 |

|-----|------|------|

| src/app/api/orderUser/route.ts | 251 | 主查询逻辑 |

| src/app/api/orderUser/getUrlMatch.ts | 96 | 查询条件生成 |

| src/models/db.ts | - | 数据库连接配置 |


参考


本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。

萧兮的博客https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/019e4ece-9076-7da3-a6d9-4a87197783d0