您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn.

快速入门:将 Azure Redis 缓存与 ASP.NET Web 应用配合使用

在本快速入门中,将使用 Visual Studio 2019 创建一个 ASP.NET Web 应用程序,该应用程序连接到 Azure Redis 缓存以存储和检索缓存中的数据。 然后,将该应用部署到 Azure 应用服务。

跳到 GitHub 上的代码

如果要直接跳到代码,请参阅 GitHub 上的 ASP.NET 快速入门

先决条件

创建 Visual Studio 项目

  1. 打开 Visual Studio,然后选择“文件” > “新建” > “项目”。

  2. 在“创建新项目”对话框中执行以下步骤:

    创建项目

    a. 在搜索框中,输入 C# ASP.NET Web 应用程序

    b. 选择“ASP.NET Web 应用程序(.NET Framework)”。

    c. 选择“下一步”。

  3. 在“项目名称”框中,为项目提供一个名称。 在此示例中,我们使用了 ContosoTeamStats

  4. 验证是否已选择“.NET Framework 4.6.1”或更高版本。

  5. 选择“创建” 。

  6. 选择“MVC”作为项目类型。

  7. 对于“身份验证”设置,请确保指定“不进行身份验证”。 默认的“身份验证”设置可能因 Visual Studio 版本而异。 若要对其进行更改,请选择“更改身份验证”,然后选择“不进行身份验证”。

  8. 选择“创建”来创建项目。

创建缓存

接下来,为应用创建缓存。

  1. 若要创建缓存,请登录到 Azure 门户并选择“创建资源” 。

    左侧导航窗格中突出显示了“创建资源”。

  2. 在“新建”页上选择“数据库”,然后选择“Azure Cache for Redis”。

    在“新建”中,突出显示了“数据库”和“Azure Cache for Redis”。

  3. 在“新建 Redis 缓存”页上配置新缓存的设置。

    设置 选择值 说明
    订阅 单击下拉箭头并选择你的订阅。 要在其下创建此新的 Azure Cache for Redis 实例的订阅。
    资源组 单击下拉箭头并选择一个资源组,或者选择“新建”并输入新的资源组名称。 要在其中创建缓存和其他资源的资源组的名称。 将所有应用资源放入一个资源组可以轻松地统一管理或删除这些资源。
    DNS 名称 输入唯一名称。 缓存名称必须是包含 1 到 63 个字符的字符串,只能包含数字、字母或连字符。 该名称必须以数字或字母开头和结尾,且不能包含连续的连字符。 缓存实例的主机名是 <DNS name> .redis.cache.windows.net
    位置 单击下拉箭头并选择一个位置。 选择与要使用该缓存的其他服务靠近的区域
    缓存类型 下拉并选择一个 此层决定可用于缓存的大小、性能和功能。 有关详细信息,请参阅用于 Redis 的 Azure 缓存概述
  4. 选择“网络”选项卡,或选择“网络”按钮(位于页面底部) 。

  5. 在“网络”选项卡中,选择你的连接方法。

  6. 选择“下一步:高级”选项卡,或者选择页面底部的“下一步:高级”按钮 。

  7. 在基本或标准缓存实例的“高级”选项卡中,如果想要启用非 TLS 端口,请选择启用开关。 还可以选择你想使用的 Redis 版本,4 或 6。

    Redis 版本 4 或 6。

  8. 在高级缓存实例的“高级”选项卡中,配置非 TLS 端口、群集和数据持久性的设置。 还可以选择你想使用的 Redis 版本,4 或 6。

  9. 选择“下一步: 标记”选项卡,或者选择页面底部的“下一步: 标记”按钮 。

  10. 或者,在“标记”选项卡中,如果希望对资源分类,请输入名称或值。

  11. 选择“查看 + 创建” 。 随后你会转到“查看 + 创建”选项卡,Azure 将在此处验证配置。

  12. 显示绿色的“已通过验证”消息后,选择“创建”。

创建缓存需要花费片刻时间。 可以在 Azure Cache for Redis 的“概述”页上监视进度。 如果“状态”显示为“正在运行”,则表示该缓存可供使用。

从 Azure 门户检索主机名、端口和访问密钥

若要连接到某个 Azure Cache for Redis 实例,缓存客户端需要该缓存的主机名、端口和密钥。 在某些客户端中,这些项的名称可能略有不同。 可以从 Azure 门户检索主机名、端口和访问密钥。

  • 若要获取访问密钥,请在缓存的左侧导航中选择“访问密钥”。

    Azure Redis 缓存密钥

  • 若要获取主机名和端口,请在缓存的左侧导航栏中选择“属性”。 主机名的格式为 <DNS name>.redis.cache.windows.net。

    Azure Redis 缓存属性

编辑 CacheSecrets.config 文件的步骤

  1. 在计算机上创建名为 CacheSecrets.config 的文件。将其放到不会连同示例应用程序的源代码一起签入的位置。 在本快速入门中,CacheSecrets.config 文件的路径为 C:\AppSecrets\CacheSecrets.config

  2. 编辑 CacheSecrets.config 文件。 然后添加以下内容:

    <appSettings>
        <add key="CacheConnection" value="<cache-name>.redis.cache.windows.net,abortConnect=false,ssl=true,allowAdmin=true,password=<access-key>"/>
    </appSettings>
    
  3. <cache-name> 替换为缓存主机名。

  4. <access-key> 替换缓存的主密钥。

    提示

    在密钥轮换期间重新生成主访问密钥时,可以将辅助访问密钥用作备用密钥。

  5. 保存文件。

更新 MVC 应用程序

在本部分,请对应用程序进行更新,使之支持一个新视图,该视图显示针对 Azure Redis 缓存执行的一项简单测试。

使用缓存的应用设置更新 web.config 文件

在本地运行应用程序时,将使用 CacheSecrets.config 中的信息连接到 Azure Redis 缓存实例。 稍后请将此应用程序部署到 Azure。 到时,请在 Azure 中配置一项应用设置,供应用程序用来检索缓存连接信息而不是此文件。

由于 CacheSecrets.config 文件未连同应用程序一起部署到 Azure,因此,只是在本地测试应用程序时才使用此文件。 请尽量安全地保管此信息,防止有人恶意访问缓存数据。

更新 web.config 文件的步骤

  1. 在“解决方案资源管理器”中,双击“web.config”文件将其打开。

    Web.config

  2. web.config 文件中找到 <appSetting> 元素。 然后添加以下 file 属性。 如果使用了其他文件名或位置,请使用这些值来替换示例中显示的值。

  • 之前: <appSettings>
  • 之后:<appSettings file="C:\AppSecrets\CacheSecrets.config">

ASP.NET 运行时合并了外部文件的内容以及 <appSettings> 元素中的标记。 如果找不到指定的文件,运行时会忽略文件属性。 应用程序的源代码中将不包括机密(连接到缓存的连接字符串)。 将 Web 应用部署到 Azure 时,不会部署 CacheSecrets.config 文件。

将应用程序配置为使用 StackExchange.Redis 的步骤

  1. 若要将应用配置为使用 Visual Studio 的 StackExchange.Redis NuGet 包,请选择“工具”>“NuGet 包管理器”>“包管理器控制台”。

  2. Package Manager Console 窗口运行以下命令:

    Install-Package StackExchange.Redis
    
  3. NuGet 程序包会为客户端应用程序下载并添加所需的程序集引用,以通过 StackExchange.Azure Redis 缓存客户端访问 Azure Redis 缓存。 如果更愿使用强命名版本的 StackExchange.Redis 客户端库,请安装 StackExchange.Redis 包。

更新 HomeController 和布局的步骤

  1. 在“解决方案资源管理器”中展开“Controllers”文件夹,然后打开“HomeController.cs”文件。

  2. 在文件的顶部,添加以下 using 语句。

    using StackExchange.Redis;
    using System.Configuration;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    
  3. 将以下成员添加到 HomeController 类,使之支持针对新缓存运行某些命令的新 RedisCache 操作。

         public async Task<ActionResult> RedisCache()
        {
            ViewBag.Message = "A simple example with Azure Cache for Redis on ASP.NET.";
    
            if (Connection == null)
            {
                await InitializeAsync();
            }
    
            IDatabase cache = await GetDatabaseAsync();
    
            // Perform cache operations using the cache object...
    
            // Simple PING command
            ViewBag.command1 = "PING";
            ViewBag.command1Result = cache.Execute(ViewBag.command1).ToString();
    
            // Simple get and put of integral data types into the cache
            ViewBag.command2 = "GET Message";
            ViewBag.command2Result = cache.StringGet("Message").ToString();
    
            ViewBag.command3 = "SET Message \"Hello! The cache is working from ASP.NET!\"";
            ViewBag.command3Result = cache.StringSet("Message", "Hello! The cache is working from ASP.NET!").ToString();
    
            // Demonstrate "SET Message" executed as expected...
            ViewBag.command4 = "GET Message";
            ViewBag.command4Result = cache.StringGet("Message").ToString();
    
            // Get the client list, useful to see if connection list is growing...
            // Note that this requires allowAdmin=true in the connection string
            ViewBag.command5 = "CLIENT LIST";
            StringBuilder sb = new StringBuilder();
            var endpoint = (System.Net.DnsEndPoint)(await GetEndPointsAsync())[0];
            IServer server = await GetServerAsync(endpoint.Host, endpoint.Port);
            ClientInfo[] clients = await server.ClientListAsync();
    
            sb.AppendLine("Cache response :");
            foreach (ClientInfo client in clients)
            {
                sb.AppendLine(client.Raw);
            }
    
            ViewBag.command5Result = sb.ToString();
    
            return View();
        }
    
        private static long _lastReconnectTicks = DateTimeOffset.MinValue.UtcTicks;
        private static DateTimeOffset _firstErrorTime = DateTimeOffset.MinValue;
        private static DateTimeOffset _previousErrorTime = DateTimeOffset.MinValue;
    
        private static SemaphoreSlim _reconnectSemaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
        private static SemaphoreSlim _initSemaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
    
        private static ConnectionMultiplexer _connection;
        private static bool _didInitialize = false;
    
        // In general, let StackExchange.Redis handle most reconnects,
        // so limit the frequency of how often ForceReconnect() will
        // actually reconnect.
        public static TimeSpan ReconnectMinInterval => TimeSpan.FromSeconds(60);
    
        // If errors continue for longer than the below threshold, then the
        // multiplexer seems to not be reconnecting, so ForceReconnect() will
        // re-create the multiplexer.
        public static TimeSpan ReconnectErrorThreshold => TimeSpan.FromSeconds(30);
    
        public static TimeSpan RestartConnectionTimeout => TimeSpan.FromSeconds(15);
    
        public static int RetryMaxAttempts => 5;
    
        public static ConnectionMultiplexer Connection { get { return _connection; } }
    
        public static async Task InitializeAsync()
        {
            if (_didInitialize)
            {
                throw new InvalidOperationException("Cannot initialize more than once.");
            }
    
            _connection = await CreateConnectionAsync();
            _didInitialize = true;
        }
    
        // This method may return null if it fails to acquire the semaphore in time.
        // Use the return value to update the "connection" field
        private static async Task<ConnectionMultiplexer> CreateConnectionAsync()
        {
            if (_connection != null)
            {
                // If we already have a good connection, let's re-use it
                return _connection;
            }
    
            try
            {
                await _initSemaphore.WaitAsync(RestartConnectionTimeout);
            }
            catch
            {
                // We failed to enter the semaphore in the given amount of time. Connection will either be null, or have a value that was created by another thread.
                return _connection;
            }
    
            // We entered the semaphore successfully.
            try
            {
                if (_connection != null)
                {
                    // Another thread must have finished creating a new connection while we were waiting to enter the semaphore. Let's use it
                    return _connection;
                }
    
                // Otherwise, we really need to create a new connection.
                string cacheConnection = ConfigurationManager.AppSettings["CacheConnection"].ToString();
                return await ConnectionMultiplexer.ConnectAsync(cacheConnection);
            }
            finally
            {
                _initSemaphore.Release();
            }
        }
    
        private static async Task CloseConnectionAsync(ConnectionMultiplexer oldConnection)
        {
            if (oldConnection == null)
            {
                return;
            }
            try
            {
                await oldConnection.CloseAsync();
            }
            catch (Exception)
            {
                // Ignore any errors from the oldConnection
            }
        }
    
        /// <summary>
        /// Force a new ConnectionMultiplexer to be created.
        /// NOTES:
        ///     1. Users of the ConnectionMultiplexer MUST handle ObjectDisposedExceptions, which can now happen as a result of calling ForceReconnectAsync().
        ///     2. Call ForceReconnectAsync() for RedisConnectionExceptions and RedisSocketExceptions. You can also call it for RedisTimeoutExceptions,
        ///         but only if you're using generous ReconnectMinInterval and ReconnectErrorThreshold. Otherwise, establishing new connections can cause
        ///         a cascade failure on a server that's timing out because it's already overloaded.
        ///     3. The code will:
        ///         a. wait to reconnect for at least the "ReconnectErrorThreshold" time of repeated errors before actually reconnecting
        ///         b. not reconnect more frequently than configured in "ReconnectMinInterval"
        /// </summary>
        public static async Task ForceReconnectAsync()
        {
            var utcNow = DateTimeOffset.UtcNow;
            long previousTicks = Interlocked.Read(ref _lastReconnectTicks);
            var previousReconnectTime = new DateTimeOffset(previousTicks, TimeSpan.Zero);
            TimeSpan elapsedSinceLastReconnect = utcNow - previousReconnectTime;
    
            // If multiple threads call ForceReconnectAsync at the same time, we only want to honor one of them.
            if (elapsedSinceLastReconnect < ReconnectMinInterval)
            {
                return;
            }
    
            try
            {
                await _reconnectSemaphore.WaitAsync(RestartConnectionTimeout);
            }
            catch
            {
                // If we fail to enter the semaphore, then it is possible that another thread has already done so.
                // ForceReconnectAsync() can be retried while connectivity problems persist.
                return;
            }
    
            try
            {
                utcNow = DateTimeOffset.UtcNow;
                elapsedSinceLastReconnect = utcNow - previousReconnectTime;
    
                if (_firstErrorTime == DateTimeOffset.MinValue)
                {
                    // We haven't seen an error since last reconnect, so set initial values.
                    _firstErrorTime = utcNow;
                    _previousErrorTime = utcNow;
                    return;
                }
    
                if (elapsedSinceLastReconnect < ReconnectMinInterval)
                {
                    return; // Some other thread made it through the check and the lock, so nothing to do.
                }
    
                TimeSpan elapsedSinceFirstError = utcNow - _firstErrorTime;
                TimeSpan elapsedSinceMostRecentError = utcNow - _previousErrorTime;
    
                bool shouldReconnect =
                    elapsedSinceFirstError >= ReconnectErrorThreshold // Make sure we gave the multiplexer enough time to reconnect on its own if it could.
                    && elapsedSinceMostRecentError <= ReconnectErrorThreshold; // Make sure we aren't working on stale data (e.g. if there was a gap in errors, don't reconnect yet).
    
                // Update the previousErrorTime timestamp to be now (e.g. this reconnect request).
                _previousErrorTime = utcNow;
    
                if (!shouldReconnect)
                {
                    return;
                }
    
                _firstErrorTime = DateTimeOffset.MinValue;
                _previousErrorTime = DateTimeOffset.MinValue;
    
                ConnectionMultiplexer oldConnection = _connection;
                await CloseConnectionAsync(oldConnection);
                _connection = null;
                _connection = await CreateConnectionAsync();
                Interlocked.Exchange(ref _lastReconnectTicks, utcNow.UtcTicks);
            }
            finally
            {
                _reconnectSemaphore.Release();
            }
        }
    
        // In real applications, consider using a framework such as
        // Polly to make it easier to customize the retry approach.
        private static async Task<T> BasicRetryAsync<T>(Func<T> func)
        {
            int reconnectRetry = 0;
            int disposedRetry = 0;
    
            while (true)
            {
                try
                {
                    return func();
                }
                catch (Exception ex) when (ex is RedisConnectionException || ex is SocketException)
                {
                    reconnectRetry++;
                    if (reconnectRetry > RetryMaxAttempts)
                        throw;
                    await ForceReconnectAsync();
                }
                catch (ObjectDisposedException)
                {
                    disposedRetry++;
                    if (disposedRetry > RetryMaxAttempts)
                        throw;
                }
            }
        }
    
        public static Task<IDatabase> GetDatabaseAsync()
        {
            return BasicRetryAsync(() => Connection.GetDatabase());
        }
    
        public static Task<System.Net.EndPoint[]> GetEndPointsAsync()
        {
            return BasicRetryAsync(() => Connection.GetEndPoints());
        }
    
        public static Task<IServer> GetServerAsync(string host, int port)
        {
            return BasicRetryAsync(() => Connection.GetServer(host, port));
        }
    
  4. 解决方案资源管理器 中,展开“视图” > “共享”文件夹。 然后打开 _Layout.cshtml 文件。

    将:

    @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
    

    替换为:

    @Html.ActionLink("Azure Cache for Redis Test", "RedisCache", "Home", new { area = "" }, new { @class = "navbar-brand" })
    

添加新 RedisCache 视图的步骤

  1. 在“解决方案资源管理器”中,展开“Views”文件夹,并右键单击“Home”文件夹。 选择“添加” > “视图...”。

  2. 在“添加视图”对话框中,输入 RedisCache 作为视图名称。 然后选择“添加”。

  3. RedisCache.cshtml 文件中的代码替换为以下代码:

    @{
        ViewBag.Title = "Azure Cache for Redis Test";
    }
    
    <h2>@ViewBag.Title.</h2>
    <h3>@ViewBag.Message</h3>
    <br /><br />
    <table border="1" cellpadding="10">
        <tr>
            <th>Command</th>
            <th>Result</th>
        </tr>
        <tr>
            <td>@ViewBag.command1</td>
            <td><pre>@ViewBag.command1Result</pre></td>
        </tr>
        <tr>
            <td>@ViewBag.command2</td>
            <td><pre>@ViewBag.command2Result</pre></td>
        </tr>
        <tr>
            <td>@ViewBag.command3</td>
            <td><pre>@ViewBag.command3Result</pre></td>
        </tr>
        <tr>
            <td>@ViewBag.command4</td>
            <td><pre>@ViewBag.command4Result</pre></td>
        </tr>
        <tr>
            <td>@ViewBag.command5</td>
            <td><pre>@ViewBag.command5Result</pre></td>
        </tr>
    </table>
    

在本地运行应用

默认情况下,项目配置为在 IIS Express 本地托管应用,以进行测试和调试。

在本地运行应用的步骤

  1. 在 Visual Studio 中选择“调试” > “开始调试”,在本地生成并启动用于测试和调试的应用。

  2. 在浏览器中,选择导航栏上的“Azure Redis 缓存测试”。

  3. 在下面的示例中,Message 键以前有一个缓存值,该值是在门户中使用 Azure Redis 缓存控制台设置的。 应用更新了该缓存值。 应用还执行了 PINGCLIENT LIST 命令。

    对完成的本地项目进行简单测试

在 Azure 中发布和运行

在本地成功测试应用后,即可将应用部署到 Azure 并在云中运行它。

将应用发布到 Azure 的步骤

  1. 在 Visual Studio 中右键单击解决方案资源管理器中的项目节点, 然后选择“发布”。

    发布

  2. 依次选择“Microsoft Azure 应用服务”、“新建”、“发布”。

    发布到应用服务

  3. 在“创建应用服务”对话框中进行以下更改:

    设置 建议的值 说明
    应用名称 使用默认值。 应用名称是应用部署到 Azure 时对应的主机名。 如果需要让该名称保持唯一,可在其后添加一个时间戳后缀。
    订阅 选择自己的 Azure 订阅。 将对此订阅收取任何相关的托管费用。 如果有多个 Azure 订阅,请验证是否选择了所需的订阅。
    资源组 使用在其中创建了此缓存的资源组(例如,TestResourceGroup)。 该资源组用于将所有资源作为一个组管理。 以后想要删除此应用时,可以直接删除该组。
    应用服务计划 选择“新建”,然后创建名为 TestingPlan 的新应用服务计划。
    使用创建缓存时所用的相同 位置
    选择“免费”作为大小。
    应用服务计划为要运行的 Web 应用定义一组计算资源。

    “应用服务”对话框

  4. 配置应用服务托管设置以后,请选择“创建”。

  5. 监视 Visual Studio 中的“输出”窗口,了解发布状态。 发布应用后,系统会记录应用的 URL:

    发布输出

为缓存添加应用设置

发布新应用以后,请添加新应用设置。 此设置用于存储缓存连接信息。

添加应用设置的步骤

  1. 在 Azure 门户顶部的搜索栏中键入应用名称,查找刚创建的新应用。

    查找应用

  2. 为应用添加名为 CacheConnection 的新应用设置,用于连接到缓存。 使用在 CacheSecrets.config 文件中为 CacheConnection 配置的相同值。 该值包含缓存主机名和访问密钥。

    添加应用设置

在 Azure 中运行应用

在浏览器中,转到应用的 URL。 该 URL 显示在 Visual Studio 输出窗口的发布操作结果中。 此外,在 Azure 门户中,所创建应用的概览页上也提供了该 URL。

选择导航栏上的“Azure Redis 缓存测试”以测试缓存访问。

对完成的 Azure 项目进行简单测试

清理资源

如果想要继续学习下一篇教程,可以保留本快速入门中创建的资源,以便重复使用。

如果已完成快速入门示例应用程序,可以删除本快速入门中创建的 Azure 资源,以免产生费用。

重要

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

删除资源组的步骤

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

  2. 在“按名称筛选...”框中键入资源组的名称。 本文的说明使用了名为 TestResources 的资源组。 在资源组的结果列表中选择“...”,然后选择“删除资源组” 。

    删除

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

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

后续步骤

下一个教程会在一个更真实的场景中使用 Azure Redis 缓存来改善应用的性能。 请更新此应用程序,以便使用 ASP.NET 和数据库的缓存端模式来缓存排行榜结果。