教程:使用 Azure Functions 和 Azure Redis 创建写入前高速缓存

本教程的目的是使用 Azure Cache for Redis 实例作为写后缓存。 本教程中的写后模式演示了写入到缓存如何触发对 Azure SQL 数据库(Azure SQL 数据库服务的实例)的相应写入。

你可使用适用于 Azure Functions 的 Redis 触发器来实现此功能。 在此方案中,你将了解如何使用 Redis 存储清单和定价信息,同时在 SQL 数据库中备份该信息。

写入缓存的每个新项或新价格后续会反映在数据库中的 SQL 表中。

在本教程中,你将了解如何执行以下操作:

  • 配置数据库、触发器和连接字符串。
  • 验证触发器是否正常工作。
  • 将代码部署到函数应用。

先决条件

  • Azure 订阅。 如果没有 Azure 订阅,请创建一个试用帐户
  • 完成上一篇教程(Azure Redis 中的 Azure Functions 触发器入门),其中预配了以下资源:
    • Azure Cache for Redis 实例
    • Azure Functions 实例
    • 使用 Azure SQL 的工作知识
    • 安装了 NuGet 包的 Visual Studio Code (VS Code) 环境设置

创建和配置新的 SQL 数据库

SQL 数据库是此示例的后备数据库。 可通过 Azure 门户或首选的自动化方法创建 SQL 数据库。

有关创建 SQL 数据库的详细信息,请参阅快速入门:创建单一数据库 - Azure SQL 数据库

此示例使用门户执行以下操作:

  1. 输入数据库名称并选择“新建”,创建新的服务器来保存数据库。

    创建 Azure SQL 资源的屏幕截图。

  2. 选择“使用 SQL 身份验证”并输入管理员登录名和密码。 请务必记住这些凭据或将其写下来。 在生产环境中部署服务器时,请改用 Microsoft Entra 身份验证。

    Azure SQL 资源的身份验证信息的屏幕截图。

  3. 转到“网络”选项卡,然后选择“公共终结点”作为连接方法。 对于显示的两个防火墙规则,请选择“”。 可通过 Azure 函数应用访问此终结点。

    Azure SQL 资源的网络设置的屏幕截图。

  4. 验证完成后,选择“查看 + 创建”,然后选择“创建”。 SQL 数据库将会开始部署。

  5. 部署完成后,转到 Azure 门户中的资源,然后选择“查询编辑器”选项卡。创建一个名为“清单”的新表,用于保存要写入其中的数据。 使用以下 SQL 命令生成带有以下两个字段的新表:

    • ItemName 列出每项的名称。
    • Price 存储项的价格。
    CREATE TABLE inventory (
        ItemName varchar(255),
        Price decimal(18,2)
        );
    

    显示在 Azure SQL 资源的查询编辑器中创建表的屏幕截图。

  6. 运行此命令后,展开“表”文件夹并验证是否已创建新表。

配置 Redis 触发器

首先,创建在上一个教程中使用的相同 VS Code 项目的副本。 使用新名称(例如 RedisWriteBehindTrigger)复制上一个教程中的文件夹,并在 VS Code 中将其打开。

其次,删除 RedisBindings.cs 和 RedisTriggers.cs 文件。

在此示例中,你将使用 pub/sub 触发器 来触发 keyevent 通知。 该示例的目标是:

  • 在每次发生 SET 事件时触发。 将新键写入缓存实例或更改键值时,会发生 SET 事件。
  • 触发 SET 事件后,请访问缓存实例来查找新键的值。
  • 确定 SQL 数据库的“清单”表中是否已存在该键。
    • 如果存在,请更新该键的值。
    • 如果不存在,请使用该键及其值写入新行。

若要配置触发器,请执行以下操作:

  1. 导入 NuGet 包 System.Data.SqlClient 来启用与 SQL 数据库的通信。 转到 VS Code 终端并使用以下命令:

      dotnet add package System.Data.SqlClient
    
  2. 创建名为 RedisFunction.cs 的新文件。 确保已删除 RedisBindings.cs 和 RedisTriggers.cs 文件。

  3. 将以下代码复制粘贴到 RedisFunction.cs 中,以替换现有代码:

using Microsoft.Extensions.Logging;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.Redis;
using System.Data.SqlClient;

public class WriteBehindDemo
{
    private readonly ILogger<WriteBehindDemo> logger;

    public WriteBehindDemo(ILogger<WriteBehindDemo> logger)
    {
        this.logger = logger;
    }
    
    public string SQLAddress = System.Environment.GetEnvironmentVariable("SQLConnectionString");

    //This example uses the PubSub trigger to listen to key events on the 'set' operation. A Redis Input binding is used to get the value of the key being set.
    [Function("WriteBehind")]
    public void WriteBehind(
        [RedisPubSubTrigger(Common.connectionString, "__keyevent@0__:set")] Common.ChannelMessage channelMessage,
        [RedisInput(Common.connectionString, "GET {Message}")] string setValue)
    {
        var key = channelMessage.Message; //The name of the key that was set
        var value = 0.0;

        //Check if the value is a number. If not, log an error and return.
        if (double.TryParse(setValue, out double result))
        {
            value = result; //The value that was set. (i.e. the price.)
            logger.LogInformation($"Key '{channelMessage.Message}' was set to value '{value}'");
        }
        else
        {
            logger.LogInformation($"Invalid input for key '{key}'. A number is expected.");
            return;
        }        

        // Define the name of the table you created and the column names.
        String tableName = "dbo.inventory";
        String column1Value = "ItemName";
        String column2Value = "Price";        
        
        logger.LogInformation($" '{SQLAddress}'");
        using (SqlConnection connection = new SqlConnection(SQLAddress))
            {
                connection.Open();
                using (SqlCommand command = new SqlCommand())
                {
                    command.Connection = connection;

                    //Form the SQL query to update the database. In practice, you would want to use a parameterized query to prevent SQL injection attacks.
                    //An example query would be something like "UPDATE dbo.inventory SET Price = 1.75 WHERE ItemName = 'Apple'".
                    command.CommandText = "UPDATE " + tableName + " SET " + column2Value + " = " + value + " WHERE " + column1Value + " = '" + key + "'";
                    int rowsAffected = command.ExecuteNonQuery(); //The query execution returns the number of rows affected by the query. If the key doesn't exist, it will return 0.

                    if (rowsAffected == 0) //If key doesn't exist, add it to the database
                 {
                         //Form the SQL query to update the database. In practice, you would want to use a parameterized query to prevent SQL injection attacks.
                         //An example query would be something like "INSERT INTO dbo.inventory (ItemName, Price) VALUES ('Bread', '2.55')".
                        command.CommandText = "INSERT INTO " + tableName + " (" + column1Value + ", " + column2Value + ") VALUES ('" + key + "', '" + value + "')";
                        command.ExecuteNonQuery();

                        logger.LogInformation($"Item " + key + " has been added to the database with price " + value + "");
                    }

                    else {
                        logger.LogInformation($"Item " + key + " has been updated to price " + value + "");
                    }
                }
                connection.Close();
            }

            //Log the time that the function was executed.
            logger.LogInformation($"C# Redis trigger function executed at: {DateTime.Now}");
    }
}

重要

此示例已为教程进行了简化。 对于生产用途,建议使用参数化 SQL 查询来防范 SQL 注入攻击。

配置连接字符串

需要更新 local.settings.json 文件,使其包含 SQL 数据库的连接字符串。 在 SQLConnectionStringValues 部分添加条目。 文件应如下例所示:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "redisConnectionString": "<redis-connection-string>",
    "SQLConnectionString": "<sql-connection-string>"
  }
}

若要查找 Redis 连接字符串,请转到 Azure Cache for Redis 资源中的资源菜单。 在“资源”菜单的“访问密钥”区域中找到该字符串。

要查找 SQL 数据库连接字符串,请转到 SQL 数据库资源中的资源菜单。 在“设置”下,依次选择“连接字符串”、“ADO.NET”选项卡。字符串位于ADO.NET(SQL 身份验证)区域中。

需要手动输入 SQL 数据库连接字符串的密码,因为密码不会自动粘贴。

重要

此示例已为教程进行了简化。

生成并运行项目

  1. 在 VS Code 中,转到“运行和调试”选项卡并运行项目。

  2. 回到 Azure 门户中的 Redis 实例,然后选择“控制台”按钮,进入 Redis 控制台。 尝试使用一些 SET 命令:

    • SET apple 5.25
    • SET bread 2.25
    • SET apple 4.50
  3. 回到 VS Code,此时正在注册触发器。 若要验证触发器是否正常工作,请执行以下操作:

    1. 转到 Azure 门户中的 SQL 数据库。

    2. 在资源菜单中,选择“查询编辑器”。

    3. 对于“新建查询”,请使用以下 SQL 命令创建查询,来查看清单表中的前 100 个项:

      SELECT TOP (100) * FROM [dbo].[inventory]
      

      确认此处显示了写入到 Redis 实例的项。

    显示信息已从缓存实例复制到 SQL 的屏幕截图。

将代码部署到函数应用

本教程以上一教程为基础。 有关详细信息,请参阅将代码部署到 Azure 函数

  1. 在 VS Code 中,转到“Azure”选项卡。

  2. 找到你的订阅并将其展开。 然后,找到“函数应用”部分并展开该部分。

  3. 选择并按住(或右键单击)函数应用,然后选择“部署到函数应用”。

添加连接字符串信息

本教程以上一教程为基础。 有关redisConnectionString的详细信息,请参阅添加连接字符串信息

  1. 在 Azure 门户中转到函数应用。 在资源菜单上,选择“环境变量”。

  2. 在“应用设置”窗格中,输入“SQLConnectionString”作为新字段。 对于“值”,输入你的连接字符串。

  3. 选择“应用”。

  4. 转到“概述”边栏选项卡,选择“重启”以使用新的连接字符串信息重启应用。

验证部署

部署完成后,请返回到 Redis 实例,并使用 SET 命令写入更多值。 确认这些值也显示在 SQL 数据库中。

如果要确认函数应用是否正常工作,请转到门户中的应用,然后从资源菜单中选择“日志流”。 应会看到触发器在这里运行,以及正在对 SQL 数据库进行的相应更新。

如果想要清除 SQL 数据库表而不将其删除,可使用以下 SQL 查询:

TRUNCATE TABLE [dbo].[inventory]

清理资源

要继续使用在本文中创建的资源,请保留资源组。

否则,如果已完成资源,可以删除创建的 Azure 资源组以避免产生费用。

重要

删除资源组的操作不可逆。 删除资源组时,包含在其中的所有资源会被永久删除。 请确保不会意外删除错误的资源组或资源。 如果在现有资源组(其中包含要保留的资源)内创建了此资源,可以逐个删除这些资源,而不是删除资源组。

删除资源组的步骤

  1. 登录到 Azure 门户,然后选择“资源组”。

  2. 选择要删除的资源组。

    如果有多个资源组,请使用“筛选任何字段...”框,键入为本文创建的资源组的名称。 在结果列表中选择资源组。

    在工作窗格中显示要删除的资源组列表的屏幕截图。

  3. 选择“删除资源组”。

  4. 系统会要求确认是否删除资源组。 键入资源组的名称进行确认,然后选择“删除”。

    显示需要资源名称才能确认删除的表单的屏幕截图。

片刻之后,将会删除该资源组及其所有资源。

总结

本教程和 Azure Redis 中的 Azure Functions 触发器入门显示了如何在 Azure 函数应用中使用 Redis 触发器和绑定。 它们还演示了如何将 Redis 用作 Azure SQL 数据库的写入前高速缓存。 Azure Cache for Redis 与 Azure Functions 的结合使用是一种强大的组合,可以解决许多集成和性能问题。