You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Azure Redis Cache的cache.set()在Promise回调中无法执行的问题

Firebase Functions中Redis缓存回调内set操作无预警终止的原因分析

问题背景

在Google Firebase Functions上搭建Angular SPA中间件,引入Azure Redis Cache缓存高频查询以降低Azure SQL按使用付费成本。顶层代码中cache.set()可正常执行,但在SQL请求的requestCompleted事件回调内调用该方法时,程序无预警终止,无错误日志,Azure Insights也无该操作记录。移除cache.set()后SQL逻辑正常,尝试多种缓存实例方式均未解决。

可行代码示例

// "cache" 为全局对象
export const testCache = functions.https.onRequest(
  async (req: any, res) => {
    await cache.connect();
    const redisKey = 'testing_global';

    const result = await cache.get(redisKey);
    await cache.set(redisKey, 'testing new class');

    console.log("\nDone");
    
    cache.disconnect();
    res.send('done');
  }
);

不可行代码示例

import { createClient } from 'redis';

const cache = createClient({
  url: `rediss://${process.env.REDIS_HOST_NAME}:6380`,
  password: process.env.REDIS_KEY,
});

export const getValues = functions.https.onRequest(
  (req: any, response) => {
    cors(req, response, async () => {
      response.set('Access-Control-Allow-Origin', origin);

      const searchText = req.body['search'];
      const offset = req.body['offset'] || 0;
      const fetch = req.body['fetch'] || 200;
      const x = req.body['x'] || null;
      const y = req.body['y'] || null;
      let districts = req.body['districtFilter'];
      let sort = req.body['sort'];

      // 参数校验逻辑
      if (offset && !/^\d+$/.test(offset)) throw new Error('bad number');
      if (fetch && !/^\d+$/.test(fetch)) throw new Error('bad number');
      if (x && !/^-?\d+$/.test(x)) throw new Error('bad number');
      if (y && !/^-?\d+$/.test(y)) throw new Error('bad number');

      if (districts && districts.length > 0) {
        districts = sanitizeStringArray(districts);
      } else {
        districts = null;
      }

      if (sort) {
        switch (sort) {
          case "scoreAsc":
          case "scoreDesc":
          case "priceAsc":
          case "priceDesc":
            break;
          default:
            sort = '';
        }
      }

      const redisKey = `get_values_${searchText}_${offset}_${fetch}_${x}_${y}_${districts}_${sort}`;
      let cacheResult: any = null;

      await cache.connect();
  
      // 测试用:强制进入else分支
      cacheResult = await cache.getFromCache('someOtherKey');
    
      if (cacheResult !== null) {
        response.send({
          "status": "success",
          "totalCount": cacheResult.totalCount,
          "data": cacheResult.result
        });
        cache.disconnect();
      } else {
        const connection = new Connection(sqlConfig);
        let totalCount: number = 0;
  
        connection.on('connect', function (err: any) {
          console.log("Connected");
          const sql = `EXEC SomeSPC;`;
  
          const sqlRequest = new Request(sql, function (err: any) {
            if (err) console.log(err);
          });
  
          const countRequest = new Request(
            `EXEC SomeOtherSPC;`,
            function (err: any) {
              if (err) console.log(err);
            }
          )
  
          sqlRequest.connection = connection;
          countRequest.connection = connection;
  
          const result: any[] = [];
          sqlRequest.on('row', function (columns: any[]) {
            const rowResult: any = {};
            columns.forEach(function (column: any) {
              rowResult[column['metadata']['colName']] = column['value'];
            });
            result.push(rowResult);
          });
  
          sqlRequest.on("requestCompleted", function (rowCount: any, more: any) {
            console.log(rowCount + ' rows returned');
            connection.execSql(countRequest);
  
            countRequest.on('row', function (columns: any[]) {
              totalCount = columns[0]['value'];
            });
  
            countRequest.on('requestCompleted', async function (rowCount: any, more: any) {
              connection.close();
              cacheResult = { totalCount, result };
  
              // 此处执行后程序无预警终止,无报错
              await cache.set(redisKey, 'in Promise');
  
              response.send({
                "status": "success",
                "totalCount": totalCount,
                "data": result
              });
            });
          });
  
          connection.execSql(sqlRequest);
        });
  
        connection.on('infoMessage', infoError);
        connection.on('errorMessage', infoError);
        connection.on('end', end);
        connection.on('debug', debug);
  
        connection.connect();
        console.log("Reading rows from the Table...");
      }
    });
  }
);

核心原因

  1. Firebase Functions的生命周期机制
    Firebase Functions会在检测到请求处理完成(如调用response.send())后立即终止函数实例,不会等待回调中未完成的异步操作。在不可行代码中,response.send()虽在await cache.set()之后,但该回调是独立的事件监听函数,Firebase的执行上下文无法感知到这个异步任务的存在,会在response.send()执行后直接结束进程,导致cache.set()未完成就被强制终止。

  2. 回调API与async/await的上下文冲突
    SQL操作使用的是基于事件回调的传统Node.js API,而cache.set()是Promise异步操作。这种混合模式下,async函数的上下文未正确传递到事件回调中,Firebase无法追踪到该异步任务的状态,函数提前退出。

  3. Redis连接的时机问题
    在不可行代码中,cache.connect()是在cors的async函数内调用,后续回调中使用cache.set()时,连接可能未完全就绪,但结合无报错的表现,核心原因仍是函数生命周期管理问题。

解决方案建议

  1. 封装SQL回调API为Promise
    将基于事件的SQL操作转换为Promise形式,让整个请求流程处于统一的async/await上下文,确保Firebase能追踪所有异步操作:
// 封装SQL查询为Promise
function executeSqlQuery(connection: any, sql: string): Promise<any[]> {
  return new Promise((resolve, reject) => {
    const request = new Request(sql, (err) => {
      if (err) reject(err);
    });
    const result: any[] = [];
    request.on('row', (columns) => {
      const row: any = {};
      columns.forEach(col => row[col.metadata.colName] = col.value);
      result.push(row);
    });
    request.on('requestCompleted', () => resolve(result));
    connection.execSql(request);
  });
}

// 封装计数查询
function executeCountQuery(connection: any, sql: string): Promise<number> {
  return new Promise((resolve, reject) => {
    const request = new Request(sql, (err) => {
      if (err) reject(err);
    });
    let count = 0;
    request.on('row', (columns) => {
      count = columns[0].value;
    });
    request.on('requestCompleted', () => resolve(count));
    connection.execSql(request);
  });
}

之后在主流程中用await调用这些Promise,确保所有操作完成后再发送响应。

  1. 统一async/await上下文
    将所有逻辑放在顶层async函数中,避免嵌套事件回调,让Firebase能完整追踪异步任务:
// 重构后的核心逻辑示例
else {
  const connection = new Connection(sqlConfig);
  await new Promise((resolve, reject) => {
    connection.on('connect', (err) => {
      if (err) reject(err);
      resolve(null);
    });
    connection.connect();
  });

  const result = await executeSqlQuery(connection, `EXEC SomeSPC;`);
  totalCount = await executeCountQuery(connection, `EXEC SomeOtherSPC;`);
  
  connection.close();
  await cache.set(redisKey, JSON.stringify({ totalCount, result }));
  
  response.send({
    "status": "success",
    totalCount,
    data: result
  });
  cache.disconnect();
}
  1. 优化Redis连接复用
    将Redis客户端初始化放在函数外部,利用Firebase的实例复用特性,避免每次请求重新连接:
// 全局Redis客户端
const cache = createClient({
  url: `rediss://${process.env.REDIS_HOST_NAME}:6380`,
  password: process.env.REDIS_KEY,
});

// 提前建立连接
cache.connect().catch(err => console.error('Redis连接失败:', err));

// 函数内直接使用已连接的客户端,无需每次connect

内容的提问来源于stack exchange,提问作者LeftBrain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 17:35:12