asp.net 服务器控件里的asp.net项目,一堆的rem文件是什么

原文地址:
PHP是一种用于创建动态Web应用程序最流行的开发语言。一些最有影响力和非常流行的开源CMS /出版平台,如,Joomla和Drupal的都是用开发的。 PHP有很多有点,它快速,稳定,安全,易于使用并且开放源码。熟悉PHP的Web开发人员要想有一个良好的开发效率,一些工具箱是必不可少的。
在下面的这篇文章中,我整理了一份有用的PHP组件,工具和教程,有了它们,PHP开发人员的生活应该会轻松许多。
PHP 集成环境
PHP 绘图工具
PHP 教程、技巧和有用的组件
详情请阅读:
由于项目需要,笔者最近需要实现Web客户端之间的消息的即时推送功能。
功能描述如下:
假设A,B,C用户登陆,内存记录下已登录的用户的信息,这时A在所在的客户端(SendInfo.aspx)页面向B发消息,则在B所在客户端页面(SendInfo.aspx)将弹出消息框。
关键点有两个:
1.保证客户端和服务端的连接
2.保证服务端能够向客户端广播消息
笔者是第一次做这样的实现,所以Google了一些资料,了解到可使用Comet,ajax轮询,WebSocket等技术实现,由于时间关系,发现有些技术不是很容易理解,这里做了一个简单Demo.希望能够达到抛砖引玉的作用,与大家分享,共同提高。
笔者做了两个框架下的实现, Web Form和 MVC 下的尝试。
ASP.NET Web Form版:
项目组织结构
AsyncHandler.cs
using S using System.Collections.G using System.W using System.T namespace CometSample { public class WebIMAsyncHandler : IHttpAsyncHandler { #region IHttpAsyncHandler 成员
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { string _UID = context.Request.Params["uid"]; WebIMClientAsyncResult _AsyncResult = new WebIMClientAsyncResult(context, cb, extraData); string _Content = context.Request.Params["content"]; string _Action = context.Request.Params["action"]; if (_Action == "login") { _UID = context.Request.Params["uid"]; _AsyncResult.LoginID = _UID; WebIMMessageHandler.Instance().Login(_UID, _AsyncResult); } else if (_Action == "logout") { _AsyncResult.LoginID = _UID; WebIMMessageHandler.Instance().Logout(_UID, _AsyncResult); } else if (_Action == "connect") { _AsyncResult.LoginID = _UID; WebIMMessageHandler.Instance().Connect(_AsyncResult); } else if (_Action == "getuserlist") { _AsyncResult.LoginID = _UID; WebIMMessageHandler.Instance().GetUserList(_AsyncResult); } //增加消息发送
else if (_Action == "sendmsg") { _AsyncResult.LoginID = _UID; //WebIMMessageHandler.Instance().GetUserList(_AsyncResult); //调用
WebIMMessageHandler.Instance().AddMessage(_Content, _AsyncResult); } //调用 //WebIMMessageHandler.Instance().AddMessage(_Content, _AsyncResult);
return _AsyncR } public void EndProcessRequest(IAsyncResult result) { } #endregion
#region IHttpHandler 成员
public bool IsReusable { get { return false; ; } } public void ProcessRequest(HttpContext context) { throw new NotImplementedException(); } #endregion } public class WebIMClientAsyncResult : IAsyncResult { bool m_IsCompleted = false; private HttpContext m_C private AsyncCallback m_C private object m_ExtraD private string m_C private string m_LoginID = string.E public WebIMClientAsyncResult(HttpContext p_Context, AsyncCallback p_Callback, object p_ExtraData) { this.m_Context = p_C this.m_Callback = p_C this.m_ExtraData = p_ExtraD } /// &summary&
/// 用户编号 /// &/summary&
public string LoginID { get { return m_LoginID; } set { m_LoginID = } } /// &summary&
/// 发送消息的内容,暂时未使用到 /// &/summary&
public string Content { get { return m_C } set { m_Content = } } #region IAsyncResult 成员
public object AsyncState { get { return null; } } public WaitHandle AsyncWaitHandle { get { return null; } } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get { return m_IsC } } #endregion
/// &summary&
/// 向客户端响应消息 /// &/summary&
/// &param name="data"&&/param&
public void Send(object data) { try { m_Context.Response.Write(this.Content); if (m_Callback != null) { m_Callback(this); } } catch { } finally { m_IsCompleted = true; } } } }
MessageHandler.cs
using S using System.C using System.Collections.G using System.W using System.T namespace CometSample { public class WebIMMessageHandler { private static readonly WebIMMessageHandler m_Instance = new WebIMMessageHandler(); //记录所有请求的客户端
List&WebIMClientAsyncResult& m_Clients = new List&WebIMClientAsyncResult&(); //Dictionary&string,WebIMClientAsyncResult& m_Clients=new Dictionary&string,WebIMClientAsyncResult&();
string m_Users = string.E public WebIMMessageHandler() { } public static WebIMMessageHandler Instance() { return m_I } /// &summary&
/// 登录 /// &/summary&
/// &param name="p_LoginID"&&/param&
/// &param name="p_ClientAsyncResult"&&/param&
public void Login(string p_LoginID, WebIMClientAsyncResult p_ClientAsyncResult) { bool _Logined = false; foreach (WebIMClientAsyncResult _Item in m_Clients) { if (_Item.LoginID == p_LoginID) { p_ClientAsyncResult.Content = "你已登录"; _Logined = true; break; } } if (!_Logined) { //m_Clients.Add(p_ClientAsyncResult);
p_ClientAsyncResult.Content = "OK"; } p_ClientAsyncResult.Send(null); } private string GetUsers() { /* string _Users = string.E foreach (WebIMClientAsyncResult _Item in m_Clients) { _Users += _Item.LoginID + ","; } return _U */
var sbUsers = new StringBuilder(); sbUsers.Append("Users:"); foreach (WebIMClientAsyncResult _Item in m_Clients) { sbUsers.Append(_Item.LoginID); sbUsers.Append(","); } return sbUsers.ToString(); } public void Logout(string p_LoginID, WebIMClientAsyncResult p_ClientAsyncResult) { foreach (WebIMClientAsyncResult _Item in m_Clients) { if (_Item.LoginID == p_LoginID) { m_Clients.Remove(_Item); break; } } p_ClientAsyncResult.Content = "退出成功"; p_ClientAsyncResult.Send(null); //UpdateUserList();
string _Users = GetUsers(); foreach (WebIMClientAsyncResult _Item in m_Clients) { _Item.Content = _U _Item.Send(null); } m_Clients.Clear(); } public void GetUserList(WebIMClientAsyncResult p_ClientAsyncResult) { Connect(p_ClientAsyncResult); string _Users = GetUsers(); foreach (WebIMClientAsyncResult _Item in m_Clients) { _Item.Content = _U _Item.Send(null); } m_Clients.Clear(); } public void Connect(WebIMClientAsyncResult p_Client) { bool _Exists = false; foreach (WebIMClientAsyncResult _Item in m_Clients) { if (_Item.LoginID == p_Client.LoginID) { _Exists = true; break; } } if (!_Exists) { m_Clients.Add(p_Client); } } /* public void UpdateUserList() { string _Users = GetUsers(); foreach (WebIMClientAsyncResult result in m_Clients) { result.Content = m_U result.Send(null); } m_Clients.Clear(); }*/
/// &summary&
/// 广播消息 /// &/summary&
/// &param name="p_Message"&&/param&
/// &param name="p_AsyncResult"&&/param&
public void AddMessage(string p_Message, WebIMClientAsyncResult p_ClientAsyncResult) { //保持连接
if (p_Message == "-1") { m_Clients.Add(p_ClientAsyncResult); } else { //将当前请求的内容输出到客户端
p_ClientAsyncResult.Content = p_M p_ClientAsyncResult.Send(null); //否则将遍历所有已缓存的client,并将当前内容输出到客户端
foreach (WebIMClientAsyncResult result in m_Clients) { //发送给所有已经登录用户
var strMsg = string.Format("{0}{1}{2}{3}{4}",p_ClientAsyncResult.LoginID,"发送给",result.LoginID,"的消息:",p_Message); //result.Content = p_M
result.Content = strM result.Send(null); //发送给指定用户
/* if (string.Equals(result.LoginID, "ZhangShan") && !string.Equals(p_ClientAsyncResult.LoginID, "ZhangShan")) { var strMsg = string.Format("{0}{1}{2}{3}{4}{5}","Msgs:", p_ClientAsyncResult.LoginID, "发送给", result.LoginID, "的消息:", p_Message); //result.Content = p_M result.Content = strM result.Send(null); } */ } //清空所有缓存
m_Clients.Clear(); } } } }
/// &reference path="jquery-1.3.2.min.js" &
$(document).ready(function () { //登录,登录成功后,获取在线用户列表,
function login() { //增加页面跳转
var strUrl = '/SendInfo.aspx?strUid=' + $("#txtLoginID").val(); window.open(strUrl); } $("#btnLogin").click(function () { if ($("#txtLoginID").val() == '') alert('空'); login(); }); })
/// &reference path="jquery-1.3.2.min.js" & //$(document).ready(function () {
//状态,代表是否登录
//var _logined =
//登录,登录成功后,获取在线用户列表,
function login() { //$.post("comet_broadcast.asyn", { action: 'login', uid: $("#txtLoginID").val() },
$.post("comet_broadcast.asyn", { action: 'login', uid: strUid }, function (data, status) { if (data == "OK") { _logined = true; getuserlist(); //增加页面跳转
/*var strUrl = '\SendInfo.aspx?strUid=' + $("#txtLoginID").val(); window.open(strUrl); */ } else { alert(data); } }); } //获取在线用户列表,获取列表后,进入消息等待
function getuserlist() { //$.post("comet_broadcast.asyn", { action: 'getuserlist', uid: $("#txtLoginID").val() },
$.post("comet_broadcast.asyn", { action: 'getuserlist', uid: strUid }, function (data, status) { //alert('getuserlist' + data);
var result = $("#divResult"); result.html(result.html() + "&br/&" + "用户列表:" + data); wait(); }); } //退出
function logout() { //$.post("comet_broadcast.asyn", { action: 'logout', uid: $("#txtLoginID").val() },
$.post("comet_broadcast.asyn", { action: 'logout', uid: strUid }, function (data, status) { _logined = false; alert(data); } ); } //消息等待,接收到消息后显示,发起下一次的消息等待
function wait() { //$.post("comet_broadcast.asyn", { action: 'connect', uid: $("#txtLoginID").val() },
$.post("comet_broadcast.asyn", { action: 'connect', uid: strUid }, function (data, status) { /* var result = $("#divResult"); result.html(result.html() + "&br/&" + "用户列表:" + data); */
new Ext.ux.ToastWindow({ title: '提示窗口', html: data, iconCls: 'error' }).show(document); //服务器返回消息,再次建立连接
if (_logined) { wait(); } }, "html"); } /*********** *********************消息发送部分*************************** ************/
function send() { //$.post("comet_broadcast.asyn", { action: 'sendmsg', uid: $("#txtLoginID").val(), content: $("#content").val() },
$.post("comet_broadcast.asyn", { action: 'sendmsg', uid: strUid, content: $("#content").val() }, function (data, status) { /* var result = $("#divResult"); result.html(result.html() + "&br/&" + "已发消息:" + data); */
//发送方页面提示
//潜规则:暂时不处理
/* //2.窗口 new Ext.ux.ToastWindow({ title: '提示窗口', html: data, iconCls: 'error' }).show(document); */ }, "html" ); //向comet_broadcast.asyn发送请求,消息体为文本框content中的内容,请求接收类为AsnyHandler
//$.post("comet_broadcast.asyn", { content: $("#content").val() });
//清空内容
$("#content").val(""); }; /** * 获取字符串中某个特殊字符首次出现的位置之前的子串 */
function GetSubStrBySpecChar(strConnStr,strSplit){ var arrStr = strConnStr.split(strSplit); var strSubStr = arrStr[0]; return strSubS }
Default.aspx
&%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="CometSample._Default" %&
&!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&
&html xmlns="http://www.w3.org/1999/xhtml"&
&head runat="server"&
&title&&/title&
&script src="Scripts/jquery-1.3.2.min.js" type="text/javascript"&&/script&
&script src="Scripts/Login.js" type="text/javascript"&&/script&
&form id="form1" runat="server"&
&asp:Label ID="Label1" runat="server" Text="帐号"&&/asp:Label&
&input id="txtLoginID" type="text" /&
&input id="btnLogin" type="button" value="Login" /&
&div id="divUserList"&
SendInfo.aspx
&%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SendInfo.aspx.cs" Inherits="CometSample.SendInfo" %&
&!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&
&html xmlns="http://www.w3.org/1999/xhtml"&
&head runat="server"&
&title&SendInfo&/title&
&script src="Scripts/jquery-1.3.2.min.js" type="text/javascript"&&/script&
&script src="Scripts/WebIM.js" type="text/javascript"&&/script&
&link href="Scripts/ext-3.4.0/resources/css/ext-all.css" rel="stylesheet" type="text/css" /&
&script src="Scripts/ext-3.4.0/adapter/ext/ext-base.js" type="text/javascript"&&/script&
&script src="Scripts/ext-3.4.0/ext-all.js" type="text/javascript"&&/script&
&script src="Scripts/ToastWindow.js" type="text/javascript"&&/script&
&script language="javascript" type="text/javascript"&
var strUid = "&%=strUid %&"; $(document).ready(function () { //状态,代表是否登录
var _logined = false; //alert(strUid);
//getuserlist_send();
login(); $("#btnSend").click(function () { send(); }) $("#content").keypress(function (e) { var keyCode = null; if (e.which) keyCode = e. else if (e.keyCode) keyCode = e.keyC if (keyCode == 13) { send(); return false; } return true; }); }) &/script&
&form id="form1" runat="server"&
&div id="divUserList"&
&br /& 广播内容: &input type="text" id="content" /&&br /& 消息记录: &div id="divResult"&
&input type="button" id="btnSend" value="广播" /&
&input id="btnLogout" type="button" value="注销" onclick="logout();"/&&/div&
最后还需要关注的是配置文件中的路径
在web.config 文件的system.web之间加上
&httpHandlers&
&add path="comet_broadcast.asyn" type="CometSample.WebIMAsyncHandler" verb="POST,GET"/&
&/httpHandlers&
好了运行程序,
登陆之后,跳转到sendinfo页面
笔者打开连个浏览器,模拟两个客户端登陆,并且模拟广播消息(能够广播,那么向指定客户端发消息也就很容易了)
我们可以看到在页面右下角,有消息弹出
在.NET WebForm下笔者实现了客户端之间即时消息的推送,但是在.NET MVC 2 下遇到了一些问题,因为mvc框架下对 ,NET WebForm中某些东西不支持。
博客园博客中的日历用的是 WebForms的日历控件(System.Web.UI.WebControls.Calendar),它会为“上一月”、“下一月”的链接生成”__doPostBack()”的js调用,如下图:
目前发现它会带来两个问题:
1. 不支持IE10;
2. 某些电脑不允许执行__doPostBack。
我们想以最低的成本解决这个问题,也就是对当前代码尽可能少的改动。所以要尽可能重用现有的日历控件代码。
日历改为Ajax加载,点击“上一月”、“下一月”时Ajax更新日历内容。
用 MVC处理Ajax请求。
要解决的问题:
如何在 Controller中加载包含WebForms日历控件的用户控件(.ascx),并得到其输出的字符串,然后将__doPostBack的代码替换为ajax调用代码。
核心问题:
如何在 Controller中得到用户控件(.ascx)输出的字符串。
public ActionResult Calendar() { var page = new Page(); var form = new HtmlForm(); var calendar = page.LoadControl("~/Controls/CNBlogsCalendar.ascx"); form.Controls.Add(calendar); page.Controls.Add(form); using (var sw = new StringWriter()) { System.Web.HttpContext.Current.Server.Execute(page, sw, true); return Content(sw.ToString()); } }
代码很简单,但得到这个代码花了今天一上午时间。
代码说明:
必须要new Page(),只有Page才能LoadControl。
必须要new HtmlForm(),因为日历控件必要要放在&form runat=”server”&之间。
关键功臣是HttpContext.Current.Server.Execute,动态加载控件并输出字符串全靠它。这个功臣是在中找到的(感谢Sam Mueller)。之前我用过的方法()不仅麻烦,而且在这个场景下会有问题。
代码运行结果:
完整代码下载:
User Control大家肯定不会陌生,在使用的过程中,除了aspx页面,最常见的就莫过于ascx了。ascx是一个有独立逻辑的组件,提供 了强大的复用特性,合理使用,能够大大提高开发效率。通过User Control直接生成HTML内容其实已经是一个比较常用的技巧了(尤其在AJAX时代),不过网络上这方面的内容比较少,很多人还是在苦苦地拼接字符 串,因此在这里我通过一个实例简单介绍一下这个技巧。
对一个对象(文章,图片,音乐,etc.)进行评论是应用中最常见的功能之一。首先,我们定义一个Comment类,以及其中会用到的“获取”方法:
public partial class Comment
public DateTime CreateTime { get; set; }
public string Content { get; set; }
public partial class Comment
private static List&Comment& s_comments = new List&Comment&
new Comment
CreateTime = DateTime.Parse(""),
Content = "今天天气不错"
new Comment
CreateTime = DateTime.Parse(""),
Content = "挺风和日丽的"
new Comment
CreateTime = DateTime.Parse(""),
Content = "我们下午没有课"
new Comment
CreateTime = DateTime.Parse(""),
Content = "这的确挺爽的"
public static List&Comment& GetComments(int pageSize, int pageIndex, out int totalCount)
totalCount = s_comments.C
List&Comment& comments = new List&Comment&(pageSize);
for (int i = pageSize * (pageIndex - 1);
i & pageSize * pageIndex && i & s_comments.C i++)
comments.Add(s_comments[i]);
为了显示一个评论列表,我们可以使用一个用户控件(ItemComments.aspx)来封装。自然,分页也是必不可少的:
&asp:Repeater runat="server" ID="rptComments"&
&ItemTemplate&
时间:&%# (Container.DataItem as Comment).CreateTime.ToString() %&&br /&
内容:&%# (Container.DataItem as Comment).Content %& &/ItemTemplate&
&SeparatorTemplate&
&/SeparatorTemplate& &FooterTemplate& &hr /& &/FooterTemplate&
&/asp:Repeater&
&% if (this.PageIndex & 1)
&a href="/ViewItem.aspx?page=&%= this.PageIndex - 1 %&" title="上一页"&上一页&/a&&
&% if (this.PageIndex * this.PageSize & this.TotalCount)
&a href="/ViewItem.aspx?page=&%= this.PageIndex + 1 %&" title="上一页"&下一页&/a&
public partial class ItemComments : System.Web.UI.UserControl
protected override void OnPreRender(EventArgs e)
base.OnPreRender(e);
this.rptComments.DataSource = Comment.GetComments(this.PageSize,
this.PageIndex, out this.m_totalCount); this.DataBind();
public int PageIndex { get; set; }
public int PageSize { get; set; }
private int m_totalC
public int TotalCount
return this.m_totalC
然后再页面(ViewItem.aspx)中使用这个组件:
&div id="comments"&&demo:ItemComments ID="itemComments" runat="server" /&&/div&
public partial class ViewItem : System.Web.UI.Page
protected void Page_Load(object sender, EventArgs e)
this.itemComments.PageIndex = this.PageI
protected int PageIndex
int result = 0;
Int32.TryParse(this.Request.QueryString["page"], out result);
return result & 0 ? result : 1;
打开ViewItem.aspx之后效果如下:
时间: 0:00:00 内容:今天天气不错
时间: 0:00:00 内容:挺风和日丽的
时间: 0:00:00 内容:我们下午没有课
这 张页面的功能非常简单,那就是察看评论。当前评论的页码会使用QueryString的page项进行指定,然后在ViewItem.aspx里获取到并 且设置ItemComments.ascx控件的属性。ItemComments控件会根据自身属性来获取数据,进行绑定,至于显示内容,全都定义在 ascx中了。由于需要分页功能,这个评论控件中还包含了上一页和下一页的链接,他们链接的目标很简单,就是ViewItem.aspx页,并且加上页码 的Query String而已。
功能是完成了,不过用着用着忽然觉得不妥,为什么呢?因为我们在翻页,或者用户发布评论的时候,整张页 面都刷新了。这可不好,要知道可能ViewItem页中还有其他几个显示部分,它们可是不变的。而且如果其他几个部分也需要分页,那么可能就需要保留页面 上每一部分的当前页码,这样开发的复杂性还是比较高的。
那么我们不如用AJAX吧。无论是用户察看评论时进行翻页还是发表评论,都不会对页面上的其他内容造成影响。要开发这个功能,自然需要服务器端的支持,那么该怎么做呢?一般我们总是有两种选择:
服务器端返回JSON数据,在客户端操作DOM进行呈现。
服务器端直接返回HTML内容,然后在客户端设置容器(例如上面id为comments的div)。
不 过无论采用哪种做法,“呈现”的逻辑一般总是另写一遍(第一次的呈现逻辑写在了ItemComments.ascx中)。如果使用第1种做法,那么呈现逻 辑就需要在客户端通过操作DOM进行呈现;如果使用第2种做法,那么就要在服务器端进行字符串拼接。无论哪种做法都违背了DRY原则,当 ItemComments.ascx里的呈现方式修改时,另一处也要跟着修改。而且无论是操作DOM元素还是拼接字符串维护起来都比较麻烦,开发效率自然 也就不高了。
如果我们能够直接从ItemComments控件获得HTML内容该多好啊——那么我们就这么做吧。请看如下代码(GetComments.ashx):
public class GetComments : IHttpHandler
public void ProcessRequest(HttpContext context)
context.Response.ContentType = "text/plain";
ViewManager&ItemComments& viewManager = new ViewManager&ItemComments&();
ItemComments control = viewManager.LoadViewControl("~/ItemComments.ascx");
control.PageIndex = Int32.Parse(context.Request.QueryString["page"]);
control.PageSize = 3;
context.Response.Write(viewManager.RenderView(control));
public bool IsReusable { ... }
很简单的代码,不是吗?创建对象,设置属性,然后通过Response.Write输出而已。实在没什么大不了的——不过关键就在于ViewManager类,我们来看一下它是怎么实现的:
public class ViewManager&T& where T : UserControl
private Page m_pageH
public T LoadViewControl(string path)
this.m_pageHolder = new Page();
return (T)this.m_pageHolder.LoadControl(path);
public string RenderView(T control)
StringWriter output = new StringWriter();
this.m_pageHolder.Controls.Add(control);
HttpContext.Current.Server.Execute(this.m_pageHolder, output, false);
return output.ToString();
ViewManager中只有两个方法:LoadViewControl和RenderView。LoadViewControl方法的作用是创 建一个Control实例并返回,RenderView方法的作用则就是生成HTML了。这个实现方式的技巧在于使用了一个新建的Page对象作为生成控 件的“容器”,而最后其实我们是将Page对象的整个生命周期运行一遍,并且将结果输出。由于这个空的Page对象不会产生任何其他代码,因此我们得到 的,就是用户控件生成的代码了。
不过要实现这个AJAX效果,还需要做两件事情。
第一,就是简单修改一下ItemComments控件中的翻页链接,让它被点击时调用一个函数。例如“上一页”的代码就会变成:
&a href="/ViewItem.aspx?page=&%= this.PageIndex - 1 %&" title="上一页"
onclick="return getComments(&%= this.PageIndex - 1 %&);"&上一页&/a&
第二,就是实现getComments这个客户端方法。在这里我使用了prototype框架,好处就是能够用相当简洁的代码来做到替换HTML的AJAX效果:
&script type="text/javascript" language="javascript"&
function getComments(pageIndex)
new Ajax.Updater(
"comments",
"/GetComments.ashx?page=" + pageIndex + "&t=" + new Date(),
{ method: "get" });
return false; // IE only
大功告成。
其实就像之前所说的那样,使用UserControl进行HTML代码生成是一个十分常用的技巧。尤其在AJAX应用 越来越普及的情况下,合理使用上面提到的方式可以方便的为我们的应用添加AJAX效果。而且很多情况下,我们即使不需要在页面上显示内容,也可以将内容使 用UserControl进行编辑。因为编写UserControl比拼接字符串的方式无论是在开发效率上还是可维护性上都高出许多。由于这个方式其实使 用了WebForms这个久经考验的模型,因此在执行效率方面也是相当高的。此外,就刚才的例子来说,使用UserCotrol进行HTML生成还有其他 好处:
页面呈现逻辑只实现了一次,提高了可维护性。
不会影响页面的,因为在客户端&a /&的href还是有效的。
事 实上,WebForms是一个非常强大的模型,所以 MVC的View也使用了WebForms的引擎。通过上面这个例子,我们其实还可以做到其他很多东西——例如用UserControl来生成XML数 据,因为UserControl本身不会带来任何额外的内容。
对象缓存和n+1问题分析
我们常见的OLTP类型的web应用,性能瓶颈往往是数据库查询,因为应用服务器层面可以水平扩展,但是数据库是单点的,很难水平扩展,当数据库服 务器发生磁盘IO,往往无法有效提高性能,因此如何有效降低数据库查询频率,减轻数据库磁盘IO压力,是web应用性能问题的根源。
对象缓存是所有缓存技术当中适用场景最广泛的,任何OLTP应用,即使实时性要求很高,你也可以使用对象缓存,而且好的ORM实现,对象缓存是完全透明的,完全不需要你的程序代码进行硬编码。
用不用对象缓存,怎么用对象缓存,不是一个简单的代码调优技巧,而是整个应用的架构问题。在你开发一个应用之前,你就要想清楚,这个应用最终的场景是什么?会有多大的用户量和数据量。你将采用什么方式来架构这个应用:
也许你偏好对语句级别的优化,数据库设计当大表有很多冗余字段,会尽量消除大表之间的关联关系,当数据量很大以后,选择分库分表的优化方式, 这是目前业界常规做法。但是也可以选择使用ORM的对象缓存优化方式:数据库设计避免出现大表,比较多的表关联关系,通过ORM以对象化方式操作,利用对 象缓存提升性能。举个例子:
论坛的列表页面,需要显示topic的分页列表,topic作者的名字,topic最后回复帖子的作者,常规做法:
select ... from topic left join user left join post .....
你需要通过join user表来取得topic作者的名字,然后你还需要join post表取得最后回复的帖子,post再join user表取得最后回贴作者名字。也许你说,我可以设计表冗余,在topic里面增加username,在post里面增加username,所以通过大 表冗余字段,消除了复杂的表关联:
select ... from topic left join post...
OK,且不说冗余字段的维护问题,现在仍然是两张大表的关联查询。然后让我们看看ORM怎么做?
select * from topic where ... --分页条件
就这么一条搞定,比上面的关联查询对数据库的压力小多了。 也许你说,不对阿,作者信息呢?回贴作者信息呢?这些难道不会发送吗?如果发送SQL,这不就是臭名昭著的n+1条问题吗? 你说的对,最坏情况下,会有很多条SQL:
select * from user where id = topic_id...; .... select * from user where id = topic_id...; select * from post where id = last_topic_id...; .... select * from post where id = last_topic_id...; select * from user where id = post_id...; .... select * from user where id = post_id...;
事实上何止n+1,根本就是3n+1条SQL了。那你怎么还说ORM性能高呢? 因为对象缓存在起作用,你可以观察到后面的3n条SQL语句全部都是基于主键的单表查询,这3n条语句在理想状况下(比较繁忙的web的热点数据), 全部都可以命中缓存。所以事实上只有一条SQL,就是:
select * from topic where ...--分页条件
这条单表的条件查询和直接使用join查询SQL通过字段冗余简化过后的大表关联查询相比,当数据量大到一定程度以后对数据库磁盘IO的压力很小,这就是对象缓存的真正威力!
更进一步分析,使用ORM,我们不考虑缓存的情况,那么就是3n+1条SQL。但是这3n+1条SQL的执行速度一定比SQL的大表关联查询慢吗? 不一定!因为使用ORM的情况下,第一条SQL是单表的条件查询,在有索引的情况下,速度很快,后面的3n条SQL都是单表的主键查询,在繁忙的数据库系 统当中,3n条SQL几乎可以全部命中数据库的data buffer。但是使用SQL的大表关联查询,很可能会造成大范围的表扫描,造成频繁的数据库服务器磁盘IO,性能有可能是非常差的。
因此,即使不使用对象缓存,ORM的n+1条SQL性能仍然很有可能超过SQL的大表关联查询,而且对数据库磁盘IO造成的压力要小很多。这个结论貌似令人难以置信,但经过我的实践证明,就是事实。前提是数据量和访问量都要比较大,否则看不出来这种效果。
对象缓存的命中率
是OLTP还是OLAP应用,即使是OLTP,也要看访问的频度,一个极少被访问到的缓存等于没有什么效果。一般来说,是非常适合缓存应用的场景。
缓存的粒度
毫无疑问,缓存的粒度越小,命中率就越高,对象缓存是目前缓存粒度最小的,因此被命中的几率更高。举个例子来说吧:你访问当前这个页面,浏览帖子, 那么对于ORM来说,需要发送n条SQL,取各自帖子user的对象。很显然,如果这个user在其他帖子里面也跟贴了,那么在访问那个帖子的时候,就可 以直接从缓存里面取这个user对象了。
架构的设计
架构的设计对于缓存命中率也有至关重要的影响。例如你应该如何去尽量避免缓存失效的问题,如何尽量提供频繁访问数据的缓存问题,这些都是考验架构师 水平的地方。再举个例子来说,对于论坛,需要记录每个topic的浏览次数,所以每次有人访问这个topic,那么topic表就要update一次,这 意味着什么呢?对于topic的对象缓存是无效的,每次访问都要更新缓存。那么可以想一些办法,例如增加一个中间变量记录点击次数,每累计一定的点击,才 更新一次数据库,从而减低缓存失效的频率。
缓存的容量和缓存的有效期
缓存太小,造成频繁的LRU,也会降低命中率,缓存的有效期太短也会造成缓存命中率下降。
所以缓存命中率问题不能一概而论,一定说命中率很低或者命中率很高。但是如果你对于缓存的掌握很精通,有意识的去调整应用的架构,去分解缓存的粒度,总是会带来很高的命中率的。
ORM缓存引言
从10年前的2003年开始,在Web应用领域,ORM(对象-关系映射)框架就开始逐渐普及,并且流行开来,其中最广为人知的就是Java的开源 ORM框架Hibernate,后来Hibernate也成为了EJB3的实现框架;2005年以后,ORM开始普及到其他编程语言领域,其中最有名气的 是Ruby on rails框架的ORM - ActiveRecord。如今各种开源框架的ORM,乃至ODM(对象-文档关系映射,用在访问NoDB)层出不穷,功能都十分强大,也很普及。
然而围绕ORM的性能问题,也一直有很多批评的声音。其实ORM的架构对插入缓存技术是非常容易的,我做的很多项目和产品,但凡使用ORM,缓存都 是标配,性能都非常好。而且我发现业界使用ORM的案例都忽视了缓存的运用,或者说没有意识到ORM缓存可以带来巨大的性能提升。
ORM缓存应用案例
我们去年有一个老产品重写的项目,这个产品有超过10年历史了,数据库的数据量很大,多个表都是上千万条记录,最大的表记录达到了9000万条,Web访问的请求数每天有300万左右。
老产品采用了传统的解决性能问题的方案:Web层采用了动态页面静态化技术,超过一定时间的文章生成静态HTML文件;对数据库进行分库分表,按年 拆表。动态页面静态化和分库分表是应对大访问量和大数据量的常规手段,本身也有效。但它的缺点也很多,比方说增加了代码复杂度和维护难度,跨库运算的困难 等等,这个产品的代码维护历来非常困难,导致bug很多。
进行产品重写的时候,我们放弃了动态页面静态化,采用了纯动态网页;放弃了分库分表,直接操作千万级,乃至近亿条记录的大表进行查询;也没有 采取读写分离技术,全部查询都是在单台主数据库上进行;数据库访问全部使用ActiveRecord,进行了大量的ORM缓存。上线以后的效果非常好:单 台MySQL数据库服务器CPU的IO Wait低于5%;用单台1U服务器2颗4核至强CPU已经可以轻松支持每天350万动态请求量;最重要的是,插入缓存并不需要代码增加多少复杂度,可维 护性非常好。
总之,采用ORM缓存是Web应用提升性能一种有效的思路,这种思路和传统的提升性能的解决方案有很大的不同,但它在很多应用场景(包括高度动态化 的SNS类型应用)非常有效,而且不会显著增加代码复杂度,所以这也是我自己一直偏爱的方式。因此我一直很想写篇文章,结合示例代码介绍ORM缓存的编程 技巧。
今年春节前后,我开发自己的个人项目,有意识的大量使用了ORM缓存技巧。对一个没多少访问量的个人站点来说,有些过度设计了,但我也想借这个机会把常用的ORM缓存设计模式写成示例代码,提供给大家参考。我的个人源代码是开源的,托管在github上:
ORM缓存的基本理念
我在2007年的时候写过一篇文章,分析ORM缓存的理念: ,所以这篇文章不展开详谈了,总结来说,ORM缓存的基本理念是:
以减少数据库服务器磁盘IO为最终目的,而不是减少发送到数据库的SQL条数。实际上使用ORM,会显著增加SQL条数,有时候会成倍增加SQL。
数据库schema设计的取向是尽量设计 细颗粒度 的表,表和表之间用外键关联,颗粒度越细,缓存对象的单位越小,缓存的应用场景越广泛
尽量避免多表关联查询,尽量拆成多个表单独的主键查询,尽量多制造 n + 1 条查询,不要害怕“臭名昭著”的 n + 1 问题,实际上 n + 1 才能有效利用ORM缓存
利用表关联实现透明的对象缓存
在设计数据库的schema的时候,设计多个细颗粒度的表,用外键关联起来。当通过ORM访问关联对象的时候,ORM框架会将关联对象的访问转化成用主键查询关联表,发送 n + 1条SQL。而基于主键的查询可以直接利用对象缓存。
我们自己开发了一个基于ActiveRecord封装的对象缓存框架: ,从这个ruby插件的名称就可以看出,实现借鉴了Hibernate的二级缓存实现。这个对象缓存的配置和使用,可以看我写的 。
下面用一个实际例子来演示一下对象缓存起到的作用:访问我个人站点的首页。 这个页面的数据需要读取三张表:blogs表获取文章信息,blog_contents表获取文章内容,accounts表获取作者信息。三张表的model定义片段如下,完整代码请看 :
class Account & ActiveRecord::Base acts_as_cached has_many :blogs end class Blog & ActiveRecord::Base acts_as_cached belongs_to :blog_content, :dependent =& :destroy belongs_to :account, :counter_cache =& true end class BlogContent & ActiveRecord::Base acts_as_cached end
传统的做法是发送一条三表关联的查询语句,类似这样的:
SELECT blogs.*, blog_contents.content, account.name FROM blogs LEFT JOIN blog_contents ON blogs.blog_content_id = blog_contents.id LEFT JOIN accounts ON blogs.account_id = account.id
往往单条SQL语句就搞定了,但是复杂SQL的带来的表扫描范围可能比较大,造成的数据库服务器磁盘IO会高很多,数据库实际IO负载往往无法得到有效缓解。
我的做法如下,完整代码请看 :
@blogs = Blog.order('id DESC').page(params[:page])
这是一条分页查询,实际发送的SQL如下:
SELECT * FROM blogs ORDER BY id DESC LIMIT 20
转成了单表查询,磁盘IO会小很多。至于文章内容,则是通过blog.content的对象访问获得的,由于首页抓取20篇文章,所以实际上会多出来20条主键查询SQL访问blog_contents表。就像下面这样:
DEBUG - BlogContent Load (0.3ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 29 LIMIT 1 DEBUG - BlogContent Load (0.2ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 28 LIMIT 1 DEBUG - BlogContent Load (1.3ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 27 LIMIT 1 ...... DEBUG - BlogContent Load (0.9ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 10 LIMIT 1
但是主键查询SQL不会造成表的扫描,而且往往已经被数据库buffer缓存,所以基本不会发生数据库服务器的磁盘IO,因而总体的数据库IO负载 会远远小于前者的多表联合查询。特别是当使用对象缓存之后,会缓存所有主键查询语句,这20条SQL语句往往并不会全部发生,特别是热点数据,缓存命中率 很高:
DEBUG - Cache read: robbin/blog/29/1 DEBUG - Cache read: robbin/account/1/0 DEBUG - Cache read: robbin/blogcontent/29/0 DEBUG - Cache read: robbin/account/1/0 DEBUG - Cache read: robbin/blog/28/1 ...... DEBUG - Cache read: robbin/blogcontent/11/0 DEBUG - Cache read: robbin/account/1/0 DEBUG - Cache read: robbin/blog/10/1 DEBUG - Cache read: robbin/blogcontent/10/0 DEBUG - Cache read: robbin/account/1/0
拆分n+1条查询的方式,看起来似乎非常违反大家的直觉,但实际上这是真理,我实践经验证明:数据库服务器的瓶颈往往是磁盘IO,而不是SQL并发数量。因此 拆分n+1条查询本质上是以增加n条SQL语句为代价,简化复杂SQL,换取数据库服务器磁盘IO的降低 当然这样做以后,对于ORM来说,有额外的好处,就是可以高效的使用缓存了。
按照column拆表实现细粒度对象缓存
数据库的瓶颈往往在磁盘IO上,所以应该尽量避免对大表的扫描。传统的拆表是按照row去拆分,保持表的体积不会过大,但是缺点是造成应用代码复杂度很高;使用ORM缓存的办法,则是按照column进行拆表,原则一般是:
将大字段拆分出来,放在一个单独的表里面,表只有主键和大字段,外键放在主表当中
将不参与where条件和统计查询的字段拆分出来,放在独立的表中,外键放在主表当中
按照column拆表本质上是一个去关系化的过程。主表只保留参与关系运算的字段,将非关系型的字段剥离到关联表当中,关联表仅允许主键查询,以Key-Value DB的方式来访问。因此这种缓存设计模式本质上是一种SQLDB和NoSQLDB的混合
下面看一个实际的例子:文章的内容content字段是一个大字段,该字段不能放在blogs表中,否则会造成blogs表过大,表扫描造成较多的磁盘IO。我实际做法是创建blog_contents表,保存content字段,schema简化定义如下:
CREATE TABLE `blogs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `blog_content_id` int(11) NOT NULL, `content_updated_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), ); CREATE TABLE `blog_contents` ( `id` int(11) NOT NULL AUTO_INCREMENT, `content` mediumtext NOT NULL, PRIMARY KEY (`id`) );
blog_contents表只有content大字段,其外键保存到主表blogs的blog_content_id字段里面。
model定义和相关的封装如下:
class Blog & ActiveRecord::Base acts_as_cached delegate :content, :to =& :blog_content, :allow_nil =& true def content=(value) self.blog_content ||= BlogContent.new self.blog_content.content = value self.content_updated_at = Time.now end end class BlogContent & ActiveRecord::Base acts_as_cached validates :content, :presence =& true end
在Blog类上定义了虚拟属性content,当访问blog.content的时候,实际上会发生一条主键查询的SQL语句,获取blog_content.content内容。由于BlogContent上面定义了对象缓存acts_as_cached,只要被访问过一次,content内容就会被缓存到memcached里面。
这种缓存技术实际会非常有效,因为: 只要缓存足够大,所有文章内容可以全部被加载到缓存当中,无论文章内容表有多么大,你都不需要再访问数据库了 更进一步的是: 这张大表你永远都只需要通过主键进行访问,绝无可能出现表扫描的状况 为何当数据量大到9000万条记录以后,我们的系统仍然能够保持良好的性能,秘密就在于此。
还有一点非常重要: 使用以上两种对象缓存的设计模式,你除了需要添加一条缓存声明语句acts_as_cached以外,不需要显式编写一行代码 有效利用缓存的代价如此之低,何乐而不为呢?
以上两种缓存设计模式都不需要显式编写缓存代码,以下的缓存设计模式则需要编写少量的缓存代码,不过代码的增加量非常少。
写一致性缓存
写一致性缓存,叫做write-through cache,是一个CPU Cache借鉴过来的概念,意思是说,当数据库记录被修改以后,同时更新缓存,不必进行额外的缓存过期处理操作。但在应用系统中,我们需要一点技巧来实现写一致性缓存。来看一个例子:
我的文章原文是markdown格式的,当页面显示的时候,需要转换成html的页面,这个转换过程本身是非常消耗CPU的,我使用的是 Github的markdown的库。Github为了提高性能,用C写了转换库,但如果是非常大的文章,仍然是一个耗时的过程,Ruby应用服务器的负 载就会比较高。
我的解决办法是缓存markdown原文转换好的html页面的内容,这样当再次访问该页面的时候,就不必再次转换了,直接从缓存当中取出已经缓存好的页面内容即可,极大提升了系统性能。我的网站文章最终页的代码执行时间开销往往小于10ms,就是这个原因。代码如下:
def md_content
APP_CACHE.fetch(content_cache_key) { GitHub::Markdown.to_html(content, :gfm) } end
这里存在一个如何进行缓存过期的问题,当文章内容被修改以后,应该更新缓存内容,让老的缓存过期,否则就会出现数据不一致的现象。进行缓存过期处理是比较麻烦的,我们可以利用一个技巧来实现自动缓存过期:
def content_cache_key "#{CACHE_PREFIX}/blog_content/#{self.id}/#{content_updated_at.to_i}" end
当构造缓存对象的key的时候,我用文章内容被更新的时间来构造key值,这个文章内容更新时间用的是blogs表的 content_updated_at字段,当文章被更新的时候,blogs表会进行update,更新该字段。因此每当文章内容被更新,缓存的页面内容 的key就会改变,应用程序下次访问文章页面的时候,缓存就会失效,于是重新调用GitHub::Markdown.to_html(content, :gfm)生成新的页面内容。 而老的页面缓存内容再也不会被应用程序存取,根据memcached的LRU算法,当缓存填满之后,将被优先剔除。
除了文章内容缓存之外,文章的评论内容转换成html以后也使用了这种缓存设计模式。具体可以看相应的源代码:
片段缓存和过期处理
Web应用当中有大量的并非实时更新的数据,这些数据都可以使用缓存,避免每次存取的时候都进行数据库查询和运算。这种片段缓存的应用场景很多,例如:
展示网站的Tag分类统计(只要没有更新文章分类,或者发布新文章,缓存一直有效)
输出网站RSS(只要没有发新文章,缓存一直有效)
网站右侧栏(如果没有新的评论或者发布新文章,则在一段时间例如一天内基本不需要更新)
以上应用场景都可以使用缓存,代码示例:
def self.cached_tag_cloud APP_CACHE.fetch("#{CACHE_PREFIX}/blog_tags/tag_cloud") do self.tag_counts.sort_by(&:count).reverse end end
对全站文章的Tag云进行查询,对查询结果进行缓存
&% cache("#{CACHE_PREFIX}/layout/right", :expires_in =& 1.day) do %&
class="tag"& &% Blog.cached_tag_cloud.select {|t| t.count & 2}.each do |tag| %& &%= link_to "#{tag.name}&span&#{tag.count}&/span&".html_safe, url(:blog, :tag, :name =& tag.name) %& &% end %& & ...... &% end %&
对全站右侧栏页面进行缓存,过期时间是1天。
缓存的过期处理往往是比较麻烦的事情,但在ORM框架当中,我们可以利用model对象的回调,很容易实现缓存过期处理。我们的缓存都是和文章,以 及评论相关的,所以可以直接注册Blog类和BlogComment类的回调接口,声明当对象被保存或者删除的时候调用删除方法:
class Blog & ActiveRecord::Base acts_as_cached after_save :clean_cache before_destroy :clean_cache def clean_cache APP_CACHE.delete("#{CACHE_PREFIX}/blog_tags/tag_cloud")
APP_CACHE.delete("#{CACHE_PREFIX}/rss/all")
APP_CACHE.delete("#{CACHE_PREFIX}/layout/right")
end end class BlogComment & ActiveRecord::Base acts_as_cached after_save :clean_cache before_destroy :clean_cache def clean_cache APP_CACHE.delete("#{CACHE_PREFIX}/layout/right")
在Blog对象的after_save和before_destroy上注册clean_cache方法,当文章被修改或者删除的时候,删除以上缓存内容。总之,可以利用ORM对象的回调接口进行缓存过期处理,而不需要到处写缓存清理代码。
对象写入缓存
我们通常说到缓存,总是认为缓存是提升应用读取性能的,其实缓存也可以有效的提升应用的写入性能。我们看一个常见的应用场景:记录文章点击次数这个功能。
文章点击次数需要每次访问文章页面的时候,都要更新文章的点击次数字段view_count,然后文章必须实时显示文章的点击次数,因此常见的读缓存模式完全无效了。每次访问都必须更新数据库,当访问量很大以后数据库是吃不消的,因此我们必须同时做到两点:
每次文章页面被访问,都要实时更新文章的点击次数,并且显示出来
不能每次文章页面被访问,都更新数据库,否则数据库吃不消
对付这种应用场景,我们可以利用对象缓存的不一致,来实现对象写入缓存。原理就是每次页面展示的时候,只更新缓存中的对象,页面显示的时候优先读取缓存,但是不更新数据库,让缓存保持不一致,积累到n次,直接更新一次数据库,但绕过缓存过期操作。具体的做法可以参考 :
def increment_view_count increment(:view_count)
write_second_level_cache
self.class.update_all({:view_count =& view_count}, :id =& id) if view_count % 10 == 0 end
increment(:view_count)增加view_count计数,关键代码是第2行write_second_level_cache,更新view_count之后直接写入缓存,但不更新数据库。累计10次点击,再更新一次数据库相应的字段。另外还要注意,如果blog对象不是通过主键查询,而是通过查询语句构造的,要优先读取一次缓存,保证页面点击次数的显示一致性,因此
这个页面模版文件开头有这样一段代码:
view_count = blog.view_count if b = Blog.read_second_level_cache(blog.id) view_count = b.view_count end %&
采用对象写入缓存的设计模式,就可以非常容易的实现写入操作的缓存,在这个例子当中,我们仅仅增加了一行缓存写入代码,而这个时间开销大约是1ms,就可以实现文章实时点击计数功能,是不是非常简单和巧妙?实际上我们也可以使用这种设计模式实现很多数据库写入的缓存功能。
常用的ORM缓存设计模式就是以上的几种,本质上都是非常简单的编程技巧,代码的增加量和复杂度也非常低,只需要很少的代码就可以实现,但是在实际 应用当中,特别是当数据量很庞大,访问量很高的时候,可以发挥惊人的效果。我们实际的系统当中,缓存命中次数:SQL查询语句,一般都是5:1左右,即每 次向数据库查询一条SQL,都会在缓存当中命中5次,数据主要都是从缓存当中得到,而非来自于数据库了。
其他缓存的使用技巧
还有一些并非ORM特有的缓存设计模式,但是在Web应用当中也比较常见,简单提及一下:
用数据库来实现的缓存
在我这个网站当中,每篇文章都标记了若干tag,而tag关联关系都是保存到数据库里面的,如果每次显示文章,都需要额外查询关联表获取tag,显然会非常消耗数据库。在我使用的acts-as-taggable-on插件中,它在blogs表当中添加了一个cached_tag_list字段,保存了该文章标记的tag。当文章被修改的时候,会自动相应更新该字段,避免了每次显示文章的时候都需要去查询关联表的开销。
HTTP客户端缓存
基于资源协议实现的HTTP客户端缓存也是一种非常有效的缓存设计模式,我在2009年写过一篇文章详细的讲解了: ,所以这里就不再复述了。
用缓存实现计数器功能
这种设计模式有点类似于对象写入缓存,利用缓存写入的低开销来实现高性能计数器。举一个例子:用户登录为了避免遭遇密码暴力破解,我限定了每小时每IP只能尝试登录5次,如果超过5次,拒绝该IP再次尝试登录。代码实现很简单,如下:
post :login, :map =& '/login' do login_tries = APP_CACHE.read("#{CACHE_PREFIX}/login_counter/#{request.ip}") halt 403 if login_tries && login_tries.to_i & 5
@account = Account.new(params[:account]) if login_account = Account.authenticate(@account.email, @account.password) session[:account_id] = login_account.id redirect url(:index) else
APP_CACHE.increment("#{CACHE_PREFIX}/login_counter/#{request.ip}", 1, :expires_in =& 1.hour) render 'home/login' end end
等用户POST提交登录信息之后,先从缓存当中取该IP尝试登录次数,如果大于5次,直接拒绝掉;如果不足5次,而且登录失败,计数加1,显示再次尝试登录页面。
以上相关代码可以从这里获取:
在Visual Studio中通过修改发布配置文件,可以在发布Web时自动调用YUICompressor批量压缩项目中JS和CSS。这种方式的优点,一是不需要在 项目的js、css文件夹中单独建立子文件夹来存放未经压缩的文件,二是使用模式发布时不会进行压缩方便调试。具体方法如下:
1 安装JRE,下载YUICompressor,并解压(如:E:\工具\yuicompressor)
2 新建Compressor.bat文件内容为:
if + == “” goto exit
pushd +
echo 正在压缩Css文件
for /r %%i in (*.css) do call “java.exe” -jar E:\工具\yuicompressor\yuicompressor.jar -o %%i %%i
echo 正在压缩js文件
for /r %%i in (*.js) do call “java.exe” -jar E:\工具\yuicompressor\yuicompressor.jar -o %%i %%i
3 修改项目的发布配置文件, 项目的发布配置文件名为 &profilename&.pubxml,位于项目文件夹下的properties\PublishProfiles文件夹
增加下面的内容:
&Target Name=”YUICompressor” AfterTargets=”CopyAllFilesToSingleFolderForPackage” Condition=”‘$(ConfigurationName)’==’Release'”&
&Message Text=”调用YUICompressor压缩CSS、JS” Importance=”high” /&
&Exec Command=”call E:\工具\Compressor.bat $(ProjectDir)obj\$(ConfigurationName)\Package\” /&
注: E:\工具\Compressor.bat路径根据需要替换。
完成上面的工作后在“解决方案资源管理器”中右击要发布的项目点击“发布”后就可以在项目发布文件夹中看到已经压缩过的CSS、JS了
&html xmlns=&http://www.w3.org/1999/xhtml&&
&meta http-equiv=&Content-Type& content=&text/ charset=utf-8& /&
&title&表格&/title&
&style type=&text/css&&
border-width:1
border-top-style:
border-left-style:
border-right-style:
border-bottom-style:
border-color:#030;
width:100%;
&script src=&jquery.js& type=&text/javascript&&
&script type=&text/javascript&&
function moveUp(obj)
var current=$(obj).parent().parent();
var prev=current.prev();
if(current.index()&1)
current.insertBefore(prev);
function moveDown(obj)
var current=$(obj).parent().parent();
var next=current.next();
current.insertAfter(next);
&table class=&grid& width=&100%& border=&1& cellspacing=&0& cellpadding=&0&&
&td&字段英文名&/td&
&td&字段中文名&/td&
&td&字段数据类型&/td&
&td&列宽&/td&
&td&是否显示&/td&
&td&是否作为查询条件&/td&
&td&调整顺序&/td&
&td&1&/td&
&td&&input type=&text& name=&textfield& class=&editText& id=&textfield&&&/td&
&td& &/td&
&td&&input type=&text& name=&textfield5& id=&textfield5&&&/td&
&td&&input type=&checkbox& name=&checkbox& id=&checkbox&&&/td&
&td&&input type=&checkbox& name=&checkbox5& id=&checkbox5&&&/td&
&td&&a href=&javascript:void(0)& onClick=&moveUp(this)&&上移&/a&&a href=&javascript:void(0)& onClick=&moveDown(this)&&下移&/a&&/td&
&td&2&/td&
&td&&input type=&text& name=&textfield2& id=&textfield2&&&/td&
&td& &/td&
&td&&input type=&text& name=&textfield6& id=&textfield6&&&/td&
&td&&input type=&checkbox& name=&checkbox2& id=&checkbox2&&&/td&
&td&&input type=&checkbox& name=&checkbox6& id=&checkbox6&&&/td&
&td&&a href=&javascript:void(0)& onClick=&moveUp(this)&&上移&/a&&a href=&javascript:void(0)& onClick=&moveDown(this)&&下移&/a&&/td&
&td&3&/td&
&td&&input type=&text& name=&textfield3& id=&textfield3&&&/td&
&td& &/td&
&td&&input type=&text& name=&textfield7& id=&textfield7&&&/td&
&td&&input type=&checkbox& name=&checkbox3& id=&checkbox3&&&/td&
&td&&input type=&checkbox& name=&checkbox7& id=&checkbox7&&&/td&
&td&&a href=&javascript:void(0)& onClick=&moveUp(this)&&上移&/a&&a href=&javascript:void(0)& onClick=&moveDown(this)&&下移&/a&&/td&
&td&4&/td&
&td&&input type=&text& name=&textfield4& id=&textfield4&&&/td&
&td& &/td&
&td&&input type=&text& name=&textfield8& id=&textfield8&&&/td&
&td&&input type=&checkbox& name=&checkbox4& id=&checkbox4&&&/td&
&td&&input type=&checkbox& name=&checkbox8& id=&checkbox8&&&/td&
&td&&a href=&javascript:void(0)& onClick=&moveUp(this)&&上移&/a&&a href=&javascript:void(0)& onClick=&moveDown(this)&&下移&/a&&/td&
说是搜索架构方案,其实就是lucene.net的应用,公司庙小,人少,也就自己平时看看,以前做过一点例子,这样就被拉上去写架构方案了。 我这个懒惰的家伙,在网上疯狂的搜集搜索架构方面的东西,因为做做架构,暂时没写代码,每天就看人家博客,结果两个星期了才弄了个大概的草图,这不清明节 过后就要详细方案了,现在只能把我的草图分享一下,希望大家板砖伺候,闷在家里鼓捣比较郁闷啊,效率太低。
基于lucene的搜索方案
Lucene 简介
Lucene是apache的一个顶级开源项目,由java实现的全文检索引擎,能基于各种文档格式的全文索引和检索,包括word、pdf,不包括图形类。
Lucene.net 是版的lucene 是由java的lucene翻译过来的,也被apache列为开源项目对外发布,功能和java的基本一样,但是由于缺乏良好的技术支持和社区活跃度,目前已被apache放入孵化器
Lucene写入:源文件经过analyzer处理,包括分词,权重处理、生成document记录,写入存储器(硬盘或者内存)。
Lucene 读出:对搜索关键词进行analyzer处理,包括分词、权重、范围匹配处理.源码结构图如下:
具体流程如下图:
数据流图如下:
二、常用推荐引擎算法问题
采用基于数据挖掘的算法来实现推荐引擎是各大、SNS社区最为常用的方法,推荐引擎常用Content-Based 推荐算法及协同过 滤算法(Item-Based 、User-based)。但从实际应用来看,对于大部分中小型企业来说,要在电子商务系统完整采用以上算法还有很大的难 度。
1)、相对成熟、完整、现成的开源解决方案较少
粗略分来,目前与数据挖掘及推荐引擎相关的开源项目主要有如下几类:
数据挖掘相关:主要包括Weka、R-Project、Knime、RapidMiner、Orange 等
文本挖掘相关:主要包括OpenNLP、LingPipe、FreeLing、GATE 、Carrot2 等,具体可以参考LingPipe’s Competition
推荐引擎相关:主要包括Apache Mahout、Duine framework、Singular Value Decomposition (SVD) ,其他包可以参考Open Source Collaborative Filtering Written in Java
搜索引擎相关:Lucene、Solr、Sphinx、Hibernate Search等
2)、常用推荐引擎算法相对复杂,入门门槛较高
3)、常用推荐引擎算法性能较低,并不适合海量数据挖掘
以上这些包或算法,除了Lucene/Sor相对成熟外,大部分都还处于学术研究使用,并不能直接应用于规模的数据挖掘及推荐引擎引擎使用。
(以上都是基于java的,需要自己去研究实现,有很大难度)
备注:除了分类查找和主动搜索,推荐系统也是用户浏览商品的重要途径,能帮助用户发现类似并感兴趣的产品,增加商品的访问量,将访问者转化为购买者,引导用户购买。最终产生的价值是提升用户购物体验和用户粘度,提高订单量,如Amazon30%的订单来自推荐系统。
采用Lucene实现推荐引擎的优势
对很多众多的中小型而言,由于开发能力有限,如果有能够集成了搜索、推荐一体化的解决方案,这样的方案肯定大受欢迎。采用Lucene来实现推荐引擎具有如下优势:
1)、Lucene 入门门槛较低,大部分的站内搜索都采用了Lucene
2)、相对于协同过滤算法,Lucene性能较高
3)、Lucene对Text Mining、相似度计算等相关算法有很多现成方案
在开源的项目中,Mahout或者Duine Framework用于推荐引擎是相对完整的方案,尤其是Mahout 核心利用了Lucene,因此其架构很值得借鉴。只不过Mahout目前功能还不 是很完整,直接用其实现电子商务网站的推荐引擎尚不是很成熟。只不过从Mahout实现可以看出采用Lucene实现推荐引擎是一种可行方案。
3、采用Lucene实现推荐引擎需要解决的核心问题
Lucene对于Text Mining较为擅长,在contrib包中提供了MoreLikeThis功能,可以较为容易实现Content-Based的推荐,但对于涉及用户协 同过滤行为的结果(所谓的Relevance Feedback),Lucene目前并没有好的解决方案。需要在Lucene中内容相似算法中加入用户协同过滤行为对因素,将用户协同过滤行为结果转化 为Lucene所支持的模型。
推荐引擎的数据源
电子商务网站与推荐引擎相关典型的行为:
购买本商品的顾客还买过
浏览本商品的顾客还看过
浏览更多类似商品
喜欢此商品的人还喜欢
用户对此商品的平均打分
因此基于Lucene实现推荐引擎主要要处理如下两大类的数据
1)、内容相似度
例如:商品名称、作者/译者/制造商、商品类别、简介、评论、用户标签、系统标签
2)、用户协同行为相似度
例如:打标签、购买商品、点击流、搜索、推荐、收藏、打分、写评论、问答、页面停留时间、所在群组等等
5、实现方案
5.1、内容相似度 基于Lucene MoreLikeThis实现即可。
5.2、对用户协同行为的处理
1)、用户每一次协同行为都使用lucene来进行索引,每次行为一条记录
2)、索引记录中包含如下重要信息:
商品名、商品id、商品类别、商品简介、标签等重要特征值、用户关联行为的其他商品的特征元素、商品缩略图地址、协同行为类型(购买、点击、收藏、评分等)、Boost值(各协同行为在setBoost时候的权重值)
3)、对评分、收藏、点击等协同行为以商品特征值(标签、标题、概要信息)来表征
4)、不同的协同行为类型(例如购买、评分、点击)设置不同的值setBoost
5)、搜索时候采用Lucene MoreLikeThis算法,将用户协同转化为内容相似度
以上方案只是基于Lucene来实现推荐引擎最为简单的实现方案,方案的准确度及细化方案以后再细说。
更为精细的实现,可以参考Mahout的算法实现来优化。
其他搜索引擎开源工具推荐:Sphinx,目前是基于出自俄罗斯的开源全文搜索引擎软件Sphinx,单一索引最大可包含1亿条记 录,在1千万条记录情况下的查询速度为0.x秒(毫秒级)。Sphinx创建索引的速度为:创建100万条记录的索引只需3~4分钟,创建1000万条记 录的索引可以在50分钟内完成,而只包含最新10万条记录的增量索引,重建一次只需几十秒。
Sphinx 是一个基于 GPL 2 协议颁发的免费开源的全文搜索引擎.它是专门为更好的整合脚本语言和数据库而设计的.当前内置的数据源支持直接从连接到 的 My 或 PostgreSQL获取数据, 或者你可以使用 XML 通道结构(XML pipe mechanism , 一种基于 Sphinx 可识别的特殊xml格式的索引通道)
基于LAMP架构的应用很广泛,目前了解的商业应用有康盛的Discuz企业版。
三、手机之家的搜索方案(参考用)
手机之家目前的Lucene应用,采用的是Lucene 2.4.1 + JDK 1.6(64 bit)的组合,运行在8 CPU, 32G内存的机器上,数据量超过3300万条,原始数据文件超过14G,每天需要支持超过35万次的查询,高峰时期QPS超过20。单看这些数据可能并没 有大的亮点,但它的重建和更新都是自动化完成,而且两项任务可以同时运行,另一方面,在不影响服务可靠性的前提下,尽可能快地更新数据(如果两者发生冲 突,则优先保证可用性,延迟更新),其中的工作量还是非常大的。
在大规模的应用中,Lucene更适合用于狭义的“搜索”,而不应当负责数据的存储。我们看看Lucene的源代码也可以知道,Document和 Field的存储效率是不够好看的。手机之家的团队也发现了这一点,他们的办法是,用Lucene存放索引,用Memcache + Berkeley DB(Java Edition)负责存储。这样有两个好处,一是减轻了Lucene的数据规模,提高了程序的效率;另一方面,这套系统也可以提供某些类似SQL的查询功 能。实际上,Lucene本身似乎也注意到了这个问题,在Store中新增了一个db的选项,其实也是利用的Berkeley DB。
在大规模应用中,Cache是非常重要的。PPT中也提到,可以在程序提供服务之前,进行几次”预热“搜索,填充Searcher的Cache。据 我们(银杏搜索)的经验,也可以在应用程序中,再提供针对Document的Cache,这样对性能有较大的改善。Lucene自己似乎也注意到了这个问 题,在2.4版本中提供了Cache,并提供了一个LRU Cache实现。不过据我们测试,在极端情况下,这个Cache可能会突破大小限制,一路膨胀最后吃光内存,甚至从网络上找的许多LRU Cache实现在极端条件下都有可能出现这样的问题,最终自己写了一个LRU Cache,并修改多次,目前来看是稳定的。
在编写Java服务程序的时候,记得设置退出的钩子函数(RunTime.getRunTime.addShutdownHook)是一个非常好的 习惯。许多Java程序员都没有这种意识,或者有,也只是写一个finalize函数,结果程序非正常退出时,可能造成某些外部资源的状态不稳定。拿 Lucene来说,之前的IndexWriter是默认autoCommit的,这样每添加一条记录,就提交一次,好处是如果中断,则之前添加的记录都是 可用的,坏处则是,索引的速度非常低。在新版本中autoCommit默认为False,速度提升明显(我们测试的结果是,提高了大约8倍),但如果中途 异常退出,则前功尽弃。如果我们添加了退出的钩子函数,捕获到退出信号则自动调用writer.close()方法,就可以避免这个问题。
目前的Lucene是兼容JDK 1.4的,它的binary版本也是JDK1.4编译的,如果对性能要求比较高,可以自行下载Lucene Source Code,用更新版本的JDK编译出.jar文件,据我测试,速度大约有30%的提升。
XX网搜索方案
4.1 初步解决方案:
实现站内产品的分词搜索、推荐关键词和简单排序,定时自动更新,索引读写分离。
基于服务器的搜索压力大,用户的搜索体验不够有好,初步解决方案目标是解决服务器的搜索压力,实现初步的分词搜索,索引的自动定时维护。
4.1.1 数据库产品表分析:
产品分类扩展基表
品牌基表,品牌系列表基表
产品基表(主表)
产品基表的数据大概在8万条左右,占用空间40m左右,单表数据量相对来书还是比较小的。
Lucene索引程序:
通过lucene的索引程序将库里的数据读入流,然后写入lucene自定义的索引文件,这个索引文件不进行搜索操作,需要完成后替换到搜索索引。 在建立索引的过程中进行分词处理,分词组件采用eaglet开发的盘古分词组件(已基于apache开源协议开源,进一步功能需要自己二次开发)。
Lucene索引库:
基表的索引文件大概在100m左右,分为写入时的库和搜索时用的库,写入库完成后并入搜索库,考虑到新索引合覆盖就索引的瞬间可能产生的索引程序错误或者索引文件损坏,在覆盖的同时通过程序控制让搜索程序读取写索引里的文件。
搜索处理services:基于产品库的的搜索,如品牌,分类,价格区间。搜索程序依赖于接口,基于数据库的搜索和基于文件的搜索要按需要随时切换。搜索的同时需要利用分词组件分词处理,对分词后的结果进行检索,数据库检索的暂时不做分词处理。
查询处理:查询前台程序使用mvc,实现产品的分词高亮显示,按照类别分类查询,品牌分类查询,价格区间查询。
4.2 第二步关键词统计:
搜索关键词的搜集和搜索的联合处理,实现简单的搜索推荐功能。主要是对前台的搜索关键字进行统计分析,并与搜索的排序进行关联,关键词的处理和与主表的关联索引方案等初步处理完成后再做完整解决方案。
4.3 第三步优化完善:
实现索引文件的基于消息的增量自动更新,权重计算,推荐产品计算研究,实时搜索的研究。权重计算,需要重新开发自己的向量算法引擎,考虑当中。
实时搜索目前在学习当中。
4.3.1 权重计算
权重计算方法会将前台用户的统计数据和产品库进行关联开发一套天天网产品的权重排序计算方法,以下算法流程图只是一个构思。
权重计算设计
4.3.2 索引自动化更新
建立基于消息机制的一个索引更新与维护机制。
基于消息队列的索引生成程序
如今,一个只在桌面屏幕上好看是远远不够的,同时也要在平板电脑和智能手机中能够良好呈现。响应式的是指它能够适应客户端的屏幕尺寸, 自动响应客户端尺寸变化。在这篇文章中,我将向您展示如何通过3个简单的步骤轻松地使网站变成响应式(Responsive)。
您可能感兴趣的相关文章
当创建一个响应式网站,或让现有的网站变成响应式的,首先要关注的元素的布局。我在建立响应式的网站,总是先创建一个非响应的布局,页面宽度固定大小。如果非响应版本完成得非常不错,我再添加媒体查询()和响应式代码。这种操作方式更容易实现响应式特性,在同一时间专注于一个任务。
当你已经完成了无响应的网站,做的第一件事是在你的
页面,粘贴下面的代码到&haed&和&/head&标签之间。这将设置屏幕按1:1的尺寸显示,在 iPhone 和其他智能手机的浏览器提供网站全视图浏览,并允许用户缩放页面。
&meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"&
&meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"&
&meta name="HandheldFriendly" content="true"&
现在是时候添加一些媒体查询了。根据 W3C 网站,媒体查询由媒体类型和零个或多个媒体查询的条件表达式组成。通过使用媒体查询,外观呈现可以针对特定范围内的输出设备,而不需要改变内容本身。换句 话说,媒体查询让您的网站在各种各种显示器上看起来都很好,从小的智能手机到大的电脑屏幕等等。
媒体查询取决于你的网站布局,所以对我来说为您提供一个现成可以使用的代码片段有点困难。但是,下面的代码对于大多数网站都是一个很好的起点。在这个例子中,#primary 是主要内容区域,#secondary 是侧栏。
从代码中你可以看到,我定义了两种规格:首先有一个最大宽度为1060px,为平板电脑优化的横向显示。#primary 占在其父容器宽度的67%,#senondary 占30%,再加上3%的左外边距。 第二个规格是用于平板电脑和更小的屏幕尺寸。
由于智能手机的屏幕尺寸小,我决定给 #primary 设置100%的宽度,#secondary 也设置100%的宽度,他将在 #primary 下面。 正如我已经说过的,你可能必须要对这段代码位进行修改才能适应您的网站的具体需求。
@media screen and (max-width: 1060px) {
#primary { width:67%; }
#secondary { width:30%; margin-left:3%;}
@media screen and (max-width: 768px) {
#primary { width:100%; }
#secondary { width:100%; margin:0; border:none; }
完成以后,让我们看看你的布局是如何响应的。要做到这一点,我用这 Matt Kersley 创建的一款非常的。
一个响应式的布局是实现响应网站的第一步。现在,让我们把注意力集中在另外一个现代化网站非常重要的方面:媒体,如视频或图像。 下面的
代码将确保您的图像将永远不会大于他们的父容器,代码非常简单,适用于大多数网站。请注意,IE6 等旧的浏览器不支持 max-width 指令。
img { max-width: 100%; }
虽然上述技术是有效的,有时你可能需要有更多的图像控制权,例如根据客户端的显示大小,显示不同的图像。
发明的好方法。让我们看看 HTML:
&img src="image.jpg" data-src-600px="image-600px.jpg" data-src-800px="image-800px.jpg" alt=""&
正如你可以看到,我们使用 data-* 属性来存储替换图像的 URL。现在,让我们使用强大的
来为匹配 min-device-width 条件的媒体指定替换图像:
@media (min-device-width:600px) {
img[data-src-600px] {
content: attr(data-src-600px, url);
@media (min-device-width:800px) {
img[data-src-800px] {
content: attr(data-src-800px, url);
令人印象深刻,是不是?现在,让我们来看看另一个在今天的网站中非常重要的媒体——视频。由于大多数网站使用的视频来自第三方网站,我决定把重点放在
的弹性视频技术,这种技术可让您嵌入的响应式的视频。
&div class="video-container"&
&iframe src="" width="800" height="450" frameborder="0"&&/iframe&
.video-container {
position: relative;
padding-bottom: 56.25%;
padding-top: 30px;
height: 0;
overflow: hidden;
.video-container iframe,
.video-container object,
.video-container embed {
position: absolute;
width: 100%;
height: 100%;
在你的网站上应用了这些代码后,嵌入的视频也是响应式(Responsive)的了。
本教程的最后一步绝对非常重要,但往往被网站开发人员忽视——字体。到现在为止,大多数开发人员(包括我自己)使用像素来定义字体的大小。虽然 像素在普通网站使用是OK的,但是对于响应式网站来说应该有响应式的字体。事实上,一个响应式的字体大小应关联它的父容器的宽度,这样它才可以适应客户端 的屏幕。
规范引入了一个新的单位叫 rem,和 em 类相似,但相对于
元素来说, rem 更易于使用。
rem 是相对于 HTML 元素的,不要忘了重置 HTML 的字体大小:
html { font-size:100%; }
完成后,您可以定义响应式的字体大小,如下所示:
@media (min-width: 640px) { body {font-size:1} }
@media (min-width:960px) { body {font-size:1.2} }
@media (min-width:1100px) { body {font-size:1.5} }
请注意,旧浏览器不支持 rem 单元,所以不要忘了实现一个替代。
这就是今天的所有内容了,希望你会喜欢这个教程!记得推荐和分享啊!
备案信息:}

我要回帖

更多关于 asp.net 文件服务器 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信