[转载]SqlServer性能优化——Partition(管理分区)

mikel阅读(1024)

[转载]SqlServer性能优化——Partition(管理分区) – smjack – 博客园.

正如上一篇文章SqlServer 性能优化——Partition(创建分区)中所述,分区并不是一个一劳永逸的操作,对一张表做好分区仅仅是开始,接下来可能需要频繁的变更分 区,管理分区。

在企业管理器中,虽然有“管理分区”的菜单,里面的内容却可能与你的预想不同,这里并没有提供直接对分区进行操作的方 法,所以一些普通的操作,比如“增加分区”、“删除分区”之类的操作就需要通过脚 本实现了。

增加分区(Split Partition)

增加分区”事实上 就是将现有的分区分割开,基于此,在SQL Server中应用的是Split操作。在分离分区的时候,不仅仅要在 Partition Function上指定分割的分界点,同样需要在Partition Scheme上指定新分区应用的文件组:

--指定下一个分区应用文件组PRIMARY
ALTER PARTITION SCHEME [MyPartitionSchema] 
NEXT USED [PRIMARY]
--指定分区分界点为5000
ALTER PARTITION FUNCTION MyPartitionFunction()
SPLIT RANGE (5000)

需要注意的一点是,新增的分区中非聚簇索引的压缩模式会被置为None

删除分区(Merge Partition)

删除分区”同样可以认为是将原来分离的分区合并在一起,所以对应的是Merge操作,而且由于并没有新增的分 区,Partition Scheme并不需要改变:

ALTER PARTITION FUNCTION MyPartitionFunction ()
MERGE RANGE (5000)

切换分区(Switch Partition)

切换分区”可能是一个比看上去会应用的更频繁的操作,它的意义在于将一个分区的数据从一张表切换到另一张表 中。这里定义被切换分区的表为“源表”,被切换到的表为“目标表”,则执行切换操作的前提是:

  1. 源表和目标表拥有同样的表结构,即同样的字段、字段类型,同样的索引结构(聚簇和非聚簇),同样的压缩格式。 但不要求默认值约束一致(Default Constaint),也不要求目标表设置了和源表一样的自增长列。
  2. 源表如果有索引且分区,则其索引必须对齐
  3. 源表中被切换的分区范围必须包含于目标表或者目标表将要被切换到的分区范围。这里有如下几种情况:
    1. 将源表的源分区切换到目标表的目标分区中,则目标分区范围>=源分区;
    2. 将源表的源分区切换到目标表中(目标表未分区),则目标表没有设约束,或约束范围>=源分区;
    3. 将源表切换到目标表中(源表、目标表都未分区),则只要目标表没有设约束就可以了(虽然Switch是分区提出的操作,但一个没有分 区的表同样可以被看做一个大分区,所以可以对没有分区的表进行Switch操作)。
  4. 目标表或目标分区不能含有数据

下面的操作将源表的第二个分区切换到目标表的第二个分区中。

ALTER TABLE [STable] SWITCH PARTITION 2 TO [DTable] PARTITION 2

分区管理操作的性能

分割、合并以及切换分区是元数据上的操作而不是对数据的移动,所以操作的效率要比直接操作数据高很多。

  1. 对于分割分区,操作时间和被分割分区的数据量相关,数据越大则分割花费的时间会越长。
  2. 对于合并分区,如果将两个空的分区合并,自然不会耗什么时间;如果两个分区都有数据,则和分割分区一样,数据越大花费的时间越长;如果两个分 区中有一个没有数据,笔者的经验是如果有大数据量的分区在右(>分界值),则消耗的时间较短,如果有大数据量的分区在左(<分界值),则会消 耗较多的时间。
  3. 对于切换分区,即使是上千万级别的数据,也可以在不到1秒的时间完成分区的切换。所以虽然从表面上看,切 换分区和调用Select或者Insert语句移动数据的结果是一样的,但效率却是不可同日而语的。

查看分区信息

除了利用上文提到的通过“管理压缩”的方式查看某张表的分区信息之外,SQL Server还提供了一张系统表查看数据库中的分区情况:

  1. SYS.PARTITION_SCHEMES,数据库中所有分区方案的信息,包括对应的分区函数的ID。
  2. SYS.PARTITION_FUNCTIONS,数据库中所有分区函数的信息,包括分区数等信息。
  3. SYS.PARTITION_RANGE_VALUES,每个分区范围的信息,可以和SYS.PARTITION_FUNCTIONS联查。

比如可以通过如下的脚本,查出分区函数MyPartitionFunc的第一个分区的右边界:

SELECT value FROM sys.partition_range_values, sys.partition_functions 
WHERE sys.partition_functions.function_id = sys.partition_range_values.function_id 
AND sys.partition_functions.name = 'MyPartitionFunc' AND boundary_id = 1

还可以通过如下脚本,获取分区表中各分区的数据情况(行数,最大值,最小值):

SELECT 
    partition = $PARTITION.MyParitionFunc([ParitionDate])
    ,rows      = COUNT(*)
    ,min    = MIN([ParitionDate])
    ,max    = MAX([ParitionDate])
FROM [MyTable]
GROUP BY $PARTITION.MyParitionFunc([ParitionDate])
ORDER BY PARTITION

具体可以参照MSDN:从已分区表和索引中查询 数据和元数据

[转载]ASP.NET MVC + ADO.NET EF 项目实战(三):引入jQuery

mikel阅读(1019)

[转载]ASP.NET MVC + ADO.NET EF 项目实战(三):引入jQuery – Kanas.Net Blog – 博客园.

jQuery是一个重要的客户端框架,ASP.NET MVC默认的项目模板中就带了这个框架。掌握这个框架对于更好地编写ASP.NET MVC应用是非常重要的。事实上,网上有很多文章讲述如何在ASP.NET MVC项目中使用JQuery。例如以下文章就是讲关于jqGrid的:

Using jQuery Grid With ASP.NET MVC

Using jqGrid with ASP.NET MVC

另外,在CodePlex上就有很多项目帮助你更方便地应用JQuery,例如:

jQuery UI Extensions for ASP.NET MVC
jQuery Grid for ASP.NET MVC

但是,要更加方便地使用JQuery,仅仅知道以上还是不够的。

一:了解ASP.NET MVC的局限性

ASP.NET MVC有几处是我不太喜欢的,但是似乎也没有太好的解决方案。

第一件事情是冗余的Action。在一个Controller中每个Action的地位是平等的。但是很遗憾,一个带Form的View总是需要两 个Action,一个用来GET;另一个用来POST。而我觉得这两个Action是不平等的。用于Post的Action是依赖用于Get的 Action的。我告诉你,这两个Action其实是可以压缩成一个的。另一个完全可以放到System.Web.Mvc中,大家公用,通过传递一个或多 个delegate来处理。也就是说,形式上是一个,实际上仍然是两个。说这个问题的意图是想说明,我们可以把一个Ajax服务通过传递一个 delegate来实现单入口。

第二件事情是Script的Render。我希望任何TagBuilder都可以引用自己的Script,在Html的Head段最后写入。可惜 WebFormViewEngine的Render过程中按次序进行的。这意味着,直到整个页面写入Response你都没有机会去更正对Script的 引用。其实我以前写过关 于ASP.NET MVC中Script的管理,不过实话说非常丑陋。其实我也可以告诉你,这个是可以实现的。在View中可以定义一个特殊的标 签,当遇到这个标签的时候,将已经生成的Html写入缓存,清空HtmlWriter,再Render剩余的部分;最后再Render Script部分,最后交这三者合并,最后写入Response。说这个问题的意图是将客户端复杂化以后给应用带来的困难暴露出来。

任何一件事情,如果单独在服务端完成,这个比较好处理。如果单独在客户端完成,这个也好处理。如果需要客户端和服务端配合,就会弄出一些麻烦来。如 果你要在一个页面中显示一个jqGrid,你一共至少需要做四件事情:

1.在你的View中加入相关的引用,包括JavaScript和CSS;包括jQueryjQuery UI、jqGrid;包括相关的主题。

2.在你的View中适当的地方添加一个table标签,并加上id,用于表格的容器;再添加一个div标签,也加上id,用于分页器的容器。

3.加入相关的脚本,将网格与给定的标签进行绑定。其中需要定义网格的列信息。

4.加入一个数据获取的Action,或者XML格式数据或者JSON格式数据。

对于第1件事情,似乎不算麻烦。如果不是过于挑剔的话甚至没有什么问题。

对于第2件事情和第3件事情,就会有冲突了。可以假设这两件事情是两个人做的,但是他们必须确保他们所使用的id是一样的。一旦不一致,到底算谁的 责任?

对于第4件事情,就更麻烦了。因为那已经是服务端的事情了。不仅获取数据的Url要正确,连Column表和数据表也必须一致。一旦不一致,到底算 谁的责任?

通常对于要求一致的事情,由一个人做比较好。所以,无比聪明的程序员会通过一个 HtmlHelper来根据单一的定义统一生成。这的确是一个好的思路。所以,使用jQuery是需要进行专门包装的。

二:封装jQuery及其插件

如果要一个基于jQuery的Total Solution,还真有比较大的困难。jQuery本身的功能是相当有限的,其丰富的功能要依赖一大堆的插件。几乎每个插件要用在ASP.NET MVC中都需要包装一下。好在jQuery插件有一些约定,所以封装起来相对比较简单。在封装的时候有一点通常容易被忽略的是:作为独立于应用的封 装模块中是可以带自己的Controller的。举个例子来说:如果你封装了一个jQuery UI的主题管理器,主题管理的ModalDialog的Action,包括Get和Post你都是可以包装在一个单独的项目中的。如果你的控制器叫 jQueryController,两个Action分别是ShowThemes和SetTheme,那你可以通过以下代码来实现:

var currTheme = "";
$(document).ready(function() {
var doClick = function() {
$("#themeGallery td img.currentTheme").removeClass();
this.setAttribute("class", "currentTheme");
currTheme = this.getAttribute("title");
}
var doOK = function() {
$.post('/jQuery/SetTheme', { theme: currTheme }, function(data) {
$("#themeDialog").dialog("close");
eval(data);
});
return false;
}
var doCancel = function() {
$("#themeDialog").dialog("close");
}
$("#themeDialog").load("/jQuery/ShowThemes");
$("#ThemeButton").click(function() {
$("#themeGallery td img").click(doClick);
var dialogOpts = {
modal: true,
width: "662px",
height: "420px",
resizable: false,
buttons: { "OK": doOK, "Cancel": doCancel }
};
$("#themeDialog").dialog(dialogOpts);
});
});

这样,在任何需要设置主题的地方放一个<a>元素,id定义为ThemeButton即可。基于同样的原理,我们在封装jqGrid的时候, 可以使用如下定义的委托:

这样,单一的入口就是:

代码

public ActionResult Employees() { var model = new GridModel { Caption = "Employees", Loader = (ctx, page, rows, sidx, sord) => { ...... 实现数据访问 return new GridDataModel { Total = q0.Count(), Page = page, Records = q.Count(), Rows = q.ToArray() }; } }; model.Columns.Add(new ColumnModel { Name = "Id", Width = "80px", Align = ColumnAlign.Right, Caption = "Id", Index = "Id", IsKey = true, Sortable = true, Hidden = true }); ...... 加入其他列 return View(model); }

这样,在View中使用的扩展,生成的JavaScript代码中,Url是由应用无关的模块提供的,以View名称+View中Table的id为标 识,存贮在服务端。获取数据时提交该标识,找出数据加载器,从而实现异步数据加载。

jQuery的插件,除了jqGrid是必须封装的以外,jQuery UI也是必须要封装的。虽然很多人包括我在内,对jQuery UI有诸多不满,主要的不满都是基于功能,项目进度严重拖后于Roadmap上的承诺。另外,其代码质量也不如jQuery。不过,毕竟jQuery UI是jQuery官方的产品,受到很多插件依赖,所以,暂时没有可替代的。

此外,还有以下插件需要封装:

  • 带CheckBox的TreeView;
  • 导航Menu和上下文菜单;
  • 封装Google Maps API的gMap;
  • tinyMCE的jQuery插件;
  • 用于布局的Panel插件;
  • 动态Form插件。

三:如何避免拼凑脚本

在ASP.NET MVC中需要经常拼凑脚本,这一点也非常令人讨厌。讨厌的并不是“拼凑脚本”,而是“经常”。需要“经常”拼凑脚本的原因是服务端的内容是动态输出到客户 端的。如果通过某种固定的机制,智能地将服务端的单一代码自动生成为客户端内容那将是非常令人高兴的事情。换句话说,你在服务端直接写C#代码,然后有一 个专门的工具将这些代码翻译成客户端代码。

我研究过JavaScript#, 发现这条路完全行不通。首先,要么翻译源码、要么翻译编译后的代码,但必须是整个文件地翻译,而不能是某个代码段。其次,JavaScript#必须依赖 一整套程序集,而这些程序集是特有的。

我建立了一个新的项目,叫jQuery#,基于一套专门的库,然后在运行时以较小的代价来实现反编译,并翻译成JavaScript代码。如果仅仅 只是生成jQuery所需要的客户端代码是非常简单的,jQuery API毕竟比较少,每个插件的API也都相当有限。麻烦的是需要插入大量的客户端Event Handler。这才是难点所在。按我的计划,今年6月初会发布jQuery#的第一个版本。 目标是可以这样写View:

代码

[转载]asp.net mvc视图篇(二)

mikel阅读(966)

[转载]asp.net mvc(二) – ASP.NET2.0 – 博客园.

MVC中的ViewData
View在MVC模式中与用户进行最直接的接触,负责数据的呈现。注意:view只是负责数据的呈现,我们要尽量让view中不涉及业务逻辑的处理。既然 View与后台代码是相分离的,但View和Controller是如何联系在一起的呢,答案就是ViewData。

ASP.NET MVC默认使用WebForm来作为view。新建的aspx页面继承自ViewPage,所有的aspx页面都必须继承自ViewPage。我们再看一 下ViewPage的部分代码:

public class ViewPage : Page, IViewDataContainer

我们使用传统的ASP.NET开发时,经常会为了开发的需要,会写一个类似PageBase类,例如会把部分比较通用的方法写入基类。同样在MVC中,我 们也可以这样做。

第一:创建一个ViewPage<T>类:这个类主要是完成一个继承功能,对 MvcContrib.FluentHtml.ModelViewPage,MvcContrib.FluentHtml.ModelViewUserControl 的继承,实现System.Web.Mvc.ViewPage的功能。还有一个非常重要的作用就是把所有的扩展方法都体现在这个类中。类中using GuestBook.Common.HtmlHelpers;可以说明。

using GuestBook.Common.HtmlHelpers;
namespace GuestBook.Web
{
public class ViewPage<T> : MvcContrib.FluentHtml.ModelViewPage<T> where T : class
{
public ViewPage()
{

}
}

public class ViewUserControl<T> : MvcContrib.FluentHtml.ModelViewUserControl<T> where T : class
{
public ViewUserControl()
{

}
}
}

第二:对MVC进行扩展。例如对Html的扩展,我们在做增删改查类似操作时,当用户提 交后一般都会根据系统处理结果显示一段提示文字给用户。

1:创建扩展类:HtmlHelperExtensions,主要包含两个方法,一个是操作成功后的处理方法,另一个则是失败后的处理结果。

public static class HtmlHelperExtensions
{
public static string ErrorBox(this HtmlHelper htmlHelper, ViewDataBase  errorViewData)
{
if (errorViewData.ErrorMessage == null) return string.Empty;

HtmlTextWriter writer = new HtmlTextWriter(new StringWriter());

writer.AddAttribute(class, error);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.Write(errorViewData.ErrorMessage);
writer.RenderEndTag();
return writer.InnerWriter.ToString();
}

public static string MessageBox(this HtmlHelper htmlHelper, ViewDataBase messageViewData)
{
if (messageViewData.Message == null) return string.Empty;

HtmlTextWriter writer = new HtmlTextWriter(new StringWriter());

writer.AddAttribute(class, message);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.Write(messageViewData.Message);
writer.RenderEndTag();
return writer.InnerWriter.ToString();
}
}

2:我们看到上面代码中有一个类:ViewDataBase,这个类是所有ViewData的基类,里面主要包含两个字段:1,一般提示内容;2,当发生 异常时的提示内容。

public class ViewDataBase
{
public string Message { get; set; }
public string ErrorMessage { get; set; }

public ViewDataBase WithErrorMessage(string errorMessage)
{
this.ErrorMessage = errorMessage;
return this;
}
public ViewDataBase WithMessage(string message)
{
this.Message = message;
return this;
}
}

说明:我们在留言所使用的ViewData类需要继承ViewDataBase

public class GuestBookInfo : ViewDataBase
{
public string sTitle
{
get; set; }
public string sContent
{
get; set; }
public int ID
{
get; set; }
}

3:在View中调用扩展方法:

1>:页面引用代码的更改:
默认的:Inherits=”System.Web.Mvc.ViewPage<GuestBook.Common .Models.GuestBookInfo>”
扩展后的:Inherits=”GuestBook.Web.ViewPage< GuestBook.Common.Models.GuestBookInfo>”

2>:页面调用代码。

<% =Html.MessageBox (Model)%>

总结:通过扩展ViewPage和ViewData实现了对Html的扩展, 使得ViewData的功能更加灵活,好用。
注:本文参考Suteki项目。

[转载]asp.net mvc教程基础篇(一) - ASP.NET2.0

mikel阅读(1019)

[转载]asp.net mvc(一) – ASP.NET2.0 – 博客园.

这些天开始学习ASP.NET mvc,用传统的ASP.NET已经快四的年了,刚开始接触ASP.NET mvc确认感觉有点不适应,主要体现在ASP.NET MVC的实现上。

问题一:要想学习asp.net mvc,我个人觉的最重要的一步是知道mvc路由机制,传统的asp.net程序要想访问一个页面,都是根据页面路径来访问,但MVC并不能直接访问 aspx页面。

问题二:理解MVC三部分的含义和用法。当我们创建一个asp.net mvc应用程序时,系统会默认生成三个文件夹:
1:Controllers,对应MVC中的C,主要是处理所有请求与做出对应的响应;
2:Models,对应MVC中的M,相当时我们平时创建工程中的实体工程,只不过在MVC中它充当了存放数据模型的作用;
3:Views,对应MVC中的V,这里就是存放用户访问的页面文件,但是这个文件不能在浏览器中根据路径访问。
对于系统生成的 ASP.NET MVC项目,我对其做了如下扩展:

扩展点一:系统之所以在web工程中直接创建了三个文件夹,是为了更加直观的体现MVC模式,真正项目中我们需要把它们分开。

扩展点二:MVC中重要的路由处理,默认情况是在Global.asax文件中,我们也可以把这块内容独立出来。

扩展点三:把Controller类和业务逻辑分离,这里可以采用Repository模式。

案例DEMO:创建一个简单的留言簿的项目,数据存储采用SQL,本想用linq to entity,但总觉的这部分还相关不完善,且性能存在问题,故使用传统ado.net实现数据存储。下面是这个项目的分层。

1:GuestBook.Web,页面表示层  ,MVC中的V。

2:GuestBook.MVC.Controller,存放项目所有的Controller,MVC中的C。我们知道Controller有两个作用: 第一,处理请求;第二,做出对应的响应。第二点就是我们平时理解的后台功能实现,例如数据的增删改查等。我们可以把这部分功能与Controller分 离,即所有的业务逻辑都写在业务逻辑层,不直接依赖Controller,我们可以进一步把这些功能点抽象出来,让Controller依赖一个公共的接 口。这个思想我之前的一篇文章有点异曲同工之处:对增删改查用面向对象进行包装

首先:创建一个Repository接口:IRepository.cs,里面包含些常见数据处理 操作方法:这个接口是一个泛型接口,以实现所有实体类的通用性。

public interface IRepository<T>
{
List
<T> FindAllInfo();
T GetInfo(T model);
bool Add(T model);
bool Delete(T model);
bool Edit(T model);
}

然后:实现一条留言的数据处理:

public List<GuestBookInfo> FindAllInfo()
{
string SQL = select * from GuestBook;

List<GuestBookInfo> list = new List<GuestBookInfo>();
using(SQLDataReader dr=SqlHelper .ExecuteReader (conn ,CommandType .Text ,sql ))
{
while (dr.Read())
{
GuestBookInfo model
= new GuestBookInfo();
model.ID
= int.Parse (dr[ID].ToString());
model.sTitle
= dr[sTitle].ToString();
model.sContent
= dr[sContent].ToString();
list.Add(model);
}

}
return list  ;
}
public GuestBookInfo GetInfo(GuestBookInfo model)
{
string sql = select * from GuestBook where ID=+model.ID .ToString ();
using (SqlDataReader dr = SqlHelper.ExecuteReader(conn, CommandType.Text, sql))
{
if (dr.Read())
{
model.ID
= int.Parse(dr[ID].ToString());
model.sTitle
= dr[sTitle].ToString();
model.sContent
= dr[sContent].ToString();

}

}
return model ;
}
public bool Add(GuestBookInfo model)
{
string sql = insert into GuestBook (sTitle,sContent) values (‘ + model.sTitle + ‘,’ + model.sContent + ‘);
int i = SqlHelper.ExecuteNonQuery(conn, CommandType.Text, sql);
if (i > 0)
{
return true; }
return false ;
}
public bool Delete(GuestBookInfo model)
{
string sql = delete GuestBook where ID= + model.ID.ToString();
int i = SqlHelper.ExecuteNonQuery(conn, CommandType.Text, sql);
if (i > 0)
{
return true; }
return false;
}
public bool Edit(GuestBookInfo model)
{
string sql = update GuestBook set sTitle=’ + model.sTitle + ‘,sContent=’ + model.sContent + ‘ where ID= + model.ID.ToString();
int i = SqlHelper.ExecuteNonQuery(conn, CommandType.Text, sql);
if (i > 0)
{
return true; }
return false;
}

其实:Controller依赖IRepository接口。

public class GuestBookController : System.Web.Mvc.Controller
{
IRepository
<GuestBookInfo> inter = new BLL_GuestBook();
public ActionResult Index()
{
var models
= inter.FindAllInfo();
return View(Index, models);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(GuestBookInfo model)
{

inter.Add(model );
return RedirectToAction(Index);
}
public ActionResult Create()
{
GuestBookInfo model
= new GuestBookInfo();
return View(model );
}
public ActionResult Details(int id)
{

GuestBookInfo model=new GuestBookInfo ();
model .ID
=id;
model
=inter.GetInfo (model );
if (string .IsNullOrEmpty (model.sTitle ))
{
return View(NotFound); }
else
{
return View(Details,model );
}
}
public ActionResult Edit(int id)
{
GuestBookInfo model
= new GuestBookInfo();
model.ID
= id;
model
= inter.GetInfo(model);
if (string.IsNullOrEmpty(model.sTitle))
{
return View(NotFound); }
else
{
return View(Edit, model);
}
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues)
{
GuestBookInfo model
= new GuestBookInfo();
model.ID
= id;
model
= inter.GetInfo(model);
UpdateModel(model );
inter.Edit(model);
return RedirectToAction(Index);
}
public ActionResult Delete(int id)
{
GuestBookInfo model
= new GuestBookInfo();
model.ID
= id;
model
= inter.GetInfo(model);
if (model == null)
return View(NotFound);
inter.Delete(model);
return RedirectToAction(Index);
}

}

3:GuestBook.Model,MVC中的M。

4:GuestBook.RouteManager,路由管理项目,把路由处理从Global.asax中分离开。我们创建一个新 类:MyMvcAppliation.cs

public class MyMvcAppliation:HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(
{resource}.axd/{*pathInfo});

routes.MapRoute(
Default, // Route name
{controller}/{action}/{id}, // URL with parameters
new { controller = Home, action = Index, id = “” }, // Parameter defaults
new string[] { GuestBook.MVC.Controller }
);

}

protected void Application_Start()
{
ControllerBuilder.Current.DefaultNamespaces.Add(
GuestBook.MVC.Controller);
RegisterRoutes(RouteTable.Routes);
}
}

5:GuestBook.Data,数据处理工具类,例如SqlHelp等等。

6:GuestBook.DAL,数据处理层。

7:GuestBook.BLL,业务逻辑层。

8:GuestBook.MyInterface,相关接口,本项目中包含Repository模式中的接口类。

这篇文章主要是探讨了MVC项目的分层以及部分扩展,欢迎大家提出更好的想法,在后面的文章中,我会总结点MVC与传统asp.net不同点,这样理解 asp.net mvc就不那么麻烦了。

[原创]ASP.NET MVC实现伪静态

mikel阅读(1256)

最近在研究搜索引擎优化(SEO),使用工具分析了网站的发现ASP.NET MVC的网站的URL格式:Http://www.mikel.cn/Home/Index,该工具抓取不了?于是想到伪静态成:Http://www.mikel.cn/Home/Index.html,于是修改了Global.asax如下(注意:一定要将Default路由配置放在最后,否则伪静态的代码不能显示,因为MVC是从上到下遍历routeDictionary的,遇到匹配的url就行解析):


public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Html",                                              // Route name
"{controller}/{action}.html",                           // URL with parameters
new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
);
routes.MapRoute(
"Default",                                              // Route name
"{controller}/{action}/{id}",                           // URL with parameters
new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
);


}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}

[转载]ASP.NET MVC Tip #39 – 在ASP.NET MVC中使用分布式缓存

mikel阅读(1175)

[转载][翻译]ASP.NET MVC Tip #39 – 在ASP.NET MVC中使用分布式缓存 – 紫色永恒 – 博客园.

原文地址:ASP.NET MVC Tip #39 – Use the Velocity Distributed Cache
原文作者:Stephen Walther

本文地址:[翻 译]ASP.NET MVC Tip #39 – 在ASP.NET MVC中使用分布式缓存
译者:紫色永恒

在这篇文章中,我将带您体验如何使用Velocity分布式缓存提高ASP.NET MVC应用程序性能。同时我也会向您说明如何使用Velocity作为会话状态提供者。

使用缓存是提高ASP.NET MVC应用程序性能的最好方式。我们知道,在ASP.NET MVC中最耗时的操作非数据库的读取莫数,那么,尽量的避免读取数据库显然成为了提升性能最好的办法。缓存可以将经常访问的数据保持在内存中从而大幅减少 数据库的读取。

由于ASP.NET MVC是ASP.NET框架的一部分,这使得我们在ASP.NET MVC中仍然可以使用标准的System.Web.Caching.Cache对象。标准的ASP.NET缓存对象很好很强大,我们可以通过它指定某些数 据在特定的时间内被缓存,建立缓存项与文件系统或者数据库表之间的依赖,甚至可以在不同的缓存向之间建立复杂的依赖链。也就是说,我们可以通过 ASP.NET缓存对象作很多很多很多…的事情。

但是标准的ASP.NET缓存对象的也有一些限制,比如它只能和我们的web应用程序运行在同一个进程,也就是说它并不是分布式的。不同 应用程序间的缓存并不能共享,我们只能为每个应用程序构造相同的缓存。

在单一服务器架构下,传统的ASP.NET缓存在web应用程序中工作得非常好。但是当我们想使用服务器组等集群架构时,问题就出现了。 比如我们有一个需要处理成十亿上百亿用户访问的web应用程序(译者按:这有点夸张吧…),或者当我们的服务器出现故障时我们并不想重新从缓存加载数据。 当我们欲在多组web服务器上共享缓存是,分布式缓存应运而生。

在这篇文章中我将向您解释如何在ASP.NET MVC应用程序中使用代号为Velocity的微软分布式缓存策略。建立和使用Velocity十分的简单,从标准的ASP.NET缓存转换至 Velocity分布式缓存也不会使您痛苦万分。

同时我也会说明如何将Velocity作为会话状态提供者(session state provider).Velocity允许您在MVC应用程序服务器群组中使用ASP.NET会话状态。您可以通过Velocity在分布式缓存中存储会 话状态。

Velocity的安装和配置

当你看到这篇文章的时候,Velocity和ASP.NET MVC一样还没有正式发布,仍然处于Preview的状态。不过你现在就可以下载Velocity来一个提前体验。

下面的地址中包括所有有关Velocity相关信息。

http://msdn.microsoft.com/en-us/data/cc655792.aspx

您可以通过这个页面的链接下载Velocity。

安装Velocity十分简单,您需要将它安装在您想使用分布式缓存的服务器上(缓存服务器),缓存会从这些服务器上自动的组织起来。

在将Velocity安装在缓存服务器上之前,您需要在您的网络中创建一个共享以便所有的缓存服务器都能够访问它。这个文件共享将包括 缓存配置文件(ClusterConfig.xml)。这个文件是一个标准的XML文件,它用来对分布式缓存进行相关配置。

在Beta版中,您必须在这个文件共享上给予Everyone帐号读写权限。右键单击文件夹,选择属性菜单然后选择安全选项单。点击编辑 按钮,选择添加,输入Everyone然后点击OK即可。这里要确认Everyone帐号具有读写权限。

当您运行Velocity(图1)的安装程序时,您将被要求填入如下信息:

· Cluster Configuration Share – 这里的路径指向您刚刚建立的文件共享.

· Cluster Name – 组名称. 按照您的意愿起个名字吧 (比如, MyCacheCluster).

· Cluster Size – 您期望在这个组中使用的缓存服务器的数量.

· Service Port Number – 应用程序和缓存服务器通信的端口(注意如果您有防火墙,您需要在防火墙中解除对该端口的屏蔽)

· Cluster Port Number — 缓存服务器之间通信的端口。Velocity将使用这个端口号。(同样需要在防火墙中解除屏蔽)

· Max Server Memory – 允许Velocity在这台服务器上使用的内存最大值。

图 1 – 安装 Velocity

clip_image002

您必须在每台安装Velocity后的服务器上配置防火墙的例外规则,否则Velocity的通讯将被阻止。您可以为 DsitributedCache.exe进程或者为每个端口号(22233端口,22234端口和22235端口等)建立一个防火墙例外规则。

使用 Velocity 管理工具

您可以通过Velocity Administration Tool命令行工具管理Velocity(图2)

图2 2 – Velocity 管理工具

clip_image004

Velocity管理工具支持如下命令(这些命令十分有用):

· start cluster – 为组中的每一台缓存服务器启动Velocity。

· stop cluster – 与上一条正相反。

· create cache – 建立并命名一个新缓存。

· delete cache – 删除一个已存在的缓存。

· list host – 列出组中的所有缓存服务器

· list cache – 类出组中的所有缓存配置。

· show hoststats <cache server>:<cache port>– 列出指定缓存服务器上的统计信息。

安装Velocity后您需要做的第一件事就是使用如下命令启动缓存组:

start cluster

ASP.NET MVC应用程序中使用Velocity

完成Velocity的建立和启动后,您可以建立一个使用Velocity缓存的ASP.NET MVC应用程序。为了在项目中使用Velocity,您需要添加对如下程序集的引用:

· CacheBaseLibrary.dll

· ClientLibrary.dll

您可以在Program Files\Microsoft Distributed Cache文件夹或任意一台安装了Velocity的机器上找到这些dll.您也可以将这些dll从缓存服务器上拷贝到您的开发环境中。

同时,您需要修改您的ASP.NET MVC应用程序的web.config文件。在<configSections>节点中添加如下子节点:

  1. <section name=“dcacheClient”
  2. type=“System.Configuration.IgnoreSectionHandler”
  3. allowLocation=“true” allowDefinition=“Everywhere”/>

接下来,在配置文件的任意位置添加如下节点:

  1. <dcacheClient deployment=“simple” localCache=“false”>
  2. <hosts>
  3. <!–List of hosts –>
  4. <host name=“localhost”
  5. cachePort=“22233”
  6. cacheHostName=“DistributedCacheService” />
  7. </hosts>
  8. </dcacheClient>

在这里您可能需要改动一下host节点下name为“localhost”的值。换句话说,MVC应用将在本地使用缓存服务。如果缓存服 务器不在您的本地环境中,您需要更改这个值,以使它指向正确的服务器。

增加缓存项及从缓存中检索指定项

分布式缓存的使用方式和普通的ASP.NET缓存使用方式十分相似。您可以使用如下的方法在分布式缓存中增加,获取和移除缓存项:

· Add() – 向分布式缓存中添加一项.如果此项的键已经存在,那么将会抛出一个异常。

· Get() – 按照指定的键从分布式缓存中获取缓存项。

· Put () – 向分布式缓存中添加一项.如果此项的键已经存在,那么它将会被替换。

· Remove() – 从分布式缓存中移除一个存在的缓存项。

让我们看看示例,代码段1中的controller使用分布式缓存来缓存影视数据库中的记录。

代码段 1 – HomeController.cs

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.Mvc;
  6. using System.Data.Caching;
  7. using Tip39.Models;
  8. using System.Diagnostics;
  9. using System.Web.Configuration;
  10. using System.Web.Hosting;
  11. using System.Data.Linq.Mapping;
  12. using System.Data.Linq;
  13. namespace Tip39.Controllers
  14. {
  15. [HandleError]
  16. public class HomeController : Controller
  17. {
  18. private DataContext _dataContext;
  19. private Table<Movie> _table;
  20. public HomeController()
  21. {
  22. // Get connection string
  23. var conString = WebConfigurationManager.ConnectionStrings[“Movies”].ConnectionString;
  24. // Get XML mapping source
  25. var url = HostingEnvironment.MapPath(“~/Models/Movie.xml”);
  26. var xmlMap = XmlMappingSource.FromUrl(url);
  27. // Create data context
  28. _dataContext = new DataContext(conString, xmlMap);
  29. _table = _dataContext.GetTable<Movie>();
  30. }
  31. public ActionResult Index()
  32. {
  33. // Try to get movies from cache
  34. var factory = new CacheFactory();
  35. var cache = factory.GetCache(“default”);
  36. var movies = (List<Movie>)cache.Get(“movies”);
  37. // If fail, get movies from db
  38. if (movies != null)
  39. {
  40. Debug.WriteLine(“Got movies from cache”);
  41. }
  42. else
  43. {
  44. movies = (from m in _table select m).ToList();
  45. cache.Put(“movies”, movies);
  46. Debug.WriteLine(“Got movies from db”);
  47. }
  48. // Display movies in view
  49. return View(“Index”, movies);
  50. }
  51. }
  52. }

Index()方法返回数据库中所有的行。首先,这个方法试图从缓存中获取数据,如果获取失败,则从数据库中获取数据并在缓存中添加一个 缓存项。

Index()方法点用Debug.WriteLine()来向Visual Studio控制台窗口显示消息。无论数据是从缓存还是从数据库取出的,控制台窗口都会忠实的体现出来。(图2)

图 2 – 使用Visual Studio 控制台跟踪缓存行为

clip_image006

您也可以使用Velocity管理工具监视分布式缓存。执行如下命令来显示分布式缓存的统计信息:

show hoststats server name:22233

将“server name”替换成您的缓存服务器的名称(很可惜,在这里不能使用“localhost”)。执行这条命令后,您将得到有关分布式缓存非常全面的统计信息, 如缓存项的数量,缓存大小,缓存被调用的次数等等(图3)。

图 3 – 缓存统计

clip_image008

代码段1中的Index()方法首先实例化了一个CacheFactory类。这个类用来从分布式缓存中提取特定的缓存项。

Velocity可以管理不同名称的缓存。您可以将不同的数据通过不同的缓存组织起来。想象一下,每个具有特定名字的缓存就像一个分布式 数据库。如果您是第一次安装Velocity,您会得到一个默认的叫做“default”的缓存项。在代码段1中,CacheFactory就是用来从获 取这个被命名为“default”的缓存项的。

接着,Cache.Get()方法用来从从缓存中获取电影数据库中的记录。当Index()方法第一次被执行时,Get()方法不会返回 任何值。因为分布式缓存中并不存在任何数据。

如果Get()方法从缓存中获取数据记录失败,则会真正的从数据库中获取记录。然后这些记录在Put()方法的帮助下添加到分布式缓存 中。

这里要注意一下,利用缓存存储或者获取的数据都是无类型的对象。在您使用这些对象之前,您必须将它们转换为特定的类型。典型的情况是比如 您将分布式缓存应用于产品信息,您需要建立一个强类型的产品信息类。

另外您还需要注意的是,当Visual Studio项目重新启动或编译后分布式缓存仍然有效。如果您需要在开发时随时清除缓存,请在Velocity管理工具中使用这些命令

stop cluster

start cluster

我们可以缓存哪些数据?

您可以将任何可序列化的类型添加到缓存中。也就是说您可以将所有标记了[Serializable]特性的类型添加到缓存中去。(这个类 所有依赖的类型也必须是可序列化的) 代码段1中的Home controller使用LINQ to SQL来从数据库中获取数据记录。当在分布式缓存中应用LINQ to SQL时您必须注意,取决于您创建LINQ to SQL类的方式,这列类有可能不允许被序列化。

如果您使用对象关系设计器建立您的LINQ to SQL类则将导致所有的类型不可以被序列化。为了解决这个问题,我选择手动创建LINQ to SQL类。请看代码段2中的Movie.cs类。

代码段 2 – Models\Movie.cs

  1. using System;
  2. namespace Tip39.Models
  3. {
  4. [Serializable]
  5. public class Movie
  6. {
  7. public int Id { getset; }
  8. public string Title { getset; }
  9. public string Director { getset; }
  10. public DateTime DateReleased { getset; }
  11. }
  12. }

注意,Movie类被包含[Serializable]特性

代码段3中的XML mapping文件被用于HomeController被构造时初始化LINQ to SQL数据上下文。这个XML mapping文件将Movie类和它的属性映射到Movies表和字段。

代码段 3 – Models\Movie.xml

  1. <?xml version=“1.0” encoding=“utf-8” ?>
  2. <Database Name=“MoviesDB” xmlns=“http://schemas.microsoft.com/linqtosql/mapping/2007”>
  3. <Table Name=“Movies” Member=“Tip39.Models.Movie”>
  4. <Type Name=“Tip39.Models.Movie”>
  5. <Column Name=“Id” Member=“Id” IsPrimaryKey=“true” IsDbGenerated=“true”/>
  6. <Column Name=“Title” Member=“Title” />
  7. <Column Name=“Director” Member=“Director” />
  8. <Column Name=“DateReleased” Member=“DateReleased” />
  9. </Type>
  10. </Table>
  11. </Database>

在分布式缓存中使用LINQ to SQL还有另外一点需要注意。您应该清楚一个LINQ to SQL请求直到您真正使用其查询结果时才会真正的执行。确认您向缓存中添加的是LINQ to SQL请求的结果而不是请求本身。

代码段1中的Index()方法将电影记录添加到缓存中,如下

  1. movies = (from m in _table select m).ToList();
  2. cache.Put(“movies”, movies);

注意LINQ to SQL中调用的ToList()方法,它使得请求真正被执行并且将数据添加到电影实体中。否则,它只是个LINQ to SQL请求表达式而已。

将Velocity作为会话状态提供者

默认情况下,ASP.NET框架将会话状态储存在与您的ASP.NET MVC应用程序相同的进程中,这种方式提供了最卓越的性能。然而这种方式有一个很大的缺点:您无法再服务器集群中应用会话状态。

ASP.NET框架提供两种改变在进程中存储会话状态的方式。您可以将会话状态信息储存在您网络中的状态服务器上或者将它们储存在SQL Server数据库中。这两种方式都可以让您实现建立一个会话状态服务中心。得益于此,您可以很好的在服务器集群中控制ASP.NET MVC应用。

不过无论是将会话状态储存在状态服务器上还是SQL Server数据库中,这里都存在一些缺点。其一,使用状态服务器的方式容错性不是很好;其二,使用SQL Server的话,则速度不会很快(您的MVC应用中用户的每个请求都将导致执行一个从数据库中读取会话状态的操作)。
Velocity为您提供了一种更好的进程外存储会话状态的方法。您可以将会话状态信息存储在Velocity分布式缓存当中。而无论是容错性还是相应速 度,都要比以上说的两种方法好的多。

假如您想计算并保持一个特定用户访问页面的次数(图4),您可以将访问次数存储在会话状态中。代码段4中的Counter controller将访问次数保存在以“count”命名的会话状态中。图 4 — 计算页面访问次数

image

代码段 4 – CounterController.cs

  1. using System.Web.Mvc;
  2. namespace Tip39.Controllers
  3. {
  4. public class CounterController : Controller
  5. {
  6. public ActionResult Index()
  7. {
  8. var count = (int?)Session[“count”] ?? 0;
  9. count++;
  10. Session[“count”] = count;
  11. return View(“Index”, count);
  12. }
  13. }
  14. }

默认情况下,Counter controller会将会话状态存储在进程内。如果您希望使用Velocity来存储会话状态的话,您需要修改web.config文件。 将<sessionState>信息添加到<system.web>节点中,如代码段5

代码段 5 – Session Configuration for Velocity

  1. <sessionState mode=“Custom” customProvider=“dcache”>
  2. <providers>
  3. <add
  4. name=“dcache”
  5. type=“System.Data.Caching.SessionStoreProvider”/>
  6. </providers>
  7. </sessionState>

这样您的会话状态就会被储存在分布式缓存当中了。您可以通过Velocity管理工具确认Counter controller是否使用了分布式缓存。执行下面的命令,结果会显示出分布式缓存被访问的次数:

show hoststats server name:22233

Counter controller的Index()方法每执行一次,总的请求统计将会增加两次:一次是读取会话状态、一次是写入会话状态。看图5

图 5 – 查看总请求数

clip_image010
需要注意的是,您根本不用改动现有程序中的任何代码就可以将会话状态存储在Velocity分布式缓存中。您可以通过修改web.config 来更改会话状态的存储方式。这意味着您可以随时根据需要在几种存储方式中自由转换。

总结

在这篇文章中,我简短的向您介绍了Velocity分布式缓存,并示范了如何在ASP.NET MVC应用程序中通过分布式缓存在多请求的情况下存储和获取数据。另外我还向您展示了如何通过Velocity分布式缓存存储会话状态的种种优点。
当然,在这里我仅仅是为您说明了Velocity的最基本的应用。Velocity还有很多的特点我在这篇文章并没有谈及。比如Velocity提供了一 个锁定机制。您可以选择使用乐观或悲观锁定来防止并发攻击。您还可以使用Velocity来标记和搜索缓存中的缓存项。
尽管这只是一个简短的介绍,但我仍希望我已经向您提供了足够的细节来帮助您尝试在ASP.NET MVC应用程序中使用Velocity。

作者:紫色永恒

出处:http://024hi.cnblogs.com/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利

[转载].NET应用访问数据库之数据库的开销问题

mikel阅读(1064)

[转载].NET应用访问数据库之数据库的开销问题 – 软件设计开发 – 博客园.

拿今天的一个例子说话吧,那就表中存放的是全国的地域信息,表结构如下:

首先用代码生成器和存储过程生成器生成基本表的操作,推荐两个工具:动软.Net代码生成器- 全功能的三层架构.Net代码生成器和codeplex上 面的一个存储过程生成工具Stored Procedure Generator (for SQL Server 2000/2005

现在提供了一个方法,可以获取顶级和二级地域的信息,最开始的做法是先获取顶级的地域信息,然后循环顶级地域信息,获取它的子节点。

代码

上面的做法,可以实现功能,最后测试获取一级和二级的地域信息,花费时间5秒左右,不说能接受吧,起码可以忍受。HttpWatch: An HTTP Viewer and HTTP Sniffer for IE and Firefox 这个工具可以查看浏览器获取数据的时间。

后面又写了一个方法,可以根据地域编号和想要获取的层级数目,获取指定地域下面的N层地域。和 上面差不多,完成后一次是,一次获取上海下面的二级花费10秒,获取三级50秒。这好像就不能忍受了吧。

然后进行优化,代码如下,变成一次获取二级的数据,然后用C#代码来生成层级关系。

代码

List<KB.DSN.Entity.District> districtList = new List<KB.DSN.Entity.District>();
KB.DSN.BusinessAccess.District dictrictBll
= new KB.DSN.BusinessAccess.District();
districtList
= dictrictBll.GetEntityList(string.Format( {0} {1}, Settings.Get_Top_And_Second_District_Where,
Settings.District_Order_By));
var top
= from c in districtList
where c.DisFatherCode.Trim() == 0
select c;
var second
= from c in districtList
where c.DisFatherCode.Trim() != 0
select c;

foreach (KB.DSN.Entity.District dis in top)
{
var se
= from c in second
where c.DisFatherCode == dis.DisCode
select c;
dis.ChildrenDis
= se as List<KB.DSN.Entity.District>;
}
return top as List<KB.DSN.Entity.District>;

组合的时候用到了LINQ的技术,好东西啊。推荐教程:LINQ体验系列文章导航

效率大增,变成了0.5和2秒,从无可救药变成可以忍受,甚至是可以接受了,很好。

结论

不应该多次往返数据库进行操作,每次都要连接数据库,然后关闭,造成很大的性能消耗,应该一次获取足量的数据,然后用C#代码 来处理,这样会提升很多。当然了,获取多少也是很需求有关系的,不是一个固定规律。

[转载]Adobe的AJAX框架--Spry

mikel阅读(1067)

[转载]Adobe的AJAX框架–Spry – AJAX – web – JavaEye论坛.

最近看完了Adobe的AJAX框架Spry的所有文档和Demo,觉得这东西挺有意思的,在这里介绍给大家。
Spry框架的开发人员是来自于DreamWeaver开发组,他们把Spry框架做为DreamWeaver的一个完美补充为设计者和开发 者提供对AJAX技术的支持。Spry框架是一个轻量级的AJAX框架,它的代码和标签十分的简洁和优雅,以保证让用户能便捷的使用,并不会为过繁杂的标 签所惑。

Spry框架的官方网址:
http://labs.adobe.com/technologies/spry
在这里你能找到最新的文档和下载最新的Spry版本,目前版本是预览版1.3_08-11。
大家可以先在下面的看到Spry的示例和Demo:
http://labs.adobe.com/technologies/spry/samples/
http://labs.adobe.com/technologies/spry/demos/

Spry框架其实就是一个客户端的JavaScript类库,包含了一组JavaScript文件,CSS,图片文件,通过官方的框架结构 图,我们能看出Spry框架的核心是四部分:XML数据器(XML Data Sets),动态区域(Dynamic Regions),装饰器库(Widgets)和变化效果库(Transition Effects)。

我们可以看出,Spry框架接收的数据格式只是XML数据格式。

一,XML数据器(XML Data Sets)
XML数据器是一个提供了从XML文档中载入和管理数据的JavaScript对象。它是Spry框架中处理XML格式数据的一个 JavaScript功能实现。通过它,我们可以从XML中直接得到转换成表格数据格式的行和列的值,其实就是数组。它封装了获取 XMLhttpRequest的方法,和发送并接收数据等一系列获取数据的方法。
要创建一个XML数据器,你必须在你的HTML文件中加入两行引入JavaScript文件的代码:

<script type=”text/javascript” src=”../../includes/xpath.js”></script>
<script type=”text/javascript” src=”../../includes/SpryData.js”></script>

上面引入的是Spry框架的核心js文件之一。”xpath.js”是Google基于XPath 1.0标准的 JavaScript功能实现。你如果想获得更多的关于它的信息,可以访问Google的开源项目 google-ajaxslt project page .
“SpryData.js”则包含了定义XML数据器和动态区域的代码。
构造XML数据器就好像新建一个类一样,用”new”关键字即可:
<script type=”text/javascript”>
var dsPhotos = new Spry.Data.XMLDataSet(“/photos.php?galleryid=2000”, “/gallery/photos/photo”);
</script>
Spry框架为XML数据器提供了一些有特色的功能:如数据排序,数据过滤,按指定时间间隔自动更新,并引入了观察者通知模式以支持事件的触 发。
关于XML数据器更多资料,大家可以参考http://labs.adobe.com/technologies/spry/articles/data_set_overview/, 这一部分笔者已经翻译完成了,但英文原文其实写的通俗易懂,如果对一些名词翻译的不准确反倒会误导了大家,所以还是不贴出来了,大家可以参考官方英文文 档。

二,动态区域(Dynamic Regions)
一旦你建立了XML数据器,你就可以在动态区域中去显示这个数据器的数据了。
创建动态区域块很简单,只要在html标签的相应位置加上”spry:region”属性就可以了,Spry框架就会知道这一块是被标识成动 态区域了。
在动态区域,你可以有条件的选择要输出的数据,也可用循环输出。
动态区域另一个特点就是,它分为master和detail两个区域类型。
如下面的Demo截图:

master区域的数据改变会使detail区域的数据相应发生改变

两者都注册成XML数据器的观察者

当master区域的数据变化时,触发detail区域的响应事件,从面达到更新相应数据。
大家可参考http://labs.adobe.com/technologies/spry/samples/DataSetMasterDetailSample.html所 示的功能。
这只是动态区域简单的示例,复杂的情况可能会有多个XML数据器与多个动态区域相互关联触发。

三,装饰器库(Widgets)
一个装饰器是由一组HTML,CSS,JavaScript封装成的高级UI。最常见的装饰器有可折叠的菜单,树型菜单和选项table面板 等。这些对象都比较难于创建,需要一些更高级的编程经验。Spry的开发组在创建装饰器这一概念就是希望开发者们能相互协作,共享各自的设计,把这些高级 界面元素用在自己的页面上。
Spry框架下的装饰器是易于编辑的。这种模型非常适合于设计者和编辑人员:要改变外观,只要改变CSS就可以了,要增加一个可折叠菜单只要 copy和paste一个代码块就够了。
如,你能看懂的这段代码是创建了一个可折叠菜单吗?
<div id=”Acc1″  class=”Accordion”>
<div class=”Panel”>
<div class=”Header”>Panel Header  1</div>
<div class=”Content”>Panel  1 Content </div>
</div>
<div class=”Panel”>
<div class=”Header”>Panel Header  2</div>
<div class=”Content”>Panel  2 Content</div>
</div>
<div class=”Panel”>
<div class=”Header”>Panel Header  3</div>
<div class=”Content”>Panel  3 Content</div>
</div>
</div>
<script>
var acc1 = new  Hanzo.Widget.Accordion(“Acc1”);
</script>
这段代码是非常简洁清晰的,没有什么繁杂的标签,这样设计是为了易于阅读。
关于这个例子,可以参考http://labs.adobe.com/technologies/spry/samples/accordion/AccordionSample.html

四,变化效果库(Transition Effects)
Spry框架的变化效果库都存于SpryEffects.js文件中,是基于JavaScript的一些动态变化效果,如,淡出,改变形状 等。
Spry框架在设计时,曾考虑直接用第三方的效果库,如Script.aculo.us,但后来开发小组觉得要保证框架代码和标签的一致性, 还是选择了自已开发,但是也基本上是以Script.aculo.us为原型进行设计,因为Script.aculo.us 本身就是一个非常优秀的变化效果库框架。
由于Spry框架现在只是预览版,所以目前只支持七种变换:
Appear/Fade Makes an element appear or fade away
Highlight Flashes a color as the background of an element
BlindUp/BlindDown Simulates a window blind, up or down, where the contents of the affected elements stay in place
SlideUp/SlideDown Simulates a window blind, where the contents of the affected element scroll accordingly
Grow/Shrink Increases/reduces the size of the element
Shake Moves the element slightly to the left, then to the right, repeatedly
Squish Reduces the element to its top-left corner and disappears

可能到这大家对Spry框架已经有了大致的了解,其实这个东西已经足够我们的大多数应用的开发了,笔者也十分期待着正式版能早日放出,并打算 在自己现在的项目中试一试了。

[转载]关于json template一点思考

mikel阅读(990)

[转载]关于json template一点思考 – AJAX – web – JavaEye论坛.

如果ajax调用从server返回的是json,那么client如何把json render成html呢?

我看到一个叫json-template的 js库,比如如下的代码,有点像xslt转换xml

{# This is a comment and will be removed from the output.}

{.section songs}
  <h2>Songs in '{playlist-name}'</h2>

  <table width="100%">
  {.repeated section @}
    <tr>
      <td><a href="{url-base|htmltag}{url|htmltag}">Play</a>
      <td><i>{title}</i></td>
      <td>{artist}</td>
    </tr>
  {.end}
  </table>
{.or}
  <p><em>(No page content matches)</em></p>
{.end}

不过这样感觉有点把事情复杂化了,我本意只是想把json转换成html。

难道应该从server端直接传回html么?

从server端直接传回HTML有个缺点,就是html只是用来显示(view)的,如果我们用html来传递所有的数据,势必有些数据要放到 hidden的element中,这样html看起来很混乱。如果不放到hidden input中,也可以放到element attribute或者class中,少量的数据可以,多了就显得有点怪异了,影响了HTML语义semantical,这里有解 释。HTML5中有些改进,不过我看 也不是很优雅的解决方法。

放到json中的话作为一个统一的model来交给ajax(control)来处理。

感觉我需要的是一个mvc的JavaScript框架,我知道dojo中也有templete的东东,不过我不大熟悉,不知道是否解决了这个问 题。(这 里有篇如何创建widget的文章,感觉正是我需要的)。dojo包装的比较深,我不大明白其底层是用何种机制来保存这些数据的。其实数据保存在 那里无关紧要(在firebug中可以看到,还是保存在DOM中),更重要的是要建立对象(widget)和标准HTML的bind关系。

Using template in Django ? 这篇文章也同意我的观点,他用到了一个叫jBind的emplate engine

google也有个tool叫closure-templates, 跟一般的模板不同的是它可以把模板文件编译成JavaScript,这样性能就更好些。

关于dojo的东西后续讨论我放到了我的博客这里:

从 “在HTML中嵌入数 据”到Dojo的组件模型

回复:

楼主可以看看这个jTemplates,这是一个JQuery模板插件,功能非常将大,就像客户端的FreeMarker,我用过感觉不错,网 址:http://jtemplates.tpython.com/

回复:

对于json数据刷新到模版,我建议楼主参考一下adobe的spry框架,完全符合楼主的需求。而且不大的一个框架。完全可以和其他js库混用。

[转载]Lucene学习总结之五:Lucene段合并(merge)过程分析 - 觉先 - 博客园

mikel阅读(988)

[转载]Lucene学习总结之五:Lucene段合并(merge)过程分析 – 觉先 – 博客园.

一、段合并过程总论

IndexWriter中与段合并有关的成员变量有:

  • HashSet<SegmentInfo> mergingSegments = new HashSet<SegmentInfo>(); //保存正在合并的段,以防止合并期间再次选中被合并。
  • MergePolicy mergePolicy = new LogByteSizeMergePolicy(this);//合并策略,也即选取哪些段来进行合并。
  • MergeScheduler mergeScheduler = new ConcurrentMergeScheduler();//段合并器,背后有一个线程负责合并。
  • LinkedList<MergePolicy.OneMerge> pendingMerges = new LinkedList<MergePolicy.OneMerge>();//等待被合并的任务
  • Set<MergePolicy.OneMerge> runningMerges = new HashSet<MergePolicy.OneMerge>();//正在被合并的任务

和段合并有关的一些 参数有:

  • mergeFactor:当大小几乎相当的段的数量达到此值的时候,开始合并。
  • minMergeSize: 所有大小小于此值的段,都被认为是大小几乎相当,一同参与合并。
  • maxMergeSize:当一个段的大小大于此值的时候,就不再 参与合并。
  • maxMergeDocs:当一个段包含的文档数大于此值的时候,就不再参与合并。

段合并 一般发生在添加完一篇文档的时候,当一篇文档添加完后,发现内存已经达到用户设定的ramBufferSize,则写入文件系统,形成一个新的段。新段的 加入可能造成差不多大小的段的个数达到mergeFactor,从而开始了合并的过程。

合并过程最重要的是两部分:

  • 一 个是选择哪些段应该参与合并,这一步由MergePolicy来决定。
  • 一个是将选择出的段合并成新段的过程,这一步由 MergeScheduler来执行。段的合并也主要包括:
    • 对正向信息的合并,如存储域,词向量,标准化因子等。
    • 对 反向信息的合并,如词典,倒排表。

在总论中,我们重点描述合并策略对段的选择以及反向信息的合并。

1.1、合并策略对段的选择

在LogMergePolicy中,选择可以合并的段的基本 逻辑是这样的:

  • 选择的可以合并的段都是在硬盘上的,不再存在内存中的段,也不是像早期的版本一样每添加一个Document 就生成一个段,然后进行内存中的段合并,然后再合并到硬盘中。
  • 由于从内存中flush到硬盘上是按照设置的内存大小来 DocumentsWriter.ramBufferSize触发的,所以每个刚flush到硬盘上的段大小差不多,当然不排除中途改变内存设置,接下来 的算法可以解决这个问题。
  • 合并的过程是尽量按照合并几乎相同大小的段这一原则,只有大小相当的mergeFacetor个段出现的 时候,才合并成一个新的段。
  • 在硬盘上的段基本应该是大段在前,小段在后,因为大段总是由小段合并而成的,当小段凑够 mergeFactor个的时候,就合并成一个大段,小段就被删除了,然后新来的一定是新的小段。
  • 比如 mergeFactor=3,开始来的段大小为10M,当凑够3个10M的时候,0.cfs, 1.cfs, 2.cfs则合并成一个新的段3.cfs,大小为30M,然后再来4.cfs, 5.cfs, 6.cfs,合并成7.cfs,大小为30M,然后再来8.cfs, 9.cfs, a.cfs合并成b.cfs, 大小为30M,这时候又凑够了3个30M的,合并成90M的c.cfs,然后又来d.cfs, e.cfs, f.cfs合并成10.cfs,大小为30M,然后11.cfs大小为10M,这时候硬盘上的段为:c.cfs(90M) 10.cfs(30M),11.cfs(10M)。

所以LogMergePolicy对合并段的选择过程如下:

  • 将所有的段按照生成的顺序,将段的大小以mergeFactor为底取对数,放入数组中,作为选择的标准。

幻灯片1

  • 从头开始,选择一个值最大的段,然后 将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为是大小差不多的段,属于同一阶梯,此处称为第一阶梯。
  • 然 后从后向前寻找第一个属于第一阶梯的段,从start到此段之间的段都被认为是属于这一阶梯的。也包括之间生成较早但大小较小的段,因为考虑到以下几点:
    • 防止较早生成的段由于人工flush或者人工调整ramBufferSize,因而很小,却破坏了基本从大到小的规则。
    • 如 果运行较长时间后,致使段的大小参差不齐,很难合并相同大小的段。
    • 也防止一个段由于较小,而不断的都有大的段生成从而始终不能参与 合并。
  • 第一阶梯总共4个段,小于mergeFactor因而不合并,接着start=end从而选择下一阶梯。

幻灯片2

  • 从start开始,选择一个值最大的 段,然后将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为属于同一阶梯,此处称为第二阶梯。
  • 然后从后向 前寻找第一个属于第二阶梯的段,从start到此段之间的段都被认为是属于这一阶梯的。
  • 第二阶梯总共4个段,小于 mergeFactor因而不合并,接着start=end从而选择下一阶梯。

幻灯片3

  • 从start开始,选择一个值最大的 段,然后将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为属于同一阶梯,此处称为第三阶梯。
  • 由于最大的 段减去0.75后为负的,因而从start到此段之间的段都被认为是属于这一阶梯的。
  • 第三阶梯总共5个段,等于 mergeFactor,因而进行合并。

幻灯片4

  • 第三阶梯的五个段合并成一个较大的 段。
  • 然后从头开始,依然先考察第一阶梯,仍然是4个段,不合并。
  • 然后是第二阶梯,因为有了新生成的段,并且 大小足够属于第二阶梯,从而第二阶梯有5个段,可以合并。

幻灯片5

  • 第二阶段的五个段合并成一个较大的 段。
  • 然后从头开始,考察第一阶梯,因为有了新生成的段,并且大小足够属于第一阶梯,从而第一阶梯有5个段,可以合并。

幻灯片6

  • 第一阶梯的五个段合并成一个大的段。

幻灯片7

1.2、反向信息的合并

反向信息的合并包括两部分:

  • 对字典的合并,词典中的Term是按照字典顺序排序的,需要对词典中的Term进行重新 排序
  • 对于相同的Term,对包含此Term的文档号列表进行合并,需要对文档号重新编号。

对词典的合 并需要找出两个段中相同的词,Lucene是通过一个称为match的SegmentMergeInfo类型的数组以及称为queue的 SegmentMergeQueue实现的,SegmentMergeQueue是继承于 PriorityQueue<SegmentMergeInfo>,是一个优先级队列,是按照字典顺序排序的。 SegmentMergeInfo保存要合并的段的词典及倒排表信息,在SegmentMergeQueue中用来排序的key是它代表的段中的第一个 Term。

我们来举一个例子来说明合并词典的过程,以便后面解析代码的时候能够很好的理解:

  • 假设要合并五个 段,每个段包含的Term也是按照字典顺序排序的,如下图所示。
  • 首先把五个段全部放入优先级队列中,段在其中也是按照第一个 Term的字典顺序排序的,如下图。

01

  • 从优先级队列中弹出第一个 Term(“a”)相同的段到match数组中,如下图。
  • 合并这些段的第一个Term(“a”)的倒排表,并把此Term和它的倒 排表一同加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

02

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

03

  • 从优先级队列中弹出第一个 Term(“b”)相同的段到match数组中。
  • 合并这些段的第一个Term(“b”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

04

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

05

  • 从优先级队列中弹出第一个 Term(“c”)相同的段到match数组中。
  • 合并这些段的第一个Term(“c”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

06

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

07

  • 从优先级队列中弹出第一个 Term(“d”)相同的段到match数组中。
  • 合并这些段的第一个Term(“d”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

08

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

09

  • 从优先级队列中弹出第一个 Term(“e”)相同的段到match数组中。
  • 合并这些段的第一个Term(“e”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

10

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

11

  • 从优先级队列中弹出第一个 Term(“f”)相同的段到match数组中。
  • 合并这些段的第一个Term(“f”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

12

  • 合并完毕。

二、段合并的详细过程

2.1、将缓存写入新的段

IndexWriter在添加文档的时候调用函数addDocument(Document doc, Analyzer analyzer),包含如下步骤:

  • doFlush = docWriter.addDocument(doc, analyzer);//DocumentsWriter添加文档,最后返回是否进行向硬盘写入
    • return state.doFlushAfter || timeToFlushDeletes();//这取决于timeToFlushDeletes

timeToFlushDeletes返回return (bufferIsFull || deletesFull()) && setFlushPending(),而在Lucene索引过程分析(2)的DocumentsWriter的缓存管理部分提到,当 numBytesUsed+deletesRAMUsed > ramBufferSize的时候bufferIsFull设为true,也即当使用的内存大于ramBufferSize的时候,则由内存向硬盘写入。 ramBufferSize可以用IndexWriter.setRAMBufferSizeMB(double mb)设定。

  • if (doFlush) flush(true, false, false);//如果内存中缓存满了,则写入硬盘
    • if (doFlush(flushDocStores, flushDeletes) && triggerMerge)  maybeMerge();//doFlush将缓存写入硬盘,此过程在Lucene索引过程分析(4)中关闭IndexWriter一节已经描述。

当缓存写入硬盘,形成了新的段后,就有可能触发一次段合并,所以调用maybeMerge()

IndexWriter.maybeMerge()

–> maybeMerge(false);

–> maybeMerge(1, optimize);

–> updatePendingMerges(maxNumSegmentsOptimize, optimize);

–> mergeScheduler.merge(this);

IndexWriter.updatePendingMerges(int maxNumSegmentsOptimize, boolean optimize)主要负责找到可以合并的段,并生产段合并任务对象,并向段合并器注册这个任务。

ConcurrentMergeScheduler.merge(IndexWriter) 主要负责进行段的合并。

2.2、选择合并段,生成合并任务

IndexWriter.updatePendingMerges(int maxNumSegmentsOptimize, boolean optimize)主要包括两部分:

  • 选择能够合并 段:MergePolicy.MergeSpecification spec = mergePolicy.findMerges(segmentInfos);
  • 向段合并器注册合并任务,将任务加到 pendingMerges中:
    • for(int i=0;i<spec.merges.size();i++)
      • registerMerge(spec.merges.get(i));

2.2.1、 用合并策略选择合并段

默认的段合并策略是LogByteSizeMergePolicy,其选择合并段由 LogMergePolicy.findMerges(SegmentInfos infos) 完成,包含以下过程:

(1) 生成levels数组,每个段一项。然后根据每个段的大小,计算每个项的值,levels[i]和段的大小的关系为 Math.log(size)/Math.log(mergeFactor),代码如下:

final int numSegments = infos.size();

float[] levels = new float[numSegments];

final float norm = (float) Math.log(mergeFactor);

for(int i=0;i<numSegments;i++) {

final SegmentInfo info = infos.info(i);

long size = size(info);

levels[i] = (float) Math.log(size)/norm;

}

(2) 由于段基本是按照由大到小排列的,而且合并段应该大小差不多的段中进行。我们把大小差不多的段称为属于同一阶梯,因而此处从第一个段开始找属于相同阶梯的 段,如果属于此阶梯的段数量达到mergeFactor个,则生成合并任务,否则继续向后寻找下一阶梯。

//计算最低阶梯值,所有小于此值的都属于最低阶梯

final float levelFloor = (float) (Math.log(minMergeSize)/norm);

MergeSpecification spec = null;

int start = 0;

while(start < numSegments) {

//找到levels数组的最大值,也即当 前阶梯中的峰值

float maxLevel = levels[start];

for(int i=1+start;i<numSegments;i++) {

final float level = levels[i];

if (level > maxLevel)

maxLevel = level;

}

//计算出此阶梯的谷值,也即最大值减去0.75,之间的都属于此阶梯。如果峰值小于最低阶梯值,则所有此阶梯的段都属于最低阶梯。如果峰值大于最低阶梯 值,谷值小于最低阶梯值,则设置谷值为最低阶梯值,以保证所有小于最低阶梯值的段都属于最低阶梯。

float levelBottom;

if (maxLevel < levelFloor)

levelBottom = -1.0F;

else {

levelBottom = (float) (maxLevel – LEVEL_LOG_SPAN);

if (levelBottom < levelFloor && maxLevel >= levelFloor)

levelBottom = levelFloor;

}

float levelBottom = (float) (maxLevel – LEVEL_LOG_SPAN);

//从最后一个段向左找,当然段越来越大,找到第一个大于此阶梯的谷值的段,从start的段开始,一直到upto这个段,都属于此阶梯了。尽管upto 左面也有的段由于内存设置原因,虽形成较早,但是没有足够大,也作为可合并的一员考虑在内了,将被并入一个大的段,从而保证了基本上左大右小的关系。从 upto这个段向右都是比此阶梯小的多的段,应该属于下一阶梯。

int upto = numSegments-1;

while(upto >= start) {

if (levels[upto] >= levelBottom) {

break;

}

upto–;

}

//从start段开始,数 mergeFactor个段,如果不超过upto段,说明此阶梯已经足够mergeFactor个了,可以合并了。当然如果此阶梯包含太多要合并的段,也 是每mergeFactor个段进行一次合并,然后再依次数mergeFactor段进行合并,直到此阶梯的段合并完毕。

int end = start + mergeFactor;

while(end <= 1+upto) {

boolean anyTooLarge = false;

for(int i=start;i<end;i++) {

final SegmentInfo info = infos.info(i);

//如果一个段的大小超过maxMergeSize或者一个段包含的文档 数量超过maxMergeDocs则不再合并。

anyTooLarge |= (size(info) >= maxMergeSize || sizeDocs(info) >= maxMergeDocs);

}

if (!anyTooLarge) {

if (spec == null)

spec = new MergeSpecification();

// 如果确认要合并,则从start到end生成一个段合并任务OneMerge.

spec.add(new OneMerge(infos.range(start, end), useCompoundFile));

}

//刚刚合并的是从start到end共mergeFactor和段,此阶梯还有更多的段,则再依次 数mergeFactor个段。

start = end;

end = start + mergeFactor;

}

//从start到upto是此阶梯的 所有的段,已经选择完毕,下面选择更小的下一个阶梯的段

start = 1+upto;

}

选择的结果保存在MergeSpecification中,结构如下:

spec    MergePolicy$MergeSpecification  (id=25)
merges    ArrayList<E>  (id=28)
elementData    Object[10]  (id=39)
[0]    MergePolicy$OneMerge  (id=42)
aborted    false
error    null
increfDone    false
info    null
isExternal    false
maxNumSegmentsOptimize    0
mergeDocStores    false
mergeGen    0
optimize    false
readers    null
readersClone    null
registerDone    false
segments    SegmentInfos  (id=50)
capacityIncrement    0
counter    0
elementCount    3
elementData    Object[10]  (id=54)
[0]    SegmentInfo  (id=62)
delCount    0
delGen    -1
diagnostics    HashMap<K,V>  (id=67)
dir    SimpleFSDirectory  (id=69)
docCount    1062
docStoreIsCompoundFile    false
docStoreOffset    0
docStoreSegment    “_0”
files    ArrayList<E>  (id=73)
hasProx    true
hasSingleNormFile    true
isCompoundFile    1
name    “_0”
normGen    null
preLockless    false
sizeInBytes    15336467
[1]    SegmentInfo  (id=64)
delCount    0
delGen    -1
diagnostics    HashMap<K,V>  (id=79)
dir    SimpleFSDirectory  (id=69)
docCount    1068
docStoreIsCompoundFile    false
docStoreOffset    1062
docStoreSegment    “_0”
files    ArrayList<E>  (id=80)
hasProx    true
hasSingleNormFile    true
isCompoundFile    1
name    “_1”
normGen    null
preLockless    false
sizeInBytes    15420953
[2]    SegmentInfo  (id=65)
delCount    0
delGen    -1
diagnostics    HashMap<K,V>  (id=86)
dir    SimpleFSDirectory  (id=69)
docCount    1068
docStoreIsCompoundFile    false
docStoreOffset    2130
docStoreSegment    “_0”
files    ArrayList<E>  (id=88)
hasProx    true
hasSingleNormFile    true
isCompoundFile    1
name    “_2”
normGen    null
preLockless    false
sizeInBytes    15420953
generation    0
lastGeneration    0
modCount    1
pendingSegnOutput    null
userData    Collections$EmptyMap  (id=57)
version    1267460515437
useCompoundFile    true
modCount    1
size    1

2.2.2、注册段合并任务

注册段合并任务由 IndexWriter.registerMerge(MergePolicy.OneMerge merge)完成:

(1) 如果选择出的段正在被合并,或者不存在,则退出。

final int count = merge.segments.size();

boolean isExternal = false;

for(int i=0;i<count;i++) {

final SegmentInfo info = merge.segments.info(i);

if (mergingSegments.contains(info))

return false;

if (segmentInfos.indexOf(info) == -1)

return false;

if (info.dir != directory)

isExternal = true;

}

(2) 将合并任务加入pendingMerges:pendingMerges.add(merge);

(3) 将要合并的段放入mergingSegments以防正在合并又被选为合并段。

for(int i=0;i<count;i++)
mergingSegments.add(merge.segments.info(i));

2.3、段合并器进行段合并

段合并器默认为 ConcurrentMergeScheduler,段的合并工作由 ConcurrentMergeScheduler.merge(IndexWriter) 完成,它包含while(true)的循环,在循环中不断做以下事情:

  • 得到下一个合并任 务:MergePolicy.OneMerge merge = writer.getNextMerge();
  • 初始化合并任 务:writer.mergeInit(merge);
    • 将删除文档写入硬盘:applyDeletes();
    • 是 否合并存储域:mergeDocStores = false。按照Lucene的索引文件格式(2)中段的元数据信息(segments_N)中提到 的,IndexWriter.flush(boolean triggerMerge, boolean flushDocStores, boolean flushDeletes)中第二个参数flushDocStores会影响到是否单独或是共享存储。其实最终影响的是 DocumentsWriter.closeDocStore()。每当flushDocStores为false时,closeDocStore不被调 用,说明下次添加到索引文件中的域和词向量信息是同此次共享一个段的。直到flushDocStores为true的时候,closeDocStore被 调用,从而下次添加到索引文件中的域和词向量信息将被保存在一个新的段中,不同此次共享一个段。如2.1节中说的那样,在addDocument中,如果 内存中缓存满了,则写入硬盘,调用的是flush(true, false, false),也即所有的存储域都存储在共享的域中(_0.fdt),因而不需要合并存储域。
    • 生成新的段:merge.info = new SegmentInfo(newSegmentName(),…)
    • 将新的段加入mergingSegments
  • 如果已经有足够多的段合并线程,则等待while (mergeThreadCount() >= maxThreadCount) wait();
  • 生成新的段合并线程:
    • merger = getMergeThread(writer, merge);
    • mergeThreads.add(merger);
  • 启动段合并线程:merger.start();

段合并线程的类型为 MergeThread,MergeThread.run()包含while(truy)循环,在循环中做以下事情:

  • 合并当 前的任务:doMerge(merge);
  • 得到下一个段合并任务:merge = writer.getNextMerge();

ConcurrentMergeScheduler.doMerge(OneMerge) 最终调用IndexWriter.merge(OneMerge) ,主要做以下事情:

  • 初始化合并任 务:mergeInit(merge);
  • 进行合并:mergeMiddle(merge);
  • 完成合并任 务:mergeFinish(merge);
    • 从mergingSegments中移除被合并的段和合并新生成的段:
      • for(int i=0;i<end;i++) mergingSegments.remove(sourceSegments.info(i));
      • mergingSegments.remove(merge.info);
    • 从runningMerges中移除此合并任务:runningMerges.remove(merge);

IndexWriter.mergeMiddle(OneMerge)主要做以下几件事情:

  • 生成用于合并段的对象SegmentMerger merger = new SegmentMerger(this, mergedName, merge);
  • 打开Reader指向要合并的段:
merge.readers = new SegmentReader[numSegments];

merge.readersClone = new SegmentReader[numSegments];

for (int i = 0; i < numSegments; i++) {

final SegmentInfo info = sourceSegments.info(i);

// Hold onto the “live” reader; we will use this to

// commit merged deletes

SegmentReader reader = merge.readers[i] = readerPool.get(info, merge.mergeDocStores,MERGE_READ_BUFFER_SIZE,-1);

// We clone the segment readers because other

// deletes may come in while we’re merging so we

// need readers that will not change

SegmentReader clone = merge.readersClone[i] = (SegmentReader) reader.clone(true);

merger.add(clone);

}

  • 进行段合 并:mergedDocCount = merge.info.docCount = merger.merge(merge.mergeDocStores);
  • 合并生成的段生成为 cfs:merger.createCompoundFile(compoundFileName);

SegmentMerger.merge(boolean) 包含以下几部分:

  • 合并域:mergeFields()
  • 合并词典和倒排 表:mergeTerms();
  • 合并标准化因子:mergeNorms();
  • 合并词向 量:mergeVectors();

下面依次分析者几部分。

2.3.1、合并存储域

合并存储域主要包含两部分:一部分是合并fnm信息,也即域元数据信息,一部分是合并fdt,fdx信息,也即域数据信息。

(1) 合并fnm信息

  • 首先生成新的域元数据信息:fieldInfos = new FieldInfos();
  • 依次用reader读取每个合并段的域元数据信息,加入上述对象
for (IndexReader reader : readers) {

SegmentReader segmentReader = (SegmentReader) reader;

FieldInfos readerFieldInfos = segmentReader.fieldInfos();

int numReaderFieldInfos = readerFieldInfos.size();

for (int j = 0; j < numReaderFieldInfos; j++) {

FieldInfo fi = readerFieldInfos.fieldInfo(j);

//在通常情况下,所有的段中的文档都包 含相同的域,比如添加文档的时候,每篇文档都包含”title”,”description”,”author”,”time”等,不会为某一篇文档添加 或减少与其他文档不同的域。但也不排除特殊情况下有特殊的文档有特殊的域。因而此处的add是无则添加,有则更新。

fieldInfos.add(fi.name, fi.isIndexed, fi.storeTermVector,

fi.storePositionWithTermVector, fi.storeOffsetWithTermVector,

!reader.hasNorms(fi.name), fi.storePayloads,

fi.omitTermFreqAndPositions);

}

}

  • 将域元数据信息fnm写入文件:fieldInfos.write(directory, segment + “.fnm”);

(2) 合并段数据信息fdt, fdx

在合并段的数据信息的时候,有两种情况:

  • 情况一:通常情况,要合并的段和新生成段包含的域的名称,顺序都是一样的,这样就可以把要合并的段的fdt信息直接拷贝到新生成段的最后,以提 高合并效率。
  • 情况二:要合并的段包含特殊的文档,其包含的域多于或者少于新生成段的域,这样就不能够直接拷贝,而是一篇文档一篇文 档的添加。这样合并效率大大降低,因而不鼓励添加文档的时候,不同的文档使用不同的域。

具体过程如下:

  • 首先检查要合并的各个段,其包含域的名称,顺序是否同新生成段的一致,也即是否属于第一种情 况:setMatchingSegmentReaders();
private void setMatchingSegmentReaders() {

int numReaders = readers.size();

matchingSegmentReaders = new SegmentReader[numReaders];

//遍历所有的要合并的段

for (int i = 0; i < numReaders; i++) {

IndexReader reader = readers.get(i);

if (reader instanceof SegmentReader) {

SegmentReader segmentReader = (SegmentReader) reader;

boolean same = true;

FieldInfos segmentFieldInfos = segmentReader.fieldInfos();

int numFieldInfos = segmentFieldInfos.size();

//依次比较要合并的段和新生成的段的段名,顺序是否 一致。

for (int j = 0; same && j < numFieldInfos; j++) {

same = fieldInfos.fieldName(j).equals(segmentFieldInfos.fieldName(j));

}

//最后生成matchingSegmentReaders数组,如果此数组的第i项不是null,则 说明第i个段同新生成的段名称,顺序完全一致,可以采取情况一得方式。如果此数组的第i项是null,则说明第i个段包含特殊的域,则采取情况二的方式。

if (same) {

matchingSegmentReaders[i] = segmentReader;

}

}

}

}

  • 生成存储域的写对象:FieldsWriter fieldsWriter = new FieldsWriter(directory, segment, fieldInfos);
  • 依次遍历所有的要合并的段,按照上 述两种情况,使用不同策略进行合并
int idx = 0;

for (IndexReader reader : readers) {

final SegmentReader matchingSegmentReader = matchingSegmentReaders[idx++];

FieldsReader matchingFieldsReader = null;

//如果 matchingSegmentReader!=null,表示此段属于情况一,得到matchingFieldsReader

if (matchingSegmentReader != null) {

final FieldsReader fieldsReader = matchingSegmentReader.getFieldsReader();

if (fieldsReader != null && fieldsReader.canReadRawDocs()) {

matchingFieldsReader = fieldsReader;

}

}

//根据此段是否包含删除的文档采取不同的策略

if (reader.hasDeletions()) {

docCount += copyFieldsWithDeletions(fieldsWriter, reader, matchingFieldsReader);

} else {

docCount += copyFieldsNoDeletions(fieldsWriter,reader, matchingFieldsReader);

}

}

  • 合并包含删除文档的段
private int copyFieldsWithDeletions(final FieldsWriter fieldsWriter, final IndexReader reader,

final FieldsReader matchingFieldsReader)

throws IOException, MergeAbortedException, CorruptIndexException {

int docCount = 0;

final int maxDoc = reader.maxDoc();

//matchingFieldsReader!=null, 说明此段属于情况一, 则可以直接拷贝。

if (matchingFieldsReader != null) {

for (int j = 0; j < maxDoc;) {

if (reader.isDeleted(j)) {

// 如果文档被删除,则跳过此文档。

++j;

continue;

}

int start = j, numDocs = 0;

do {

j++;

numDocs++;

if (j >= maxDoc) break;

if (reader.isDeleted(j)) {

j++;

break;

}

} while(numDocs < MAX_RAW_MERGE_DOCS);

//从要合并的段中从第start篇文档开始,依次读取numDocs篇文档的文档长度到rawDocLengths中。

IndexInput stream = matchingFieldsReader.rawDocs(rawDocLengths, start, numDocs);

//用fieldsStream.copyBytes(…)直接将fdt信息从要合并的段拷贝到新生成的段,然后将上面读出的rawDocLengths 转换成为每篇文档在fdt中的偏移量,写入fdx文件。

fieldsWriter.addRawDocuments(stream, rawDocLengths, numDocs);

docCount += numDocs;

checkAbort.work(300 * numDocs);

}

} else {

//matchingFieldsReader==null,说明此段属于情况二,必须每篇文档依次添加。

for (int j = 0; j < maxDoc; j++) {

if (reader.isDeleted(j)) {

// 如果文档被删除,则跳过此文档。

continue;

}

//同addDocument 的过程中一样,重新将文档添加一遍。

Document doc = reader.document(j);

fieldsWriter.addDocument(doc);

docCount++;

checkAbort.work(300);

}

}

return docCount;

}

  • 合 并不包含删除文档的段:除了跳过删除的文档的部分,同上述过程一样。
  • 关闭存储域的写对 象:fieldsWriter.close();

2.3.2、合并标准化因子

合并标准化因子的过程比较简单,基本就是对每一个域,用指向合并段的reader读出标准化因子,然后再写入新生成的段。

private void mergeNorms() throws IOException {

byte[] normBuffer = null;

IndexOutput output = null;

try {

int numFieldInfos = fieldInfos.size();

//对于每一个域

for (int i = 0; i < numFieldInfos; i++) {

FieldInfo fi = fieldInfos.fieldInfo(i);

if (fi.isIndexed && !fi.omitNorms) {

if (output == null) {

//指向新生成的段的nrm文件的写入流

output = directory.createOutput(segment + “.” + IndexFileNames.NORMS_EXTENSION);

//写nrm文件头

output.writeBytes(NORMS_HEADER,NORMS_HEADER.length);

}

//对于每一个合并段的reader

for ( IndexReader reader : readers) {

int maxDoc = reader.maxDoc();

if (normBuffer == null || normBuffer.length < maxDoc) {

// the buffer is too small for the current segment

normBuffer = new byte[maxDoc];

}

//读出此段的nrm信息。

reader.norms(fi.name, normBuffer, 0);

if (!reader.hasDeletions()) {

//如果没有文档被删除则写入新生成的段。

output.writeBytes(normBuffer, maxDoc);

} else {

//如果有文档删除则跳过删除的文档写入新生成的段。

for (int k = 0; k < maxDoc; k++) {

if (!reader.isDeleted(k)) {

output.writeByte(normBuffer[k]);

}

}

}

checkAbort.work(maxDoc);

}

}

}

} finally {

if (output != null) {

output.close();

}

}

}

2.3.3、合并词向量

合并词向量的过程同合并存储域的过程非常相似,也包括两种情况:

  • 情况一:通常情况,要合并的段和新生成段包含的域的名称,顺序都是一样的,这样就可以把要合并的段的词向量信息直接拷贝到新生成段的 最后,以提高合并效率。
  • 情况二:要合并的段包含特殊的文档,其包含的域多于或者少于新生成段的域,这样就不能够直接拷贝,而是一篇 文档一篇文档的添加。这样合并效率大大降低,因而不鼓励添加文档的时候,不同的文档使用不同的域。

具体过程如下:

  • 生成词向量的写对象:TermVectorsWriter termVectorsWriter = new TermVectorsWriter(directory, segment, fieldInfos);
  • 依次遍历所有的要合并的 段,按照上述两种情况,使用不同策略进行合并
int idx = 0;

for (final IndexReader reader : readers) {

final SegmentReader matchingSegmentReader = matchingSegmentReaders[idx++];

TermVectorsReader matchingVectorsReader = null;

//如果matchingSegmentReader!=null,表示此段属于情况一,得到matchingFieldsReader

if (matchingSegmentReader != null) {

TermVectorsReader vectorsReader = matchingSegmentReader.getTermVectorsReaderOrig();

if (vectorsReader != null && vectorsReader.canReadRawDocs()) {

matchingVectorsReader = vectorsReader;

}

}

//根据此段是否包含删除的文档采取不同的策略

if (reader.hasDeletions()) {

copyVectorsWithDeletions(termVectorsWriter, matchingVectorsReader, reader);

} else {

copyVectorsNoDeletions(termVectorsWriter, matchingVectorsReader, reader);

}

}

  • 合并包含 删除文档的段
private void copyVectorsWithDeletions(final TermVectorsWriter termVectorsWriter, final TermVectorsReader matchingVectorsReader, final IndexReader reader)

throws IOException, MergeAbortedException {

final int maxDoc = reader.maxDoc();

//matchingFieldsReader!=null,说明此段属于情况一, 则可以直接拷贝。

if (matchingVectorsReader != null) {

for (int docNum = 0; docNum < maxDoc;) {

if (reader.isDeleted(docNum)) {

// 如果文档被删除,则跳过此文档。

++docNum;

continue;

}

int start = docNum, numDocs = 0;

do {

docNum++;

numDocs++;

if (docNum >= maxDoc) break;

if (reader.isDeleted(docNum)) {

docNum++;

break;

}

} while(numDocs < MAX_RAW_MERGE_DOCS);

//从要合并的段中从第start篇文档开始,依次读取 numDocs篇文档的tvd到rawDocLengths中,tvf到rawDocLengths2。

matchingVectorsReader.rawDocs(rawDocLengths, rawDocLengths2, start, numDocs);

//用tvd.copyBytes(…)直接将tvd信息从要合并的段拷贝到新生成的 段,然后将上面读出的rawDocLengths转换成为每篇文档在tvd文件中的偏移量,写入tvx文件。用tvf.copyBytes(…)直接将 tvf信息从要合并的段拷贝到新生成的段,然后将上面读出的rawDocLengths2转换成为每篇文档在tvf文件中的偏移量,写入tvx文件。

termVectorsWriter.addRawDocuments(matchingVectorsReader, rawDocLengths, rawDocLengths2, numDocs);

checkAbort.work(300 * numDocs);

}

} else {

//matchingFieldsReader==null,说明此段属于情况二,必须每篇文档依次添加。

for (int docNum = 0; docNum < maxDoc; docNum++) {

if (reader.isDeleted(docNum)) {

// 如果文档被删除,则跳过此文档。

continue;

}

//同addDocument的过程中一样,重新将文档添加一遍。

TermFreqVector[] vectors = reader.getTermFreqVectors(docNum);

termVectorsWriter.addAllDocVectors(vectors);

checkAbort.work(300);

}

}

}

  • 合并不包含删除文档的段:除了跳过删除的文档的部分,同上述过程一样。
  • 关闭词向量的写对 象:termVectorsWriter.close();

2.3.4、合并词典和倒排表

以上都是合并正向信息,相对过程比较清晰。而合并词典和倒排表就不这么简单了,因为在词典中,Lucene要求按照字典顺序排序,在倒排表中,文 档号要按照从小到大顺序排序排序,在每个段中,文档号都是从零开始编号的。

所以反向信息的合并包括两部分:

  • 对 字典的合并,需要对词典中的Term进行重新排序
  • 对于相同的Term,对包含此Term的文档号列表进行合并,需要对文档号重新编 号。

后者相对简单,假设如果第一个段的编号是0~N,第二个段的编号是0~M,当两个段合并成一个段的时候,第一个段的编号 依然是0~N,第二个段的编号变成N~N+M就可以了,也即增加一个偏移量(前一个段的文档个数)。

对词典的合并需要找出两个段中相同的 词,Lucene是通过一个称为match的SegmentMergeInfo类型的数组以及称为queue的SegmentMergeQueue实现 的,SegmentMergeQueue是继承于PriorityQueue<SegmentMergeInfo>,是一个优先级队列,是按 照字典顺序排序的。SegmentMergeInfo保存要合并的段的词典及倒排表信息,在SegmentMergeQueue中用来排序的key是它代 表的段中的第一个Term。

在总论部分,举了一个例子表明词典和倒排表合并的过程。

下面让我们深入代码看一看具体的实 现:

(1) 生成优先级队列,并将所有的段都加入优先级队列。

//在Lucene索引过程分析(4)中提到 过,FormatPostingsFieldsConsumer 是用来写入倒排表信息的。

//FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息,其返回FormatPostingsTermsConsumer用于添加词信息。

//FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加词信息,其返回FormatPostingsDocsConsumer用于添加freq信息

//FormatPostingsDocsConsumer.addDoc(int docID, int termDocFreq)用于添加freq信息,其返回FormatPostingsPositionsConsumer用于添加prox信息

//FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength)用于添加prox信息

FormatPostingsFieldsConsumer consumer = new FormatPostingsFieldsWriter(state, fieldInfos);

// 优先级队列

queue = new SegmentMergeQueue(readers.size());

//对于每一个段

final int readerCount = readers.size();

for (int i = 0; i < readerCount; i++) {

IndexReader reader = readers.get(i);

TermEnum termEnum = reader.terms();

//生成SegmentMergeInfo对象,termEnum就是此段的词典及倒排表。

SegmentMergeInfo smi = new SegmentMergeInfo(base, termEnum, reader);

//base就是下一个段的文档号偏移量,等于此段的文档数目。

base += reader.numDocs();

if (smi.next()) //得到段的第一个Term

queue.add(smi); //将此段放入优先级队列。

else

smi.close();

}

(2) 生成match数组

SegmentMergeInfo[] match = new SegmentMergeInfo[readers.size()];

(3) 合并词典

//如果队列不为空,则合并尚未结束

while (queue.size() > 0) {

int matchSize = 0;

//取出优先级队列的第一个段,放到match数组中

match[matchSize++] = queue.pop();

Term term = match[0].term;

SegmentMergeInfo top = queue.top();

//如果优先级队列的最顶端和已经弹出的match中的段的第一个Term相同,则全部弹出。

while (top != null && term.compareTo(top.term) == 0) {

match[matchSize++] =  queue.pop();

top =  queue.top();

}

if (currentField != term.field) {

currentField = term.field;

if (termsConsumer != null)

termsConsumer.finish();

final FieldInfo fieldInfo = fieldInfos.fieldInfo(currentField);

//FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息,其返回FormatPostingsTermsConsumer用于添加词信息。

termsConsumer = consumer.addField(fieldInfo);

omitTermFreqAndPositions = fieldInfo.omitTermFreqAndPositions;

}

//合并match数组中的所有的段的第一个Term的倒排表信息,并写入新生成的段。

int df = appendPostings(termsConsumer, match, matchSize);

checkAbort.work(df/3.0);

while (matchSize > 0) {

SegmentMergeInfo smi = match[—matchSize];

//如果match中的段还有下一个Term,则放回优先级队列,进行下一轮的循环。

if (smi.next())

queue.add(smi);

else

smi.close();

}

}

(4) 合并倒排表

private final int appendPostings(final FormatPostingsTermsConsumer termsConsumer, SegmentMergeInfo[] smis, int n)

throws CorruptIndexException, IOException {

//FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加词信息,其返回FormatPostingsDocsConsumer用于添加freq信息

//将match数组中段的第一个Term添加到新生成的段中。

final FormatPostingsDocsConsumer docConsumer = termsConsumer.addTerm(smis[0].term.text);

int df = 0;

for (int i = 0; i < n; i++) {

SegmentMergeInfo smi = smis[i];

//得到要合并的段的位置信息(prox)

TermPositions postings = smi.getPositions();

//此段的文档号偏移量

int base = smi.base;

//在要合并的段中找到Term的倒排表位置。

postings.seek(smi.termEnum);

//不断得到下一篇文档号

while (postings.next()) {

df++;

int doc = postings.doc();

//文档号都要加上偏移量

doc += base;

//得到词频信息(frq)

final int freq = postings.freq();

//FormatPostingsDocsConsumer.addDoc(int docID, int termDocFreq)用于添加freq信息,其返回FormatPostingsPositionsConsumer用于添加prox信息

final FormatPostingsPositionsConsumer posConsumer = docConsumer.addDoc(doc, freq);

//如果位置信息需要保存

if (!omitTermFreqAndPositions) {

for (int j = 0; j < freq; j++) {

//得到位置信息(prox)以及payload信息

final int position = postings.nextPosition();

final int payloadLength = postings.getPayloadLength();

if (payloadLength > 0) {

if (payloadBuffer == null || payloadBuffer.length < payloadLength)

payloadBuffer = new byte[payloadLength];

postings.getPayload(payloadBuffer, 0);

}

//FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength)用于添加prox信息

posConsumer.addPosition(position, payloadBuffer, 0, payloadLength);

}

posConsumer.finish();

}

}

}

docConsumer.finish();

return df;

}