如何从AWS Lambda函数连接到Redis实例?

25
我正在尝试使用AWS LambdaServerless Framework为单页Web应用程序构建API。我想使用Redis Cloud进行存储,主要是因为其速度和数据持久性的组合。将来可能会使用更多的Redis Cloud功能,因此我希望避免使用ElastiCache。我的Redis Cloud实例正在与我的函数相同的AWS区域中运行。
我有一个名为related的函数,它从API端点的GET请求中获取一个标签,并检查数据库中是否有该条目。如果存在,则应立即返回结果。如果不存在,则应查询RiteTag,将结果写入Redis,然后将结果返回给用户。
我对此还比较陌生,所以我可能在做一些可爱而幼稚的事情。以下是事件处理程序:
'use strict'

const lib = require('../lib/related')

module.exports.handler = function (event, context) {
  lib.respond(event, (err, res) => {
    if (err) {
      return context.fail(err)
    } else {
      return context.succeed(res)
    }
  })
}

这是 ../lib/related.js 文件:

var redis = require('redis')
var jsonify = require('redis-jsonify')
var rt = require('./ritetag')
var redisOptions = {
  host: process.env.REDIS_URL,
  port: process.env.REDIS_PORT,
  password: process.env.REDIS_PASS
}
var client = jsonify(redis.createClient(redisOptions))

module.exports.respond = function (event, callback) {
  var tag = event.hashtag.replace(/^#/, '')
  var key = 'related:' + tag

  client.on('connect', () => {
    console.log('Connected:', client.connected)
  })

  client.on('end', () => {
    console.log('Connection closed.')
  })

  client.on('ready', function () {
    client.get(key, (err, res) => {
      if (err) {
        client.quit()
        callback(err)
      } else {
        if (res) {
          // Tag is found in Redis, so send results directly.
          client.quit()
          callback(null, res)
        } else {
          // Tag is not yet in Redis, so query Ritetag.
          rt.hashtagDirectory(tag, (err, res) => {
            if (err) {
              client.quit()
              callback(err)
            } else {
              client.set(key, res, (err) => {
                if (err) {
                  callback(err)
                } else {
                  client.quit()
                  callback(null, res)
                }
              })
            }
          })
        }
      }
    })
  })
}

所有这些都按预期工作,但只到一定程度。如果我在本地运行函数(使用sls function run related),我完全没有任何问题 - 标签按照应有的方式从Redis数据库中读取和写入。然而,当我部署它(使用sls dash deploy)时,它在部署后第一次运行时可以工作,然后就停止工作了。所有后续尝试运行它只是简单地向浏览器(或Postman、curl或Web应用程序)返回null。不管我用于测试的标签是否已经存在于数据库中,这都是真实的。如果我重新部署,对函数本身不做任何更改,它再次工作-一次。
在我的本地机器上,该函数首先将Connected: true记录到控制台,然后记录查询的结果,然后是Connection closed.。在AWS上,它记录Connected: true,然后是查询的结果,仅此而已。在第二次运行时,它记录Connection closed.,没有别的东西。在第三次及所有后续运行中,它根本不记录任何内容。两个环境都从未报告任何错误。
似乎问题明显出在与 Redis 的连接上。如果我在回调函数中不关闭它,那么后续尝试调用该函数只会超时。我还尝试使用 redis.unref 替代 redis.quit,但似乎没有任何区别。

非常感谢您的帮助。

1个回答

38

我现在已经解决了自己的问题,希望我的解决过程可以帮助未来遇到这个问题的人。

连接到像我在上面代码中所做的那样的数据库时,有两个主要的注意事项:

  1. 一旦调用 context.succeed()context.fail()context.done() 中任意一个方法,AWS 可能会冻结尚未完成的任何进程。这就是导致 AWS 在第二次调用我的 API 端点时记录“连接已关闭”的原因——进程在 Redis 完成关闭之前被冻结,然后在下一个调用时解冻,在此时继续刚才的操作,并报告连接已关闭。要点:如果想要关闭数据库连接,请确保在调用这些方法之前它已完全关闭。你可以通过在由连接关闭触发的事件处理程序(如.on('end'))中放置回调函数来实现这一点(以我的例子为例)。
  2. 如果将代码拆分为不同的文件并在每个文件的顶部使用require导入,就像我所做的那样,Amazon 将尽可能多地缓存这些模块在内存中。如果这造成了问题,请尝试将require()调用移动到函数内而不是文件顶部,然后导出该函数。当运行该函数时,这些模块将被重新导入。

这是我更新的代码。请注意,我还将 Redis 配置放到了单独的文件中,以便在其他 Lambda 函数中导入它,避免了重复编写代码。

事件处理程序

'use strict'

const lib = require('../lib/related')

module.exports.handler = function (event, context) {
  lib.respond(event, (err, res) => {
    if (err) {
      return context.fail(err)
    } else {
      return context.succeed(res)
    }
  })
}

Redis 配置

module.exports = () => {
  const redis = require('redis')
  const jsonify = require('redis-jsonify')
  const redisOptions = {
    host: process.env.REDIS_URL,
    port: process.env.REDIS_PORT,
    password: process.env.REDIS_PASS
  }

  return jsonify(redis.createClient(redisOptions))
}

这个函数

'use strict'

const rt = require('./ritetag')

module.exports.respond = function (event, callback) {
  const redis = require('./redis')()

  const tag = event.hashtag.replace(/^#/, '')
  const key = 'related:' + tag
  let error, response

  redis.on('end', () => {
    callback(error, response)
  })

  redis.on('ready', function () {
    redis.get(key, (err, res) => {
      if (err) {
        redis.quit(() => {
          error = err
        })
      } else {
        if (res) {
          // Tag is found in Redis, so send results directly.
          redis.quit(() => {
            response = res
          })
        } else {
          // Tag is not yet in Redis, so query Ritetag.
          rt.hashtagDirectory(tag, (err, res) => {
            if (err) {
              redis.quit(() => {
                error = err
              })
            } else {
              redis.set(key, res, (err) => {
                if (err) {
                  redis.quit(() => {
                    error = err
                  })
                } else {
                  redis.quit(() => {
                    response = res
                  })
                }
              })
            }
          })
        }
      }
    })
  })
}

这正如它应该的那样工作 - 而且速度飞快。


8
这是很好的信息。我只想澄清一个关于AWS Lambda中“require”的误解:那些“require”被缓存并不是由AWS缓存的,而是Node.js核心模块导入器的工作方式。正如你所说,导出函数是处理这个问题最安全的方式。 - Jonathan Kempf
嗨@JonathanKempf,你可以更具体一些吗?为什么导出函数是处理此问题最安全的方式? - C.Lee
7
在Node.js中,require的工作方式是在运行时获取并缓存任何需要的模块。如果所需的代码需要调用者上下文来给出正确结果,则可能会出现问题,因为Node环境对缓存的首次运行进行操作。要正确执行此操作的一种方法是,使用将作为IIFE运行并在编译代码时返回“新”的执行的函数进行引用。 - Jonathan Kempf
3
@JonathanKempf 这个跟ES6中的import是一样的吗?谢谢。 - elliotrock

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接