Azure Redis Cache的cache.set()在Promise回调中无法执行的问题
问题背景
在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..."); } }); } );
核心原因
Firebase Functions的生命周期机制
Firebase Functions会在检测到请求处理完成(如调用response.send())后立即终止函数实例,不会等待回调中未完成的异步操作。在不可行代码中,response.send()虽在await cache.set()之后,但该回调是独立的事件监听函数,Firebase的执行上下文无法感知到这个异步任务的存在,会在response.send()执行后直接结束进程,导致cache.set()未完成就被强制终止。回调API与async/await的上下文冲突
SQL操作使用的是基于事件回调的传统Node.js API,而cache.set()是Promise异步操作。这种混合模式下,async函数的上下文未正确传递到事件回调中,Firebase无法追踪到该异步任务的状态,函数提前退出。Redis连接的时机问题
在不可行代码中,cache.connect()是在cors的async函数内调用,后续回调中使用cache.set()时,连接可能未完全就绪,但结合无报错的表现,核心原因仍是函数生命周期管理问题。
解决方案建议
- 封装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,确保所有操作完成后再发送响应。
- 统一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(); }
- 优化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

