csharp - 云代码空间
——
我看过一些ASP.NET的书,也看过一些人写的关于Cache方面的文章,基本上,要么是一带而过,要么只是举个毫无实际意义的示例。 可惜啊,这么强大的特性,我很少见到有人把它用起来。
今天,我就举个有实际意义的示例,再现Cache的强大功能!
我有这样一个页面,可以让用户调整(上下移动)某个项目分支记录的上线顺序:
当用户需要调整某条记录的位置时,页面会弹出一个对话框,要求输入一个调整原因,并会发邮件通知所有相关人员。
由于界面的限制,一次操作(点击上下键头)只是将一条记录移动一个位置,当要对某条记录执行跨越多行移动时,必须进行多次移动。 考虑到操作的方便性以及不受重复邮件的影响,程序需要实现这样一个需求: 页面只要求输入一次原因便可以对一条记录执行多次移动操作,并且不要多次发重复邮件,而且要求将最后的移动结果在邮件中发出来。
这个需求很合理,毕竟谁都希望操作简单。
那么如何实现这个需求呢?这里要从二个方面来实现,首先,在页面上我们应该要完成这个功能,对一条记录只弹一次对话框。 由于页面与服务端的交互全部采用Ajax方式进行(不刷新),状态可以采用JS变量来维持,所以这个功能在页面中是很容易实现。 再来看一下服务端,由于服务端并没有任何状态,当然也可以由页面把它的状态传给服务端,但是,哪次操作是最后一次呢? 显然,这是无法知道的,最后只能修改需求,如果用户在2分钟之内不再操作某条记录时,便将最近一次操作视为最后一次操作。
基于新的需求,程序必须记录用户的最近一次操作,以便在2分钟不操作后,发出一次邮件,但要包含第一次输入的原因, 还应包含最后的修改结果哦。
该怎么实现这个需求呢? 我立即就想到了ASP.NET Cache,因为我了解它,知道它能帮我完成这个功能。下面我来说说在服务端是如何实现的。
整个实现的思路是:
1. 客户端页面还是每次将记录的RowGuid, 调整方向,调整原因,这三个参数发到服务端。
2. 服务端在处理完顺序调整操作后,将要发送的邮件信息Insert到Cache中,同时提供slidingExpiration和onRemoveCallback参数。
3. 在CacheItemRemovedCallback回调委托中,忽略CacheItemRemovedReason.Removed的通知,如果是其它的通知,则发邮件。
为了便于理解,我特意为大家准备了一个示例。整个示例由三部分组成:一个页面,一个JS文件,服务端代码。先来看页面代码:
<body> <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> /// 移动记录的相关信息。 /// </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中加入如下配置:
<httpHandlers> <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秒的频率去检查这些已过时的缓存项。