最近公开预览的 Azure Redis Cache 很容易就能整合进您的 Azure 网站应用程序中,这里我将 MVC Movie 范例程序整合 Azure Redis Cache 然后部署到 Azure 网站服务(Websites)上,大约只花了 15 分钟左右。
在导入快取(cache)之后,程序的速度比起纯用数据库来说快了将近 100 倍,因为经常被存取的数据就可以直接从快取中取得,而不必再到数据库中捞数据,如此一来你也可以降低数据库的存取次数,让数据查询的动作变得更迅速。
接下来我会说明我是如何把 Azure Redis Cache 整合进我的 ASP.NET MVC Movie 范例之中。
在预览中新的 Azure 管理界面,我们可以如图所示建立一个新的 Redis Cache 服务。
建立 Cache 服务大概会花个 15 分钟左右,不过同一时间我不会在这里等着,而是去修改我的程序。如果你需要完整的参考手册,可以参考 How to Use Azure Redis Cache 这篇文章,不过要记得的是,Redis Cache 服务必须要跟你的网站服务是在同样的数据中心,不同的数据中心可能速度会相差到 25 倍左右,建立的步骤可以参考 Create a Redis Cache 这一页,你可以下载原始的 MvcMovie 范例程来改,或是直接下载我修改后的范例,但是要记得修改 cache 服务的 URL 及认证数据才会正常运作。
当 Cache 服务建立完成后,记下 Cache 名称像是 <yourName>.redis.cache.windows.net 以及密码(点击 keys 按钮就可以取得名称及密码)。
在你的 MVC 项目中,使用 NuGet 套件管理工具安装 StackExchange.Redis 这个套件,如果您下载的是修改后的范例,那项目中已经安装并参考了这个套件。
打开套件管理控制台(package manager console)中输入 Update-Database 的指令。
在相关的 controller 中加入连结快取服务的程序代码:
public class MoviesController : Controller
{
private MovieDBContext db = newMovieDBContext();
private static ConnectionMultiplexer connection;
private static ConnectionMultiplexer Connection
{
get
{
if (connection == null || !connection.IsConnected)
{
connection = ConnectionMultiplexer.Connect(
".redis.cache.windows.net,ssl=true," +
"password=");
}
return connection;
}
}
注意:一般来说都不建议你把账号密码的数据直接写在程序代码中,这里只是为了方便了解程序代码才这么做。请参考 Windows Azure Web Sites: How Application Strings and Connection Strings Work 这篇文章介绍的方法来处理账号密码这些敏感数据。
在上述的程序代码中,我已经将连结快取服务的部份用静态成员的方式储存,所以你不必在每一次 request 都重新建立一个快取服务的连结,你只需要在使用时检查连结是否还在,若已经失去连结再重新建立连结就好。
新增一个类别,并包含这个 SampleStackExchangeRedisExtension 类别:
public static class SampleStackExchangeRedisExtensions
{
public static T Get<T>(this IDatabase cache, string key)
{
return Deserialize<T>(cache.StringGet(key));
}
public static object Get(this IDatabase cache, string key)
{
return Deserialize<object>(cache.StringGet(key));
}
public static void Set(this IDatabase cache, string key, object value)
{
cache.StringSet(key, Serialize(value));
}
static byte[] Serialize(object o)
{
if (o == null)
{
return null;
}
BinaryFormatter binaryFormatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, o);
byte[] objectDataAsStream = memoryStream.ToArray();
return objectDataAsStream;
}
}
static T Deserialize<T>(byte[] stream)
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
if (stream == null)
return default(T);
using (MemoryStream memoryStream = new MemoryStream(stream))
{
T result = (T)binaryFormatter.Deserialize(memoryStream);
return result;
}
}
}
SampleStackExchangeRedisExtensions 类别可以让你很轻易就将任何可串行化(serializable)的类别做快取。你可以在你的模型(model)上加上 [Serializable] 属性。
[Serializable]
public class Movie
然后将所有 Movie movie = db.Movies.Find(id); 的部份都修改成:
//Movie movie = db.Movies.Find(id);
Movie movie = getMovie((int)id);
在 POST 呼叫的 Edit 及 Delete 方法中,记得要清除快取。
ClearMovieCache(movie.ID);
在 controller 中加入下面这段程序代码,其中 getMovie 是一个很标准的快取操作:
Movie getMovie(int id)
{
Stopwatch sw = Stopwatch.StartNew();
IDatabase cache = Connection.GetDatabase();
Movie m = (Movie)cache.Get(id.ToString());
if (m == null)
{
Movie movie = db.Movies.Find(id);
cache.Set(id.ToString(), movie);
StopWatchMiss(sw);
return movie;
}
StopWatchHit(sw);
return m;
}
private void ClearMovieCache(int p)
{
IDatabase cache = connection.GetDatabase();
if (cache.KeyExists(p.ToString()))
cache.KeyDelete(p.ToString());
}
void StopWatchEnd(Stopwatch sw, string msg)
{
sw.Stop();
double ms = sw.ElapsedTicks / (Stopwatch.Frequency / (1000.0));
ViewBag.cacheMsg = msg + ms.ToString() +
” PID: ” + Process.GetCurrentProcess().Id.ToString();
}
void StopWatchMiss(Stopwatch sw)
{
StopWatchEnd(sw, “Miss – MS:”);
}
void StopWatchHit(Stopwatch sw)
{
StopWatchEnd(sw, “Hit – MS:”);
}
另外,在 Views\Shared\_Layout.cshtml 档案中加入 ViewBag.cacheMsg 的程序代码,这是为了在页面上显示快取的信息。
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<h2>@ViewBag.cacheMsg</h2>
</footer>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>
现在你可以在开发环境中测试快取的效果了,不过如果你的数据库很小,而快取服务又是在云端,效果可能不会太明显,部署到 Azure 上应该就可以感受到明显的差异。
在管理接口中监控快取服务
在管理接口中,你可以看到快取服务的 hit/miss 的统计数据:
你可以加上其它的相关信息来监控,像是自定义时间范围、被清除的键值、过期的键值、使用的 CPU 或内存等。
当然,你也可以加入警告通知(Add Alert)来帮助你监控快取服务的使用状况,像是下图我就加入了一个警告通知,在 15 分钟内,清除的键值过多时,可能要使用更大的快取。
从 Visual Studio 将网站部署到 Azure 是十分容易的,只要在 Web 项目上右键单击,选择发行就可以了,再次提醒,网站服务跟快取服务一定要在同一个数据中心,否则网络传输的延迟会拖垮快取的效能。而在发行时别忘了勾选 Execute Code First Migrations。
部署完成后,你就可以试试看有没有快取的效能是不是有明显的差异。
压力测试快取服务
预设的快取操作时间是 1000ms (1秒),你可以试着用下面这段程序代码,将 time out (强制清除快取)改成更长或更短的时间,测试你的快取服务是否正常。当 #define NotTestingTimeOut 这段程序代码被批注掉时,timeout 便会被设定为 150ms,让快取在很短的时间被清掉。
#else
#region StressTest
private static ConnectionMultiplexer Connection
{
get
{
if (connection != null && connection.IsConnected)
{
return connection;
}
var config = new ConfigurationOptions();
config.EndPoints.Add(Keys.URL);
config.Password = Keys.passwd;
config.Ssl = true;
config.SyncTimeout = 150;
connection = ConnectionMultiplexer.Connect(config);
return connection;
}
}
#endregion
#endif
在压力测试时,最好将 session 快取关闭,简单的作法是到 web.config 档案中把整个应用程序的 session 快取关闭。
<sessionState mode="Off" />
或是在你的 controller 中使用 [SessionState(SessionStateBehavior.Disabled)] 来做。以下这个更新后的 getMovie 方法可以运作得更稳定,因为当 time out 例外发生时会重试 3 次。
Movie getMovie(int id, int retryAttempts = 0)
{
IDatabase cache = Connection.GetDatabase();
if (retryAttempts > 3)
{
string error = "getMovie timeout with " + retryAttempts.ToString()
+ " retry attempts. Movie ID = " + id.ToString();
Logger(error);
ViewBag.cacheMsg = error + " Fetch from DB";
// Cache unavailable, get data from DB
return db.Movies.Find(id);
}
Stopwatch sw = Stopwatch.StartNew();
Movie m;
try
{
m = (Movie)cache.Get(id.ToString());
}
catch (TimeoutException tx)
{
Logger("getMovie fail, ID = " + id.ToString(), tx);
return getMovie(id, ++retryAttempts);
}
if (m == null)
{
Movie movie = db.Movies.Find(id);
cache.Set(id.ToString(), movie);
StopWatchMiss(sw);
return movie;
}
StopWatchHit(sw);
return m;
}
这个范例程序中还有很多方法可以测试快取服务。
像 WriteCache 或 ReadCache 方法会预设写入或读取 1,000 笔数据,你可以在 URL 后加上 "/n" 让它们变成读写 n * 1000 笔的数据,像上图的例子 http://<your site>.azurewebsites.net/Movies/ReadCache/3 就会读取 3,000 笔快取的数据。
在这个 150ms timeout 的环境下,我的程序就会很容易碰到 timeout 的状况而去存取数据库,这是因为我的程序有正确处理这个 timeout 的例外才能顺利去读取数据库。所以建议您上线的应用程序也要能处理好这个例外,因为根据云端平台的服务水平,如果您选择的是基本方案,那一个月中可能会有几分钟无法存取(比如正在更新 VM),除非选择了标准方案,并且建立好 master-slave 的架构备援,不过还是建议您在程序代码中预先处理这个可能发生的例外状况。
Azure Redis Cache (Preview) ASP.NET Session State Provider
ASP.NET 默认的 In-memory Session State Provider 无法同时被多个网站实体使用,而 SQL Server session state 虽然可以同时让不同的网站使用相同的 session state,但这会受限于数据库查询的延迟时间,进而影响效能。而 Redis session state cache provider 则是另一个选择,如果你的网站只会用到不是很大的 session state,则可以利用 Redis Cache 来快取这些 session state data。
您可以参考这篇文章,在你的网站应用程序中加入 RedisSessionStateProvider,然后修改 Web.config 档案来设定 Redis Cache 服务:
<system.web>
<customErrors mode="Off" />
<!--<sessionState mode="Off" />-->
<authentication mode="None" />
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
<sessionState mode="Custom" customProvider="RedisSessionProvider">
<add name="RedisSessionProvider"
type="Microsoft.Web.Redis.RedisSessionStateProvider"
port="6380"
host="movie2.redis.cache.windows.net"
accessKey="m7PNV60CrvKpLqMUxosC3dSe6kx9nQ6jP5del8TmADk="
ssl="true" />
<!--<add name="MySessionStateStore" type="Microsoft.Web.Redis.RedisSessionStateProvider" host="127.0.0.1" accessKey="" ssl="false" />-->
</providers>
</sessionState>
</system.web>
<system.webServer>
如此一来你就可以在你的网站应用程序中使用 Redis Cache 来处理 session state。在范例程序中也提供了测试的方法,你可以透过 http://<your site>.azurewebsites.net/SessionTest/WriteSession/Hello_joe 来存取,如此便会将 "Hello_joe" 写入 session state,你可以试着增加网站服务实体,看看这个 session state 是否会在多个实体间共享。