65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Redis 在 Azure 上构建带缓存管理器的单页待办事项应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2015年4月11日

CPOL

6分钟阅读

viewsIcon

16584

本文介绍如何使用 ASP.NET Web API 服务创建一个单页网站,该服务通过缓存管理器存储数据。

引言

本文介绍如何使用 ASP.NET Web API 服务创建一个单页网站,该服务通过缓存管理器存储数据。
我将解释服务实现和缓存管理器(Cache Manager)的使用方法,并讨论如何将新创建的网站托管在 Azure 上以及如何配置 Azure 的 Redis 缓存。

为了实现该网站,我将使用一个现有的基于 AngularJS 的 示例,该示例来自 todomvc.com
当然,这个示例网站的这部分所有功劳都归功于他们。

您可以在 cachemanager-todo.azurewebsites.net 上查看示例网站的运行效果,或者在 Github 上 浏览代码

如果您不知道待办事项应用(todo app)是做什么的,请访问 todomvc.com。有许多不同实现的相同应用程序,它们看起来都和这个类似。

todomvc example

基本功能

通过这个简单的应用程序,用户可以添加新的待办事项、编辑现有待办事项、删除它们以及将它们设置为已完成状态。还有一个“删除所有已完成”的功能。

服务定义

这个单页应用程序将使用一个 Web API 服务来存储或删除待办事项,该服务必须提供以下方法:

  • Get - 检索所有现有的 todo
  • Get(id) - 按 id 检索单个 todo
  • Post(todo) - 创建一个新的 todo 项,分配一个新的 id 并返回它
  • Put(todo) - 更新 todo
  • Delete(id) - 按 id 删除单个 todo
  • Delete - 删除所有已完成的 todo

项目设置

我将使用 AP.NET Web API 实现该服务,所以让我们创建一个空的 Web API 项目并向其中添加示例代码。我们的解决方案看起来会是这样:

enter image description here

提示

不用担心,您可以直接从 Github 仓库 获取此示例的完整源代码。

我还安装了一些额外的 nuget 包:CacheManager 包、Unity 和 Json.Net。

模型

让我们向解决方案中添加 Todo 模型,该模型具有三个属性:IdTitleCompleted

using System;
using System.Linq;
using Newtonsoft.Json;

namespace Website.Models
{
    [Serializable]
    [JsonObject]
    public class Todo
    {
        [JsonProperty(PropertyName = "id")]
        public int Id { get; set; }

        [JsonProperty(PropertyName = "title")]
        public string Title { get; set; }

        [JsonProperty(PropertyName = "completed")]
        public bool Completed { get; set; }
    }
}

为了获得 JavaScript 通常的 camelCase 命名风格,我们定义了 JsonProperty 的名称。此外,我们必须将对象标记为 Serializable,否则像 Redis 这样的缓存句柄将无法存储 Todo 实体。

设置缓存管理器

我们的服务应该使用缓存管理器来存储和检索待办事项。为了使控制器能够访问缓存管理器实例,我将使用 Unity 作为 IoC 容器。当然,这可以通过多种方式完成,请使用您喜欢的任何 IoC 容器。

Global.asax 文件中,在应用程序初始化(Application_Start)期间,我们只需要创建 IUnityContainer 并注册缓存管理器实例。
为了让 Unity 在框架实例化控制器时每次都将缓存管理器实例注入到我们的控制器中,我们还必须告诉 Web API 框架使用 Unity 作为依赖项解析器。

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configure(WebApiConfig.Register);

        var container = new UnityContainer();
        GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);

        var cache = CacheFactory.Build("todos", settings =>
        {
            settings
                .WithSystemRuntimeCacheHandle("inprocess");
        });

        container.RegisterInstance(cache);
    }
}

在 API 控制器中,我将添加一个带有 Dependency 属性的属性。这将允许 Unity 为我们设置该属性。我们也可以使用基于构造函数的注入,但这需要编写更多代码……

[Dependency]
protected ICacheManager<object> todoCache { get; set; }

实现 REST 服务

让我们创建服务。我将让 MVC 自动生成一个完整的 CRUD Web API 控制器。
生成的代码将使用 string 作为类型,我们需要将其更改为我们的 Todo 模型。

我们如何存储这些项

我们知道我们需要检索所有待办事项并删除部分待办事项。我们可以将 todo 项存储为一个列表,但这通常效率不高,尤其是在考虑扩展性和性能时。

更好的解决方案是独立存储每个项目。
话虽如此,这个解决方案使得从缓存中检索所有 todo 项变得稍微困难一些。
解决这个问题的一种方法是在缓存中放入另一个键,该键存储所有可用的 id
这样,我们也有了一种生成新 id 的方法;如果我们知道所有现有的 id,我们就可以简单地创建一个新的……

让我们继续并实现这个解决方案。

我将添加一个简单的 private 属性来检索 id 列表。如果键不存在,我将向缓存中添加一个空数组。这是必要的,因为我们稍后需要调用 Update,如果没有任何缓存键可以更新,该方法将不起作用!

在这种情况下,我使用 Add,因为它只在值不存在时添加,以防止在分布式环境中出现问题。

   // key to store all available todos' keys.
   private const string KeysKey = "todo-sample-keys";

   // retrieves all todos' keys or adds an empty int array if the key is not set
   private List<int> AllKeys
   {
       get
       {
           var keys = todoCache.Get<int[]>(KeysKey);

           if (keys == null)
           {
               keys = new int[] { };
               todoCache.Add(KeysKey, keys);
           }

           return keys.ToList();
       }
   }

实现 Get 和 Get by Id

我们可以使用 AllKeys 属性并遍历它来返回 Todo 项列表。

   // GET: api/ToDo
   public IEnumerable<Todo> Get()
   {
       var keys = this.AllKeys;

       foreach (var key in keys)
       {
           yield return this.Get(key);
       }
   }

   // GET: api/ToDo/5
   public Todo Get(int id)
   {
       return todoCache.Get<Todo>(id);
   } 

实现 Put

更新现有项也非常简单,我们只需要使用 cache.Put

 // PUT: api/ToDo/5
  public void Put(int id, [FromBody]Todo value)
  {
      todoCache.Put(id, value);
  }

实现 Post

创建一个新项稍微复杂一些,因为我们将所有可用 ID 存储在单独的缓存键中,并且要创建新项,我们需要“生成”唯一 ID。

为了安全地做到这一点,即使有分布式缓存,我们也可以使用缓存管理器的 Update 方法。
如果我们使用 PutAdd 而不是 Update,我们将遇到并发问题,因为多个客户端可能会为新项使用相同的 ID。

   // POST: api/ToDo
   public Todo Post([FromBody]Todo value)
   {
       int newId = -1;
       todoCache.Update(KeysKey, obj =>
       {
           var keys = (obj as int[]).ToList();
           newId = !keys.Any() ? 1 : keys.Max() + 1;
           keys.Add(newId);
           return keys.ToArray();
       });

       value.Id = newId;
       todoCache.Add(newId, value);
       return value;
   }

正如在 关于 Update 方法的文章 中讨论的那样,您传递的 Action 可能会根据版本冲突被调用多次。但我们总是会收到“最新”的值,在本例中是 obj
如果在更新过程中发生版本冲突,我们的更改将被丢弃,并且 Action 会再次运行。这意味着我们不会多次添加新 ID,只有 Max 值在每次迭代中会有所不同。

最后,我们可以设置我们 TodoId 属性,然后将其 Add 到我们的缓存中并返回它。

实现 Delete

要删除所有已完成的 Todo 项,我们将不得不遍历所有现有的 Todo 项,检查 Completed 状态,然后按 ID 调用 Delete

   // DELETE ALL completed: api/ToDo
   public void Delete()
   {
       var keys = this.AllKeys;

       foreach (var key in keys)
       {
           var item = this.Get(key);
           if (item != null && item.Completed)
           {
               this.Delete(item.Id);
           }
       }
   }

实现 Delete by Id

Id 删除与 Post 类似,我们也必须更新存储所有 Todo id 的键。这里也是一样,我们将使用 Update 方法来确保我们处理的是数组的正确版本。

   // DELETE: api/ToDo/5
   public void Delete(int id)
   {
       todoCache.Remove(id);
       todoCache.Update(KeysKey, obj =>
       {
           var keys = (obj as int[]).ToList();
           keys.Remove(id);
           return keys.ToArray();
       });
   }

使用缓存管理器进行扩展

您可能已经注意到,在本文的“设置缓存管理器”部分,我只指定了一个进程内缓存句柄。这意味着我们的 Todo 项仅存储在内存中,并在应用程序重新启动时被清除。
为了持久化我们的 Todo 项,我们可以使用一些分布式缓存,如 Redis 或 couchbase。

使用缓存管理器,更改起来非常容易。只需几行配置,我们的 API 控制器就无需进行任何更改!

var cache = CacheFactory.Build("todos", settings =>
{
    settings
        .WithSystemRuntimeCacheHandle("inprocess")
            .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10))
        .And
        .WithRedisConfiguration("redisLocal", "localhost:6379,ssl=false")
        .WithRedisCacheHandle("redisLocal", true);
});

配置现在有两个缓存句柄!一个“第一级”进程内缓存,以及一个“第二级”分布式缓存。这样,我们可以减少到 Redis 服务器的流量,这将使我们的应用程序速度大大提高。

托管

如果我们现在将此网站托管在 Azure 上,例如,我们可以稍微更改配置并使用连接字符串而不是硬编码的连接参数。

我们还可以使用缓存管理器的回写(back plate)功能,以使配置的第一级进程内缓存保持同步。

var cache = CacheFactory.Build("todos", settings =>
{
    settings
        .WithSystemRuntimeCacheHandle("inprocess")
            .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10))
        .And
        .WithRedisBackPlate("redis.azure.us")
        .WithRedisCacheHandle("redis.azure.us", true);
});

您可以通过 web.configConnectionStrings 部分添加连接字符串,或者通过 Azure 管理门户添加(出于安全原因,这是首选方式……)。
在 Azure 管理门户中,单击您的 Web 应用,选择“所有设置”(All Settings),然后选择“应用程序设置”(Application Settings),向下滚动到“连接字符串”(Connection Strings)部分,然后将连接字符串添加到列表中。
它看起来应该与此类似:

Azure portal

连接字符串本身必须至少包含主机、SSL 设置为 true 以及密码设置为门户提供的 Redis 访问密钥之一。

hostName:6380,ssl=true,password=ThEaCcessKey

就这样,您可以在 cachemanager-todo.azurewebsites.net 上看到示例网站的运行效果,并且可以在 Github 上 浏览代码

此外,在我 的网站 上还有更多关于缓存管理器(Cache Manager)的文章。

© . All rights reserved.