ASP.NET夜话之21:asp.net网站的性能优化
本篇主要讲述在ASP.NET中如何提高程序性能。提高程序性能的方法主要从编码和数据操作及优化配置三方面,本章要讲述的知识点有:
l 程序编码优化
l 数据操作优化
l 配置优化
l 总结
21.1 程序编码优化
从编码方面提高程序性能的方法主要涉及到集合操作、字符串连接、类型转换等。
21.1.1 集合操作
在.NET Framework中提供了很多集合类,如ArrayList、BitArray、Hashtable、Queue、SortedList、Stack、ListDictionary、NameValueCollection、OrderedDictionary、StringCollection、List<T>及数组等,要了解各个集合的特性,选择合适的集合。在所有的集合中数组是性能最高的,如果要存储的数据类型一致和容量固定,特别是对值类型的数组进行操作时没有装箱和拆箱操作,效率极高。
在选择集合类型时应考虑几点:
(1)集合中的元素类型是否是一致的,比如集合中将要存储的元素都是int或者都是string类型的就可以考虑使用数组或者泛型集合,这样在存储数值类型元素就可以避免装箱拆箱操作,即使是引用类型的元素也可以避免类型转换操作。
(2)集合中的元素个数是否是固定的,如果集合中存储的元素是固定的并且元素类型是一致的就可以使用数组来存储。
(3)将来对集合的操作集中在那些方面,如果对集合的操作以查找居多可以考虑HashTable或者Dictionary<TKey,TValue>这样的集合,因为在.NET Framework中对这类集合采用了特殊机制,所以在查找时比较的次数比其它集合要少。
另外,在使用可变集合时如果不制定初始容量大小,系统会使用一个默认值来指定可变集合的初始容量大小,如果将来元素个数超过初始容量大小就会先在内部重新构建一个集合,再将原来集合中的元素复制到新集合中,可以在实例化可变集合时指定一个相对较大的初始容量,这样在向可变集合中添加大量元素时就可以避免集合扩充容量带来的性能损失。
下面以一个例子演示一下数组、ArrayList及List<T>集合操作的例子。页面的设计代码如下:
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// 用来测试对集合进行操作所花费的时间
/// </summary>
public class CollectionDemo
{
public static void Main()
{
Test(100000);
Console.WriteLine("==========");
Test(1000000);
Console.WriteLine("==========");
Test(10000000);
Console.ReadLine();
}
/// <summary>
/// 操作数组
/// </summary>
/// <param name="maxCount">要操作的次数</param>
/// <returns></returns>
private static TimeSpan ArrayOperation(int maxCount)
{
//在程序开始运行时记录下系统当前时间
DateTime start = DateTime.Now;
int[] intList = new int[maxCount];
int j = 0;
for (int i = 0; i < maxCount; i++)
{
intList[i] = i;
}
for (int i = 0; i < maxCount; i++)
{
j = intList[i];
}
//在程序结束后记录下系统当前时间
DateTime end = DateTime.Now;
//用程序结束的系统时间减去开始运行时的时间就是代码运行时间
return end - start;
}
/// <summary>
///
/// </summary>
/// <param name="maxCount"></param>
/// <returns></returns>
private static TimeSpan ArrayListOperation(int maxCount)
{
//在程序开始运行时记录下系统当前时间
DateTime start = DateTime.Now;
//用默认的容量来初始化ArrayList
//ArrayList intList = new ArrayList();
//用指定的容量来初始化ArrayList
ArrayList intList = new ArrayList(maxCount);
int j = 0;
for (int i = 0; i < maxCount; i++)
{
intList.Add(i);
}
for (int i = 0; i < maxCount; i++)
{
j = (int)intList[i];
}
//在程序结束后记录下系统当前时间
DateTime end = DateTime.Now;
return end - start;
}
/// <summary>
///
/// </summary>
/// <param name="maxCount"></param>
/// <returns></returns>
private static TimeSpan GenericListOperation(int maxCount)
{
//在程序开始运行时记录下系统当前时间
DateTime start = DateTime.Now;
//用默认的容量来初始化泛型集合
//List<int> intList = new List<int>();
//用指定的容量来初始化泛型集合
List<int> intList = new List<int>(maxCount);
int j = 0;
for (int i = 0; i < maxCount; i++)
{
intList.Add(i);
}
for (int i = 0; i < maxCount; i++)
{
j = intList[i];
}
//在程序结束后记录下系统当前时间
DateTime end = DateTime.Now;
return end - start;
}
private static void Test(int maxCount)
{
TimeSpan ts1 = ArrayOperation(maxCount);
TimeSpan ts2 = ArrayListOperation(maxCount);
TimeSpan ts3 = GenericListOperation(maxCount);
Console.WriteLine("执行" + maxCount + "次操作:");
Console.WriteLine("数组耗时" + ts1.TotalMilliseconds + "毫秒");
Console.WriteLine("ArrayList耗时" + ts2.TotalMilliseconds + "毫秒");
Console.WriteLine("泛型集合耗时" + ts3.TotalMilliseconds + "毫秒");
}
}
对上面的程序代码做几点说明:
(1)上面的代码仅仅是给集合中的元素赋值,然后将集合中的元素取出来,分别用了数组、ArrayList和List<T>泛型集合,并且操作了不同的次数。
(2)在开始运行时获取到系统的当前时间,然后在运行结束之后再次获取系统时间,两次时间之差就是程序运行这段代码所花费的时间,这是一个TimeSpan类型的变量。
(3)为了将测试结果放大,所以操作的次数要尽量设置大一点,实际在网站运行中程序代码也会被成千上万次运行,所以这么做是可以接受的,也使得比较更明显,并且这样也可以减小某些偶然因素带来的干扰。
因为在ASP.NET中测试不稳定因素太多,所以这部分代码是以控制台程序来运行的,运行上面的代码可得到如图21-1所示的效果:
图21-1 程序执行结果
4.利用Stopwatch类也可以方便地实现上面的计时功能,对于上面用到的GenericListOperation(int maxCount)方法,如果使用Stopwatch类的话,代码如下:
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();//在程序开始运行时开始计时
List<int> intList = new List<int>(maxCount);
int j = 0;
for (int i = 0; i < maxCount; i++)
{
intList.Add(i);
}
for (int i = 0; i < maxCount; i++)
{
j = intList[i];
}
stopwatch.Stop();//在程序结束后停止计时
return stopwatch.Elapsed;
}
在上面的代码中我们是采用了指定ArrayList和List<int>泛型集合的初始化容量大小,可以看出操作在集合元素固定的情况下,数组的操作是最快的,泛型集合的操作次之,ArrayList最慢。
以上测试是针对值类型数据的测试,如果是String这类的引用类型也会有类似的效果,只不过效果引用类型作为集合元素没有值类型作为集合元素明显。
21.1.2 字符串连接优化
在.NET Framework中String类是一个比较特殊的类,我们知道值类型变量直接在栈中分配内存来存储变量的值,并且不需要垃圾回收器来回收,大部分引用类型变量是在堆中分配内存来存储变量的值,在不再使用的情况下会被垃圾回收器回收所占用的内存。String类型的变量虽然是引用类型变量(常用的赋值方式却很类似于值类型变量的赋值方式,如string a=”123”),但是CLR(Common Language Runtime,通用语言运行时)通过了一种特殊的方法来存放字符串,CLR会维护一个会自动维护一个名为“拘留池”(intern pool,不知道为什么微软会这么叫) 的表,它包含在程序中声明的每个唯一字符串常数的单个实例,以及以编程方式添加的 String 的任何唯一实例。该拘留池节约字符串存储区。如果将字符串常数分配给几个变量,则每个变量设置为引用“拘留池”(intern pool) 中的同一常数,而不是引用具有相同值的 String 的几个不同实例。
看如下代码:
String a=”abc”;
String b=”abc”;
在上面的代码中变量a和变量b都指向了堆中的同一个引用,也就是和下面的代码是等效的:
String a=”abc”;
String b=a;
在给字符串变量赋值时会首先在“拘留池”中检查是否有与要赋值的值相等的字符串,如果存在就会返回该字符串的引用,如果不存在就向字符串“驻留池”中添加该字符串,并且将该字符串的引用返回。这样一来在每次连接字符串时都有可能创建新的字符串对象(如果“驻留池”中不存在对应的字符串的话),从而导致了性能低下。
在String类有个方法专门用来检测“拘留池”中是否存在指定字符串引用的方法,这个方法就是IsInterned(string str)方法,如果存在这个引用则返回str的引用,如果不存在这个引用就返回null。
在需要多次连接字符串时可以考虑使用System.Text.StringBuilder对象,这是一个可变容量的字符串对象。在实例化StringBuilder对象时会指定一个容量(如果不显示指定,则系统默认会指定初始容量为16,如果在程序中最终连接后的容量大于这个值可以自行指定一个较大的值作为初时容量,这样也能提高性能),在进行添加、插入及替换等修改操作时如果不超过容量,则会直接在缓冲区中操作,如果超过容量则会重新分配一个更大的缓冲区,并将原来的数据复制到新缓冲区。
下面通过一个控制台的例子来演示一下String类和StringBuilder类的区别,代码如下:
using System.Text;
public class StringDemo
{
public static void Main()
{
//如果存在"abc"字符串则a="abc",否则a=null;
string a = string.IsInterned("abc");
string b = new StringBuilder().Append("as").Append("df").ToString();
if (string.IsNullOrEmpty(a))
{
Console.WriteLine(a + "存在'拘留池’中");
}
else
{
Console.WriteLine(a + "不存在'拘留池’中");
}
if (string.IsNullOrEmpty(b))
{
Console.WriteLine(b + "存在'拘留池’中");
}
else
{
Console.WriteLine(b + "不存在'拘留池’中");
}
int count = 9000;
TimeSpan ts1 = StringConcat(count);
TimeSpan ts2 = StringBuilderConcat(count);
Console.WriteLine("使用了String来连接"+count.ToString()+"次耗时"+ts1.TotalMilliseconds+"毫秒");
Console.WriteLine("使用StringBuilder来连接" + count.ToString() + "次耗时" + ts2.TotalMilliseconds + "毫秒");
Console.ReadLine();
}
/// <summary>
/// 用String对象来连接字符串
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
public static TimeSpan StringConcat(int count)
{
DateTime start = DateTime.Now;
string text = string.Empty;
for (int i = 0; i < count; i++)
{
text=text+"0"+i;
}
DateTime end = DateTime.Now;
return end - start;
}
/// <summary>
/// 用StringBuilder对象来连接字符串
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
public static TimeSpan StringBuilderConcat(int count)
{
DateTime start = DateTime.Now;
StringBuilder text = new StringBuilder();
for (int i = 0; i < count; i++)
{
text.Append("0"+i);
}
DateTime end = DateTime.Now;
return end - start;
}
}
这个程序的运行效果如图21-2所示:
图21-2 String类和StringBuilder类连接字符串的运行效果
21.1.3 类型转换优化
在开发中经常会遇到类型转换的问题,一种情况是由字符串类型转换成数值类型,另一种情况是存在继承关系或者实现关系的类之间进行类型转换。在上面的两种转换中如果存在不能转换的情况,则会抛出异常,在引发和处理异常时将消耗大量的系统资源和执行时间。引发异常是为了确实处理异常情况,而不是为了处理可预知的时间或控制流(这一点尤其要注意,不要在代码中来使用异常进行流程控制)。
21.1.3.1 字符串类型向值类型转换
在.NET Framework2.0版本以前将字符串类型转换成数值类型都是使用Parse()方法,如int.Parse("123")、char.Parse("a")及bool.Parse("TrueString")等等,如果出现了指定的字符串不能转换成相应的数值类型时就会抛出异常,可能会对性能造成不良的影响。在.NET Framework2.0及以后版本中增加了TryParse()方法,减小了性能问题。TryParse()方法使用了两个参数:第一个参数是要转换成数值类型的字符串,第二个参数是一个带有out关键字的参数,并且这个方法有返回值,指示指定的字符串是否能转换成相应的数据类型。如果指定的字符串能转换成相应的数据类型则方法返回true,out参数就是指定字符串转换成相应数值的结果,否则方法返回false,表示不能进行转换而不会抛出异常。
其用法如下面的代码所示:
string str1 = "123";
string str2 = "ed1";
//因为作为out参数,所以即使是据不变量也不用赋值
int number1;
//因为作为out参数,所以即使是据不变量也不用赋值
int number2;
//"123"能转换成int,所以b1=true,number1=123
bool b1 = int.TryParse(str1, out number1);
//"ed1"不能转换成int,所以b2=false
bool b2 = int.TryParse(str2, out number2);
21.1.3.2 引用类型之间转换
在引用类型之间转换有两种方式:强制转换和as转换。下面是强制转换的例子:
object d = "asdf";
//将d牵制转换成string类型
string e = (string)d;
同字符串类型转换成数值类型一样,如果不存在对应的转换关系也会抛出异常。为了避免引用类型之间转换抛出异常,可以使用as关键字来转换,如下面的代码:
object d ="123";
//下面的转换会抛出异常
StringBuilder e = (StringBuilder)d;
//下面的转换不会抛出异常,并且f为null
StringBuilder f = d as StringBuilder;
在C#中还有一个is关键字,它用来检查对象是否与给定类型兼容,如果兼容表达式的值为true,否则为false。例如下面的表达式:
//b1=true,因为string类实现了ICloneable接口
bool b1 = a is ICloneable;
//b2=true,因为string类是object类的派生类
bool b2 = a is object;
//b3=false,因为string类与StringBuilder类之间不存在派生或者实现关系
bool b3 = a is StringBuilder;
假如有A类型的变量a和B类型,对于A c=a as B这个转换,存在如下情况:如果A实现或者派生自B,那么上面的转换成功,否则转换不成功,c为null,并且不会抛出异常。
上面讲到的都是关于编码方面提高程序性能应该注意的实现,此外在编码过程中还应注意尽量减少装箱拆箱操作。装箱操作是指将值类型转换成引用类型,拆箱操作是指将引用类型转换成值类型,通过装箱操作使得值类型可以被视作对象。相对于简单的赋值而言,装箱和取消装箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。次之,取消装箱所需的强制转换也需要进行大量的计算。在向ArrayList这样的非范型集合中添加值类型元素时就会存在装箱过程,再从非范型集合中取出值类型的值就会存在拆箱过程。
21.1.4 使用Server.Transfer()方法
使用Server.Transfer()方法实现同一应用程序下不同页面间的重定向可以避免不必要的客户端页面重定向。它比Response.Redirect()方法性能要高,并且Server.Transfer()方法具有允许目标页从源页中读取控件值和公共属性值的优点。由于调用了这个方法之后浏览器上不会反应更改后的页的信息,因此它也适合以隐藏URL的形式向用户呈现页面,不过如果用户点击了浏览器上的“后退“按钮或者刷新页面有可能导致意外情况。
21.1.5 避免不必要的服务器往返
虽然使用服务器控件能够节省时间和代码,但是使用服务器控件有时间会增加页面的往返次数,如果在页面中使用了数据绑定控件,在默认情况下每次响应客户端回发而加载页面时都会重新绑定数据,其实在很多情况下这个过程是没有必要的,使用 Page.IsPostBack 避免对往返过程执行不必要的处理,这个在数据绑定控件那一章有所体现。
21.1.6 尽早释放对象
在.NET Framework中有很多类实现了IDisposable接口,实现了IDisposable接口的类中都会有一个Dispose()方法,当这些类的实例不再使用时,应及早调用该类的Dispose()方法以释放所占用的资源。
21.1.7 尽量减少服务器控件的使用
服务器控件在编程中使用起来确实方便,但是这种方便是牺牲了一定的性能为前提的,比如需要在页面某个地方显示一个字符串,这个字符串在任何时候都不会发生变化,那么可以在HTML代码中直接输出,还有有些表单要实现点击按钮之后清空表单输入,利用HTML中的重置按钮就可以完成这个功能,都没有必要使用服务器控件。
21.2 数据操作优化
数据操作优化方面主要是数据访问优化,主要有数据库连接对象使用、数据访问优化、优化SQL语句、使用缓存等。
21.2.1 数据库连接对象使用优化
对于数据库连接的使用始终遵循的一条原则是:尽可能晚打开数据库连接,尽可能早关闭数据库连接。这个在ADO.NET一章作过讲述,再次不在赘述。
除此之外,还可以使用数据库连接池来优化。连接到数据库通常需要几个需要很长时间的步骤组成,如建立物理通道(例如套接字或命名管道)、与服务器进行初次握手、分析连接字符串信息、由服务器对连接进行身份验证、运行检查以便在当前事务中登记等等。实际上,大多数应用程序仅使用一个或几个不同的连接配置。这意味着在执行应用程序期间,许多相同的连接将反复地打开和关闭。为了使打开的连接成本最低,ADO.NET 使用称为连接池的优化方法。连接池减少新连接需要打开的次数。池进程保持物理连接的所有权。通过为每个给定的连接配置保留一组活动连接来管理连接。只要用户在连接上调用 Open,池进程就会检查池中是否有可用的连接。如果某个池连接可用,会将该连接返回给调用者,而不是打开新连接。应用程序在该连接上调用 Close 时,池进程会将连接返回到活动连接池集中,而不是真正关闭连接。连接返回到池中之后,即可在下一个 Open 调用中重复使用。
池连接可以大大提高应用程序的性能和可缩放性。默认情况下,ADO.NET 中启用连接池。除非显式禁用,否则,连接在应用程序中打开和关闭时,池进程将对连接进行优化。在开发大型网站时可以更改默认的数据库连接池配置信息,例如可以增加数据库连接池的最大连接数(默认是100),如下面的代码就是将数据库连接池的最大连接数设为200:
Data Source=(local);Initial Catalog=AspNetStudy;User ID=sa;Password=sa;Pooling=true;Min Pool Size=0;Max Pool Size=200
当然也不是设置数据库连接池的最大连接数越大越好,实际上还会受其它因素的限制。
21.2.2 数据访问优化
如果对数据库中的数据不是需要经常读取,可以使用相应的DataReader对象来读取(如SqlDataReader、OleDbDataReader或OracleDataReader),在这种情况下使用DataReader对象会得到一定的性能提升。
此外,在数据访问时还可以使用存储过程。使用存储过程除了可以防范SQL注入之外,还可以提高程序性能和减少网络流量。存储过程是存储在服务器上的一组预编译的SQL语句,具有对数据库立即访问的功能,信息处理极为迅速。使用存储过程可以避免对命令的多次编译,在执行一次后其执行规划就驻留在高速缓存中,以后需要时只需直接调用缓存中的二进制代码即可。
21.2.3 优化SQL语句
在开发中除了从C#代码方面优化数据访问之外,还可以从SQL语句上优化数据访问。有人做过调查,在数据量大的库中进行数据访问,不同的人编写的SQL语句所花费的时间有可能相差上百倍,因此尽量让项目中对数据查询优化有经验的人编写SQL语句以提高程序性能。
在优化SQL语句时,有几条原则需要注意:
(1)尽量避免”select * from 表名”这样的SQL语句,特别是在表中字段比较多而只需要显示某几个字段数据的情况下更应该注意这个问题,比如针对SQL Server数据库来说,如果不需要显示或者操作表中的image、Text、ntext及xml这样的字段,就尽量不要出现在select语句中的字段列表中。
(2)尽量不要在查询语句中使用子查询。
(3)尽量使用索引。索引是与表或视图关联的磁盘上结构,可以加快从表或视图中检索行的速度。索引包含由表或视图中的一列或多列生成的键。这些键存储在一个结构中,使数据库可以快速有效地查找与键值关联的行。设计良好的索引可以减少磁盘 I/O 操作,并且消耗的系统资源也较少,从而可以提高查询性能。对于包含 SELECT、UPDATE 或 DELETE 语句的各种查询,索引会很有用。查询优化器使用索引时,搜索索引键列,查找到查询所需行的存储位置,然后从该位置提取匹配行。通常,搜索索引比搜索表要快很多,因为索引与表不同,一般每行包含的列非常少,且行遵循排序顺序。对于常用作where查询的字段可以建立索引以提高查询速度。注意,使用索引后会降低对表的插入、更新和删除速度,在一张表上也不宜建立过多的索引。
21.2.4 合理使用缓存
在ASP.NET中在不同级别提供了缓存功能,比如控件级和页面级及全局级都提供了缓存功能,在控件中或者页面中都可以通过@ OutputCache指令来使用缓存,这对于减少一些不经常变化并且比较耗时的操作的性能损耗很有用。
除此之外,还有System.Web.Caching.Cache类对提高程序性能也非常有用,虽然利用Session或者Application也能实现在内存中保存数据,但是在Session中保存的数据只能被单个用户使用,而在Application中使用的数据如果不手动释放就会一直保存在内存当中,利用Cache就完全克服了上面的缺点。Cache类提供了强大的功能,允许自定义缓存项及缓存时间和优先级等,在服务器内存不够用时会自动较少使用的或者优先级比较低的项以释放内存。另外还可以指定缓存关联依赖项,如果缓存关联依赖项发生改变缓存项就会实效并从缓存中移除。比如可以将一个经常要读取的文件的内容缓存起来,并在文件上保留一个依赖项,一旦文件内容发生变化就会从内存中移除缓存的文件内容,可以再次从文件中重新读取文件内容到缓存中,这样就保证了得到的文件内容是最新的。
下面就是一个在ASP.NET使用Cache的例子,页面的设计部分代码如下:
<!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>使用Cache的例子</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="txtContent" runat="server" Columns="50" Rows="10" TextMode="MultiLine"></asp:TextBox>
<asp:Button ID="btnGetFileContent" runat="server" OnClick="btnGetFileContent_Click"
Text="显示文件内容" />
</div>
</form>
</body>
</html>
页面的逻辑代码如下:
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Text;
using System.Web.Caching;
public partial class day21_CacheDemo : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void btnGetFileContent_Click(object sender, EventArgs e)
{
//从缓存中根据键读取,并使用as转换
string content = Cache["Content"] as string;
//如果不存在缓存项,则设置缓存
if (content== null)
{
String path = Server.MapPath("~/day21/Content.txt");
//注意这里没有使用FileStream和StreamReader来读取文件内容
string text = File.ReadAllText(path, Encoding.UTF8);
//创建缓存依赖项
CacheDependency dependency = new CacheDependency(path);
//创建当缓存移除时调用的方法
CacheItemRemovedCallback callBack = new CacheItemRemovedCallback(ItemRemovedCallBack);
//添加缓存,并且设定缓存过期时间为30分钟
Cache.Add("Content", text, dependency, Cache.NoAbsoluteExpiration, new TimeSpan(0, 30, 0), CacheItemPriority.Default, callBack);
txtContent.Text = text;
}
else
{
txtContent.Text = content;
}
}
/// <summary>
/// 当从缓存中移除缓存项时要调用的方法
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="reason"></param>
private void ItemRemovedCallBack(string key, object value, CacheItemRemovedReason reason)
{
String path = Server.MapPath("~/day21/CacheChangeLog.txt");
string log=string.Format("键为{0}的缓存项于{1}被删除,原因是:{2}。\r\n",
key,DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ms"),reason.ToString());
//如果不存在文件下面的方法会创建文件并将内容写入到文件
//如果存在文件就会追加
File.AppendAllText(path, log);
}
}
为了达到演示效果,还需要在页面所在文件夹下创建一个txt文件,文件内容为“这是《ASP.NET夜话》第二十一章中进行的Cache测试文件。”,并用UTF-8编码保存,如图21-3所示:
图21-3 创建Cotent.txt文件并保存成UTF-8编码
运行页面之后的效果如图21-4所示:
图21-4 页面的初始运行效果
点击“显示文件内容”按钮,因为首次显示在缓存中并不存在文件内容,所以会将文件内容读取出来并在文本框中显示。在文本框显示了文件内容之后,手动修改Content.txt文件的内容并保存,然后再次点击显示文件内容,在文本框中就会显示文件的最新内容了,如图21-5所示:
图21-5 在修改文件内容之后重新显示文件内容的效果
由此可见,使用了缓存关联依赖项之后确实能移除缓存数据,下次显示时因为缓存项已经被移除所以会重新读取文件内容并进行缓存,因而就能看到最新的文件内容。同时,还能在Content.txt文件所在文件夹下看到一个CacheChangeLog.txt文件,这个文件的内容如图21-6所示:
图21-6 CacheChangeLog.txt文件的内容
总之,Cache对象是一个使用起来很灵活的对象,可以满足复杂条件下的数据缓存要求,合理使用缓存有时候能提高数量级的性能。不过在使用缓存时也要注意一些事项,比如不要缓存频繁变化和很少使用的数据,也不要将数据缓存的时间设置过短,否则不但不能提高性能,严重情况下反而会降低性能。
21.3 配置优化
不光从程序代码上能提高程序性能,调用某些设置也能提高程序的性能。
21.3.1 禁用调试模式
在开发过程中因为经常要进行调试,所以配置将Web网站项目设置成允许调试模式,在部署网站时一定要禁用此模式,在运行过程中使用调试模式将会使网站的性能受到很大影响。禁用调试模式是在web.config文件中设置,如下面的代码就是禁用调试模式:
<compilation debug="false">
21.3.2 合理使用ViewState
在ASP.NET中为了维护服务器控件在HTTP请求之间维护其状态启用了服务器控件的视图状态。服务器控件视图状态为其所有属性的累计值,这些值在后面的请求处理中作为变量传递给隐藏的字段,一般情况下这些值是经过了一定的编码或者加密处理之后再保存到隐藏字段中的,在后面的请求中再经过反向处理得到原始的值,这些处理都是需要花费时间的。有时候为了提高应用程序的性能,在不需维护服务器控件的情况下可以禁用视图状态(默认情况下是启用视图状态的),特别是在使用数据绑定控件时一定要注意这个问题。
下面是一个使用GridView控件来显示数据的例子,这个文件其实在讲述GridView控件时曾用来作为例子,现在又再次用来作为例子,代码如下:
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
//默认显示第一页,不过在GridView中第一页的页索引是0
//注意:在C#中集合里的索引也都是以0开始
BindGridView(0);
}
}
//指定绑定页面的数据
private void BindGridView(int pageIndex)
{
//实例化Connection对象
SqlConnection connection = new SqlConnection("Data Source=zhou\\sql2000;Initial Catalog=AspNetStudy;Persist Security Info=True;User ID=sa;Password=sa");
//实例化Command对象
SqlCommand command = new SqlCommand("select * from UserInfo", connection);
SqlDataAdapter adapter = new SqlDataAdapter(command);
DataTable data = new DataTable();
adapter.Fill(data);
#region 注意这部分代码可以在设计视图中设置,不必写在代码里
gvUserList.AllowPaging = true;//设置允许自动分页
//gvUserList.AutoGenerateColumns = false;//设置不允许自动绑定列
gvUserList.PageSize = 5;//设置每页显示5条记录
#endregion
gvUserList.DataSource = data;
gvUserList.PageIndex = pageIndex;//设置当前显示第几页
gvUserList.DataBind();
}
//翻页事件
protected void gvUserList_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
//指定新页面,重新绑定数据
BindGridView(e.NewPageIndex);
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>用GridView显示数据的例子(注意:采用单页模式)</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="gvUserList" runat="server" AutoGenerateColumns="False" OnPageIndexChanging="gvUserList_PageIndexChanging">
<Columns>
<asp:BoundField DataField="UserId" HeaderText="编号" />
<asp:HyperLinkField DataNavigateUrlFields="UserId" DataNavigateUrlFormatString="DetailsViewDemo.aspx?UserId={0}"
DataTextField="RealName" HeaderText="查看" />
<asp:BoundField DataField="UserName" HeaderText="用户名" >
<ItemStyle BackColor="#C0FFC0" />
</asp:BoundField>
<asp:BoundField DataField="RealName" HeaderText="真实姓名" />
<asp:BoundField DataField="Age" HeaderText="年龄" />
<asp:CheckBoxField DataField="Sex" HeaderText="男" />
<asp:BoundField DataField="Mobile" HeaderText="手机" />
<asp:TemplateField HeaderText="电子邮件">
<AlternatingItemTemplate>
<a href='emailto:<%#Eval("Email") %>'>发邮件给<%#Eval("RealName") %></a>
</AlternatingItemTemplate>
<ItemTemplate>
<%#Eval("Email") %>
</ItemTemplate>
</asp:TemplateField>
</Columns>
<EmptyDataTemplate>
温馨提示:当前没有任何记录哦。
</EmptyDataTemplate>
</asp:GridView>
</div>
</form>
</body>
</html>
运行这个页面会看到如图21-7所示的效果。
图21-7 用GridView显示用户信息
如果看到这个页面的最终生成的HTML页面源代码,会看到如图21-8显示的效果:
图21-8 查看最终HTML页面的源代码效果
从上图可以看出为了维护GridView控件的状态所花费的代价是客观的,如果不需要维护这个状态可以禁用GridView控件的视图状态(在大部分情况下都用不着)。具体操作是切换到设计视图下,在页面中选中GridView控件然后在属性窗口中找到EnableViewState属性并将其设为false,如图21-9所示:
图21-9 禁用GridView控件的视图状态
禁用GridView的视图状态之后再次运行页面并查看HTML源代码,会看到如图21-10所示的效果:
图21-10 禁用GridView的视图状态之后生成的HTML源代码
从图21-8和图21-10对比情况来看,禁用了视图状态之后生成的HTML代码大小大大减少,不断降低了网络流量传输,还减轻了服务器的负担。如果要禁用整个页面的服务器控件的视图状态,可以在页面@Page指令中添加EnableViewState="false"值,如本实例中页面禁用页面中所有控件视图状态之后,页面的@Page指令如下:
<%@ Page Language="C#" EnableViewState="false" %>
最后要提示一点的是,禁用服务器控件的视图状态之后有可能有些服务器控件的内置功能无法使用,例如在本利中禁用GridView控件的视图状态之后就没有办法使用内置分页功能了。
21.3.3 合理选择会话状态存储方式
ASP.NET中支持多种会话状态数据存储方式,这是一个SessionStateMode枚举值,如下表所示:
数据存储模式
说明
InProc 模式
将会话状态数据保存在ASP.NET进程中,只能用于单个服务器,Web服务器重其后不能保留会话状态,这是默认的存储方式
StateServer 模式
将会话状态存储在单独的进程中,可将会话状态用于多个Web服务器,并在Web服务器重启后还能保留会话状态
SQLServer 模式
会话数据保存到SQL Server 数据库中,可将会话状态用于多个Web服务器,并在Web服务器重启后还能保留会话状态
Custom 模式
自定义存储方式
Off 模式
禁用会话状态
上面的每种方式都有字节的优点和缺点,InProc 模式是存储会话状态最快的解决方案,如果不需要保存大量数据并且不需要Web服务器重其后还能保存数据,则建议使用这种方式。例如下面的设置就是用了使用了进程内存储会话状态,并且设置Session的超期时间为20分钟。
<configuration>
<system.web>
<sessionState mode="InProc" timeout="20"/>
</system.web>
</configuration>
21.4 总结
上面提到的优化方法是笔者平时做性能优化时常用到的优化方法,除了上面的方法之外还可以通过使用更好的算法来提高程序性能。尽管优化的方法各异,但目的只有一个:今最大可能在满足程序要求的情况下提高性能。除了上面的方法之外还有其它的方式,希望读者朋友在开发和学习中自己多积累这方面的经验。