为了便于理解,我特意为大家准备了一个示例。整个示例由三部分组成:一个页面,一个JS文件,服务端代码。先来看页面代码:
<p> 为了简单,示例页面只处理一条记录,且将记录的RowGuid直接显示出来。<br />
实际场景中,这个RowGuid应该可以从一个表格的【当前选择行】中获取到。
</p>
<p> 当前选择行的 RowGuid = <span id="spanRowGuid"><%= Guid.NewGuid().ToString() %></span><br />
当前选择行的 Sequence= <span id="spanSequence">0</span>
</p>
<p><input type="button" id="btnMoveUp" value="上移" />
<input type="button" id="btnMoveDown" value="下移" />
</p>
</body>
页面的显示效果如下:
处理页面中二个按钮的JS代码如下:
var g_reason = null;
$(function(){
$("#btnMoveUp").click( function() { MoveRec(-1); } );
$("#btnMoveDown").click( function() { MoveRec(1); } );
});
function MoveRec(direction){
if( ~~($("#spanSequence").text()) + direction < 0 ){
alert("已经不能上移了。");
return;
}
if( g_reason == null ){
g_reason = prompt("请输入调整记录顺序的原因:", "由于什么什么原因,我要调整...");
if( g_reason == null )
return;
}
$.ajax({
url: "/AjaxDelaySendMail/MoveRec.fish",
data: { RowGuid: $("#spanRowGuid").text(),
Direction: direction,
Reason: g_reason
},
type: "POST", dataType: "text",
success: function(responseText){
$("#spanSequence").text(responseText);
}
});
}
说明:在服务端,我使用了我在【用Asp.net写自己的服务框架】那篇博客中提供的服务框架, 服务端的全部代码是这个样子的:(注意代码中的注释)
/// 移动记录的相关信息。
/// </summary>
public class MoveRecInfo
{
public string RowGuid;
public int Direction;
public string Reason;
}
[MyService]
public class AjaxDelaySendMail
{
[MyServiceMethod]
public int MoveRec(MoveRecInfo info)
{
// 这里就不验证从客户端传入的参数了。实际开发中这个是必须的。
// 先来调整记录的顺序,示例程序没有数据库,就用Cache来代替。
int sequence = 0;
int.TryParse(HttpRuntime.Cache[info.RowGuid] as string, out sequence);
// 简单地示例一下调整顺序。
sequence += info.Direction;
HttpRuntime.Cache[info.RowGuid] = sequence.ToString();
string key = info.RowGuid +"_DelaySendMail";
// 这里我不直接发邮件,而是把这个信息放入Cache中,并设置2秒的滑过过期时间,并指定移除通知委托
// 将操作信息放在缓存,并且以覆盖形式放入,这样便可以实现保存最后状态。
// 注意:这里我用Insert方法。
HttpRuntime.Cache.Insert(key, info, null, Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(2.0), CacheItemPriority.NotRemovable, MoveRecInfoRemovedCallback);
return sequence;
}
private void MoveRecInfoRemovedCallback(string key, object value, CacheItemRemovedReason reason)
{
if( reason == CacheItemRemovedReason.Removed )
return; // 忽略后续调用HttpRuntime.Cache.Insert()所触发的操作
// 能运行到这里,就表示是肯定是缓存过期了。
// 换句话说就是:用户2分钟再也没操作过了。
// 从参数value取回操作信息
MoveRecInfo info = (MoveRecInfo)value;
// 这里可以对info做其它的处理。
// 最后发一次邮件。整个延迟发邮件的过程就处理完了。
MailSender.SendMail(info);
}
}
为了能让JavaScript能直接调用C#中的方法,还需要在web.config中加入如下配置:
<add path="*.fish" verb="*" validate="false" type="MySimpleServiceFramework.AjaxServiceHandler"/>
</httpHandlers>
好了,示例代码就是这些。如果您有兴趣,可以在本文的结尾处下载这些示例代码,自己亲自感受一下利用Cache实现的【延迟处理】的功能。
其实这种【延迟处理】的功能是很有用的,比如还有一种适用场景:有些数据记录可能需要频繁更新,如果每次更新都去写数据库,肯定会对数据库造成一定的压力, 但由于这些数据也不是特别重要,因此,我们可以利用这种【延迟处理】来将写数据库的时机进行合并处理, 最终我们可以实现:将多次的写入变成一次或者少量的写入操作,我称这样效果为:延迟合并写入
这里我就对数据库的延迟合并写入提供一个思路:将需要写入的数据记录放入Cache,调用Insert方法并提供slidingExpiration和onRemoveCallback参数, 然后在CacheItemRemovedCallback回调委托中,模仿我前面的示例代码,将多次变成一次。不过,这样可能会有一个问题:如果数据是一直在修改,那么就一直不会写入数据库。 最后如果网站重启了,数据可能会丢失。如果担心这个问题,那么,可以在回调委托中,遇到CacheItemRemovedReason.Removed时,使用计数累加的方式,当到达一定数量后, 再写入数据库。比如:遇到10次CacheItemRemovedReason.Removed我就写一次数据库,这样就会将原来需要写10次的数据库操作变成一次了。 当然了,如果是其它移除原因,写数据库总是必要的。注意:对于金额这类敏感的数据,绝对不要使用这种方法。
再补充二点:
1. 当CacheItemRemovedCallback回调委托被调用时,缓存项已经不在Cache中了。
2. 在CacheItemRemovedCallback回调委托中,我们还可以将缓存项重新放入缓存。
有没有想过:这种设计可以构成一个循环?如果再结合参数slidingExpiration便可实现一个定时器的效果。
关于缓存的失效时间,我要再提醒一点:通过absoluteExpiration, slidingExpiration参数所传入的时间,当缓存时间生效时,缓存对象并不会立即移除, ASP.NET Cache大约以20秒的频率去检查这些已过时的缓存项。
巧用缓存项的移除通知 实现【自动加载配置文件】
在本文的前部分的【文件依赖】小节中,有一个示例演示了:当配置文件更新后,页面可以显示最新的修改结果。 在那个示例中,为了简单,我直接将配置参数放在Cache中,每次使用时再从Cache中获取。 如果配置参数较多,这种做法或许也会影响性能,毕竟配置参数并不会经常修改,如果能直接访问一个静态变量就能获取到,应该会更快。 通常,我们可能会这样做: 『点击此处展开』
但是,这种做法有一缺点就是:不能在配置文件更新后,自动加载最新的配置结果。
为了解决这个问题,我们可以使用Cache提供的文件依赖以及移除通知功能。 前面的示例演示了移除后通知功能,这里我再演示一下移除前通知功能。
说明:事实上,完成这个功能,可以仍然使用移除后通知,只是移除前通知我还没有演示,然而,这里使用移除前通知并没有显示它的独有的功能。
下面的代码演示了在配置文件修改后,自动更新运行参数的实现方式:(注意代码中的注释) 『点击此处展开』
改动很小,只是LoadRunOptions方法做了修改了而已,但是效果却很酷。
还记得我在上篇博客【在.net中读写config文件的各种方法】的结尾处留下来的问题吗? 这个示例就是我的解决方案。
文件监视技术的选择
对于文件监视,我想有人或许会想到FileSystemWatcher。正好我就来说说关于【文件监视技术】的选择问题。
说明,本文所有结论均为我个人的观点,仅供参考。
这个组件,早在做WinForm开发时就用过了,对它也是印象比较深的。
它有一个包装不好的地方是:事件会重复发出。比如:一次文件的保存操作,它却引发了二次事件。
什么,你不信? 正好,我还准备了一个示例程序。
说明:图片中显示了发生过二次事件,但我只是在修改了文件后,做了一次保存操作而已。 本文的结尾处有我的示例程序,您可以自己去试一下。这里为了方便,还是贴出相关代码:
{
this.fileSystemWatcher1.Path = Environment.CurrentDirectory;
this.fileSystemWatcher1.Filter = "RunOptions.xml";
this.fileSystemWatcher1.NotifyFilter = System.IO.NotifyFilters.LastWrite;
this.fileSystemWatcher1.EnableRaisingEvents = true;
}
private void fileSystemWatcher1_Changed(object sender, System.IO.FileSystemEventArgs e)
{
string message = string.Format("{0} {1}.", e.Name, e.ChangeType);
this.listBox1.Items.Add(message);
}
对于这个类的使用,只想说一点:会引发的事件很多,因此一定要注意过滤。以下引用MSDN的一段说明:
Windows 操作系统在 FileSystemWatcher 创建的缓冲区中通知组件文件发生更改。如果短时间内有很多更改,则缓冲区可能会溢出。这将导致组件失去对目录更改的跟踪,并且它将只提供一般性通知。使用 InternalBufferSize 属性来增加缓冲区大小的开销较大,因为它来自无法换出到磁盘的非页面内存,所以应确保缓冲区大小适中(尽量小,但也要有足够大小以便不会丢失任何文件更改事件)。若要避免缓冲区溢出,请使用 NotifyFilter 和 IncludeSubdirectories 属性,以便可以筛选掉不想要的更改通知。
幸运的是,ASP.NET Cache并没有使用这个组件,我们不用担心文件依赖发引用的重复操作问题。 它直接依赖于webengine.dll所提供的API,因此,建议在ASP.NET应用程序中,优先使用Cache所提供的文件依赖功能。
各种缓存方案的共存
ASP.NET Cache是一种缓存技术,然而,我们在ASP.NET程序中还可以使用其它的缓存技术, 这些不同的缓存也各有各自的长处。由于ASP.NET Cache不能提供对外访问能力,因此,它不可能取代以memcached为代表的分布式缓存技术, 但它由于是不需要跨进程访问,效率也比分布式缓存的速度更快。如果将ASP.NET Cache设计成【一级缓存】, 分布式缓存设计成【二级缓存】,就像CPU的缓存那样,那么将能同时利用二者的所有的优点,实现更完美的功能以及速度。
其实缓存是没有一个明确定义的技术,一个static变量也是一个缓存,一个static集合就是一个缓存容器了。 这种缓存与ASP.NET Cache相比起来,显然static变量的访问速度会更快,如果static集合不是设计得很差的话, 并发的冲突也可能会比ASP.NET Cache小,也正是因为这一点,static集合也有着广泛的使用。 然而,ASP.NET Cache的一些高级功能,如:过期时间,缓存依赖(包含文件依赖),移除通知,也是static集合不具备的。 因此,合理地同时使用它们,会让程序有着最好的性能,也同时拥有更强大的功能。