假设,我们设计一个博客系统,其中包含一个用户表users,它用来存储用户的一些信息,具体的表设计如下:
CREATE TABLE USERS ( ID INT IDENTITY(1,1) NOT NULL PRIMARY KEY, USER_LOGIN VARCHAR(64), USER_PASS VARCHAR(64), USER_NICENAME VARCHAR(50), USER_EMAIL VARCHAR(100), USER_URL VARCHAR(100), USER_REGISTERED DATETIME DEFAULT GETDATE(), USER_ACTIVATION_KEY VARCHAR(60), USER_STATUS INT, DISPLAY_NAME VARCHAR(250) )
我们通过客户端代码实现数据插入到USERS表中,代码如下:
NeoUserModal user=new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True"); con.Open(); StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME)"); INSERT_USERS.Append(string.Format("VALUES('{0}','{1}','{2}','{3}',{4},'{5}')",user.User_Login,user.User_Pass,user.User_NiceName,user.Email,user.User_Status,user.Displaoy_Name)); SqlCommand cmd = new SqlCommand(); cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.ExecuteNonQuery();
代码中存在的问题,在客户端创建一个数据库链接,它需要占用一定的系统资源,当操作完毕之后,我们需要释放占用的系统资源,我们手工释放,代码如下:
NeoUserModal user=new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True"); con.Open(); StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME)"); INSERT_USERS.Append(string.Format("VALUES('{0}','{1}','{2}','{3}',{4},'{5}')",user.User_Login,user.User_Pass,user.User_NiceName,user.Email,user.User_Status,user.Displaoy_Name)); SqlCommand cmd = new SqlCommand(); cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.ExecuteNonQuery(); cmd.Dispose(); con.Close(); con.Dispose();
假如释放SqlCommand对象时候,报错,那么后面的SqlConnection的对象就得不到关闭和释放,我们知道对try catch finally的语法,可以通过catch扑捉异常,无论是否发生异常都会执行finally里面的语句,即可以在finally里面释放资源,代码如下:
NeoUserModal user = new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; SqlConnection con = null; SqlCommand cmd = null; try { con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True"); con.Open(); StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME)"); INSERT_USERS.Append(string.Format("VALUES('{0}','{1}','{2}','{3}',{4},'{5}')", user.User_Login, user.User_Pass, user.User_NiceName, user.Email, user.User_Status, user.Displaoy_Name)); cmd = new SqlCommand(); cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.ExecuteNonQuery(); } catch (Exception ex) { } finally { if (cmd != null) { cmd.Dispose(); } if (con != null) { con.Dispose(); } }
.NET中提供了using来释放资源,代码如下:
NeoUserModal user = new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME)"); INSERT_USERS.Append(string.Format("VALUES('{0}','{1}','{2}','{3}',{4},'{5}')", user.User_Login, user.User_Pass, user.User_NiceName, user.Email, user.User_Status, user.Displaoy_Name)); using (SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True")) { con.Open(); using (SqlCommand cmd = new SqlCommand()) { cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.ExecuteNonQuery(); } }
注:只有实现了IDisposable接口和重写了Dispose()方法可以实现using资源的释放。
在客户端代码中,我们使用拼接SQL语句方式实现数据写入,由于SQL语句是动态执行的,所以恶意用户可以通过拼接SQL的方式实施SQL注入攻击。
对于SQL注入攻击,我们可以通过以下方式防御:
- 正则表达校验用户输入
- 参数化存储过程
- 参数化SQL语句
- 添加数据库新架构
- LINQ to SQL接下来,我们将通过参数化SQL语句防御SQL注入攻击
NeoUserModal user = new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME)"); INSERT_USERS.Append(" VALUES(@USER_LOGIN,@USER_PASS,@USER_NICENAME,@USER_EMAIL,@USER_STATUS,@DISPLAY_NAME)"); using (SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True")) { con.Open(); using (SqlCommand cmd = new SqlCommand()) { SqlParameter[] op = { new SqlParameter("@USER_LOGIN",user.User_Login), new SqlParameter("@USER_PASS",user.User_Pass), new SqlParameter("@USER_NICENAME",user.User_NiceName), new SqlParameter("@USER_EMAIL",user.Email), new SqlParameter("@USER_STATUS",user.User_Status), new SqlParameter("@DISPLAY_NAME",user.Displaoy_Name) }; cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.Parameters.AddRange(op); cmd.ExecuteNonQuery(); } }
接下来,让我们简单的测试一下代码执行时间,首先我们在代码中添加方法Stopwatch.StartNew()和Stopwatch.Stop()来计算写入代码的执行时间,具体代码如下:
NeoUserModal user = new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; int index = 0; Stopwatch sw = new Stopwatch(); sw.Start(); while (index < 10000) { StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME)"); INSERT_USERS.Append(" VALUES(@USER_LOGIN,@USER_PASS,@USER_NICENAME,@USER_EMAIL,@USER_STATUS,@DISPLAY_NAME)"); using (SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True")) { con.Open(); using (SqlCommand cmd = new SqlCommand()) { SqlParameter[] op = { new SqlParameter("@USER_LOGIN",user.User_Login), new SqlParameter("@USER_PASS",user.User_Pass), new SqlParameter("@USER_NICENAME",user.User_NiceName), new SqlParameter("@USER_EMAIL",user.Email), new SqlParameter("@USER_STATUS",user.User_Status), new SqlParameter("@DISPLAY_NAME",user.Displaoy_Name) }; cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.Parameters.AddRange(op); cmd.ExecuteNonQuery(); } } index = index + 1; } sw.Stop(); Console.WriteLine(sw.Elapsed);
执行时间:00:00:06.2088262
- 数据库的性能开销:
当我们执行conn.Open()时,首先,必须建立物理通道(例如套接字或命名管道),必须与服务器进行初次握手,必须分析连接字符串信息,必须由服务器对连接进行身份验证,必须运行检查以便在当前事务中登记,等等
这一系列操作可能需要一两秒钟时间,如果我们每次执行conn.Open()都有进行这一系列操作是很耗费时间的,为了使打开的连接成本最低,ADO.NET使用称为连接池的优化方法。
连接池:减少新连接需要打开的次数,只要用户在连接上调用Open()方法,池进程就会检查池中是否有可用的连接,如果某个池连接可用,那么将该连接返回给调用者,而不是创建新连接;应用程序在该连接上调用Close()或Dispose()时,池进程会将连接返回到活动连接池集中,而不是真正关闭连接,连接返回到池中之后,即可在下一个Open调用中重复使用。
解析器的开销
当我们向SQL Server传递SQL语句INSERT INTO …时,它需要对SQL语句进行解析,由于SQL Server解析器执行速度很快,所以解析时间往往是可以忽略不计,但我们仍然可以通过使用存储过程,而不是直SQL语句来减少解析器的开销。
数据库连接
为了提供ACID(事务的四个特性),SQL Server必须确保所有的数据库更改是有序的。它是通过使用锁来确保该数据库插入、删除或更新操作之间不会相互冲突
由于,大多数数据库都是面向多用户的环境,当我们对User表进行插入操作时,也许有成千上百的用户也在对User表进行操作,所以说,SQL Server必须确保这些操作是有序进行的。
那么,当SQL Server正在做所有这些事情时,它会产生锁,以确保用户获得有意义的结果。SQL Server保证每条语句执行时,数据库是完全可预测的(例如:预测SQL执行方式)和管理锁都需要耗费一定的时间。
约束处理
在插入数据时,每个约束(如:外键、默认值、SQL CHECK等)需要额外的时间来检测数据是否符合约束;由于SQL Server为了保证每个插入、更新或删除的记录都符合约束条件,所以,我们需要考虑是否应该在数据量大的表中增加约束条件。
Varchar
VARCHAR是数据库常用的类型,但它也可能导致意想不到的性能开销;每次我们存储可变长度的列,那么SQL Server必须做更多的内存管理;字符串可以很容易地消耗数百字节的内存的,如果我们在一个VARCHAR列中设置索引,那么SQL Server执行B-树搜索时,就需要进行O(字符串长度)次比较,然而,整数字段比较次数只受限于内存延迟和CPU频率。
磁盘IO
SQL Server最终会将数据写入到磁盘中,首先,SQL Server把数据写入到事务日志中,当执行备份时,事务日志会合并到永久的数据库文件中;这一系列操作由后台完成,它不会影响到数据查询的速度,但每个事物都必须拥有属于自己的磁盘空间,所以我们可以通过给事务日志和主数据文件分配独立的磁盘空间减少IO开销,当然,最好解决办法是尽可能减少事务的数量。
正如大家所看到的,我们通过优化联接时间、 解析器的开销、 数据库联接、约束处理,、Varchar和磁盘IO等方法来优化数据库,接下来,我们将对前面的例子进行进一步的优化。
使用存储过程
前面例子中,我们把SQL代码直接Hardcode在客户端代码中,那么,数据库就需要使用解析器解析客户端中SQL语句,所以我们可以改用使用存储过程,从而,减少解析器的时间开销;更重要的一点是,由于SQL是动态执行的,所以我们修改存储过程中的SQL语句也无需重新编译和发布程序。
User表中的字段user_registered设置了默认值(GETDATE()),那么我们通过消除表默认值约束来提高系统的性能,简而言之,我们需要提供字段user_registered的值。
接下来,让我们省去User表中的默认值约束和增加存储过程,具体代码如下:
NeoUserModal user = new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; int index = 0; Stopwatch sw = new Stopwatch(); sw.Start(); while (index < 10000) { StringBuilder INSERT_USERS = new StringBuilder(); INSERT_USERS.Append("INSERT INTO USERS(USER_LOGIN,USER_PASS,USER_NICENAME,USER_EMAIL,USER_STATUS,DISPLAY_NAME,user_registered)"); INSERT_USERS.Append(" VALUES(@USER_LOGIN,@USER_PASS,@USER_NICENAME,@USER_EMAIL,@USER_STATUS,@DISPLAY_NAME,@user_registered)"); using (SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True")) { con.Open(); using (SqlCommand cmd = new SqlCommand()) { SqlParameter[] op = { new SqlParameter("@USER_LOGIN",user.User_Login), new SqlParameter("@USER_PASS",user.User_Pass), new SqlParameter("@USER_NICENAME",user.User_NiceName), new SqlParameter("@USER_EMAIL",user.Email), new SqlParameter("@USER_STATUS",user.User_Status), new SqlParameter("@DISPLAY_NAME",user.Displaoy_Name), new SqlParameter("@user_registered",DateTime.Now) }; cmd.Connection = con; cmd.CommandText = INSERT_USERS.ToString(); cmd.Parameters.AddRange(op); cmd.ExecuteNonQuery(); } } index = index + 1; } sw.Stop(); Console.WriteLine(sw.Elapsed);
耗费时间:00:00:05.9925965
使用SqlBulkCopy
NeoUserModal user = new NeoUserModal(); user.User_Login = "QIAOX"; user.User_Pass = "ABC563"; user.User_NiceName = "SCOTT"; user.Email = "qiaox@hmpl.com.cn"; user.User_Status = 0; user.Displaoy_Name = "乔霞"; DataTable dt = new DataTable(); dt.Columns.Add("user_login", typeof(string)); dt.Columns.Add("user_pass", typeof(string)); dt.Columns.Add("user_nicename", typeof(string)); dt.Columns.Add("user_email", typeof(string)); dt.Columns.Add("user_status", typeof(int)); dt.Columns.Add("display_name", typeof(string)); for (int i = 0; i < 10000; i++) { DataRow dr = dt.NewRow(); dr["user_login"] = user.User_Login; dr["user_pass"] = user.User_Pass; dr["user_nicename"] = user.User_NiceName; dr["user_email"] = user.Email; dr["user_status"] = user.User_Status; dr["display_name"] = user.Displaoy_Name; dt.Rows.Add(dr); } Stopwatch sw = new Stopwatch(); sw.Start(); using (SqlConnection con = new SqlConnection("Data Source=.;Initial Catalog=dbtest;Integrated Security=True")) { con.Open(); SqlBulkCopy sbc = new SqlBulkCopy(con); sbc.ColumnMappings.Clear(); foreach (DataColumn dc in dt.Columns) { sbc.ColumnMappings.Add(dc.ColumnName, dc.ColumnName); } sbc.DestinationTableName = "[dbo].[users]"; sbc.WriteToServer(dt); } sw.Stop(); Console.WriteLine(sw.Elapsed);
耗费时间:00:00:01.6786963