使用 Redis 在 Azure 上构建带缓存管理器的单页待办事项应用
本文介绍如何使用 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。有许多不同实现的相同应用程序,它们看起来都和这个类似。
基本功能
通过这个简单的应用程序,用户可以添加新的待办事项、编辑现有待办事项、删除它们以及将它们设置为已完成状态。还有一个“删除所有已完成”的功能。
服务定义
这个单页应用程序将使用一个 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 项目并向其中添加示例代码。我们的解决方案看起来会是这样:
提示
不用担心,您可以直接从 Github 仓库 获取此示例的完整源代码。
我还安装了一些额外的 nuget 包:CacheManager
包、Unity 和 Json.Net。
模型
让我们向解决方案中添加 Todo
模型,该模型具有三个属性:Id
、Title
和 Completed
。
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
方法。
如果我们使用 Put
或 Add
而不是 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
值在每次迭代中会有所不同。
最后,我们可以设置我们 Todo
的 Id
属性,然后将其 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.config 的 ConnectionStrings
部分添加连接字符串,或者通过 Azure 管理门户添加(出于安全原因,这是首选方式……)。
在 Azure 管理门户中,单击您的 Web 应用,选择“所有设置”(All Settings),然后选择“应用程序设置”(Application Settings),向下滚动到“连接字符串”(Connection Strings)部分,然后将连接字符串添加到列表中。
它看起来应该与此类似:
连接字符串本身必须至少包含主机、SSL 设置为 true
以及密码设置为门户提供的 Redis 访问密钥之一。
hostName:6380,ssl=true,password=ThEaCcessKey
就这样,您可以在 cachemanager-todo.azurewebsites.net 上看到示例网站的运行效果,并且可以在 Github 上 浏览代码。
此外,在我 的网站 上还有更多关于缓存管理器(Cache Manager)的文章。