SQL Server[转]SQL Server 高性能写入的有总结

正文转自:http://www.cnblogs.com/rush/archive/2012/08/31/2666090.html

1.1.1 摘要

于付出进程遭到,我们经常会逢系统特性瓶颈问题,而滋生这同样题目原因可以多多,有或是代码不够迅速、有或是硬件还是网络问题,也发生或是数据库设计之问题。

本篇博文将对部分常用的数据库性能调休方法开展介绍,而且,为了编制高效的SQL代码,我们要掌握一些主干代码优化的技艺,所以,我们拿从局部核心优化技术进行介绍。

本文目录

  • 代码中之题材
  • 数据库性能开销
  • 使用存储过程
  • 使用数据库事务
  • 使用SqlBulkCopy
  • 应用说明参数

1.1.2 正文

设若,我们若统筹一个博客系统,其中蕴蓄一个用户表(User),它因此来存储用户的账户称、密码、显示名称和登记日期等信息。

由时日之关联,我们都拿User表设计好了,它概括账户称、密码(注意:这里没设想隐私信息之加密存储)、显示名称和登记日期等,具体计划如下:

-- =============================================
-- Author:        JKhuang
-- Create date: 7/8/2012
-- Description:    A table stores the user information.
-- =============================================
CREATE TABLE [dbo].[jk_users](
     -- This is the reference to Users table, it is primary key.
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [user_login] [varchar](60) NOT NULL,
    [user_pass] [varchar](64) NOT NULL,
    [user_nicename] [varchar](50) NOT NULL,
    [user_email] [varchar](100) NOT NULL,
    [user_url] [varchar](100) NOT NULL,

    -- This field get the default from function GETDATE().
    [user_registered] [datetime] NOT NULL CONSTRAINT [DF_jk_users_user_registered]  DEFAULT (getdate()),
    [user_activation_key] [varchar](60) NOT NULL,
    [user_status] [int] NOT NULL CONSTRAINT [DF_jk_users_user_status]  DEFAULT ((0)),
    [display_name] [varchar](250) NOT NULL
)

SQL Server 1

图1 Users表设计

点,我们定义了Users表,它蕴含账户名、密码、显示名称和登记日期等10只字段,其中,ID是一个自增的主键,user_resistered用来记录用户的登记时间,它装了默认值GETDATE()。

联网下去,我们将透过客户端代码实现数据存储到Users表中,具体的代码如下:

//// Creates a database connection.
var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString());
conn.Open();

//// This is a massive SQL injection vulnerability, 
//// don't ever write your own SQL statements with string formatting!
string sql = String.Format(
      @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)
        VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')",
      userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey);
var cmd = new SqlCommand(sql, conn);
cmd.ExecuteNonQuery();

//// Because this call to Close() is not wrapped in a try/catch/finally clause, 
//// it could be missed if an exception occurs above.  Don't do this!
conn.Close();

代码中之题目

方,我们使用还普通不了的ADO.NET方式贯彻数据勾勒副功能,但大家是不是察觉代码有问题或者可改善之地方呢?

率先,我们当客户端代码中,创建一个数据库连接,它用占用一定的系统资源,当操作结束后咱们要自由占用的系统资源,当然,我们好手动释放资源,具体实现如下:

//// Creates a database connection.
var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString());
conn.Open();

//// This is a massive SQL injection vulnerability, 
//// don't ever write your own SQL statements with string formatting!
string sql = String.Format(
      @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)
        VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')",
      userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey);
var cmd = new SqlCommand(sql, conn);
cmd.ExecuteNonQuery();

//// If throws an exception on cmd dispose.
cmd.Dispose();
//// conn can't be disposed.
conn.Close();
conn.Dispose();

而,在放SqlCommand资源时抛来十分,那么以它后的资源SqlConnection将得无至释放。我们密切想当起特别时,可以经try/catch捕获异常,所以无论是否发生甚都可以应用finally检查资源是否曾经刑满释放了,具体落实如下:

SqlCommand cmd = null;
SqlConnection conn = null;
try
{
    //// Creates a database connection.
    conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString());
    conn.Open();

    //// This is a massive SQL injection vulnerability, 
    //// don't ever write your own SQL statements with string formatting!
    string sql = String.Format(
          @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)
        VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')",
          userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey);
    cmd = new SqlCommand(sql, conn);
    cmd.ExecuteNonQuery();
}
finally
{
    //// Regardless of whether there is an exception,
    //// we will dispose the resource. 
    if (cmd != null) cmd.Dispose();
    if (conn != null) conn.Dispose();
}

经地方的finally方式处理了异常情况是格外普遍的,但为更安全释放资源,使得我们多了finally和if语句,那么是否有更简洁之艺术实现资源的平安释放吧?

实际,我们可采取using语句实现资源的放走,具体实现如下:

using语句:定义一个范围,将当斯限外释放一个要么多单对象。

string sql = String.Format(
      @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)
        VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}')",
              userLogin, userPass, userNicename, userEmail, userStatus, displayName, userUrl, userActivationKey);

//// Creates a database connection.
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()))
using (var cmd = new SqlCommand(sql, conn))
{
    //// Your code here.
}

上面的代码应用了using语句实现资源的放,那么是否富有目标还足以以using语句实现自由也?

惟有生品种实现了IDisposable接口并且再写Dispose()方法好应用using语句实现资源自由,由于SqlConnection和SqlCommand实现了IDisposable接口,那么我们可以采用using语句实现资源自由及异常处理。

以客户端代码中,我们下拼接SQL语句方式贯彻数据写入,由于SQL语句是动态执行之,所以恶意用户可经拼接SQL的方执行SQL注入攻击。

对于SQL注入攻击,我们可由此以下办法守护:

  • 正则达校验用户输入
  • 参数化存储过程
  • 参数化SQL语句
  • 加上数据库新架
  • LINQ to SQL

连接下,我们拿通过参数化SQL语句防御SQL注入攻击,大家也得以用另外的办法防御SQL注入攻击,具体实现代码如下:

//// Creates a database connection.
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN1"].ToString()))
{
    conn.Open();
    string sql = string.Format(
             @"INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, 
                user_status,display_name, user_url, user_activation_key)");

    using (var cmd = new SqlCommand(sql, conn))
    {
        //// Parameterized SQL to defense injection attacks
        cmd.Parameters.Add("@user_login", userLogin);
        cmd.Parameters.Add("@user_pass", userPass);
        cmd.Parameters.Add("@user_nicename", userNicename);
        cmd.Parameters.Add("@user_email", userEmail);
        cmd.Parameters.Add("@user_status", userStatus);
        cmd.Parameters.Add("@display_name", displayName);
        cmd.Parameters.Add("@user_url", userUrl);
        cmd.Parameters.Add("@user_activation_key", userActivationKey);
        cmd.ExecuteNonQuery();
    }
}

上面通过参数化SQL语句和using语句对代码进行改进,现在代码的可读性更胜了,而且为避免了SQL注入攻击与资源自由等问题。

连片下,让我们简要的测试一下代码执行时间,首先我们以代码中补充加方Stopwatch.StartNew()和Stopwatch.Stop()来计量写副代码的实践时间,具体代码如下:

    //// calc insert 10000 records consume time.
    var sw = Stopwatch.StartNew();

    //// Creates a database connection.
    using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString()))
    {
        conn.Open();
        int cnt = 0;
        while (cnt++ < 10000)
        {
            string sql = string.Format(@"INSERT INTO jk_users 
                 (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key)
                 VALUES (@user_login, @user_pass, @user_nicename, @user_email, @user_status, @display_name, @user_url, @user_activation_key)");

            using (var cmd = new SqlCommand(sql, conn))
            {
                //// Parameterized SQL to defense injection attacks
                cmd.Parameters.Add("@user_login", userLogin);
                cmd.Parameters.Add("@user_pass", userPass);
                cmd.Parameters.Add("@user_nicename", userNicename);
                cmd.Parameters.Add("@user_email", userEmail);
                cmd.Parameters.Add("@user_status", userStatus);
                cmd.Parameters.Add("@display_name", displayName);
                cmd.Parameters.Add("@user_url", userUrl);
                cmd.Parameters.Add("@user_activation_key", userActivationKey);
                cmd.ExecuteNonQuery();
            }
        }
    }

    sw.Stop();
}

面,我们通往数据库中描绘副了10000漫漫数,执行时间吧
7.136秒(我之机器坏破了),这样系统性能还是得以满足众多铺的需要了。

若,用户请求量增大了,我们尚能够保证系统能满足要求为?事实上,我们无应有满足于现有的系统特性,因为咱们理解代码的推行效率还发出大要命的升级换代空间。

紧接下去,将越来越介绍代码改善的法门。

SQL Server 2

希冀2 数目形容副Users表

为了要数据库获得更快的描写副速度,我们不能不了解数据库在进行勾勒副操作时之根本耗时。

数据库性能开销

连接时间

当我们实施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表中的默认值约束和长存储过程,具体代码如下:

-- =============================================
-- Author:        JKhuang
-- Create date: 08/16/2012
-- Description:    Creates stored procedure to insert
-- data into table jk_users.
-- =============================================
ALTER PROCEDURE [dbo].[SP_Insert_jk_users] 
    @user_login varchar(60), 
    @user_pass varchar(64), 
    @user_nicename varchar(50), 
    @user_email varchar(100), 
    @user_url varchar(100), 
    @user_activation_key varchar(60),
    @user_status int, 
    @display_name varchar(250)

AS
BEGIN
    SET NOCOUNT ON;

-- The stored procedure allows SQL server to avoid virtually all parser work
INSERT INTO jk_users 
       (user_login, user_pass, user_nicename, user_email, user_status,display_name, user_url, user_activation_key, user_registered)
       VALUES (@user_login, @user_pass, @user_nicename, @user_email, @user_status, @display_name, @user_url, @user_activation_key, GETDATE());
END

方我们定义了储存过程SP_Insert_jk_users向表中插数据,当我们再度履行代码时,发现数插入的时刻缩短为6.7401秒。

SQL Server 3

图3数目写入时间

使数据库事务

思想数据是否可拉开写副到数据库被,是否可以批量地写入呢?如果同意延迟一段时间才写副到数据库被,那么我们可以用Transaction来推延数量写入。

数据库事务是数据库管理体系执行进程遭到之一个逻辑单位,由一个少的数据库操作序列构成。
SQL Server确保业务执行成功后,数据写入到数据库被,反之,事务将回滚。

使我们本着数据库进行十次等独自的操作,那么SQL
Server就用分配十不成锁开销,但如把这些操作都封装在一个事情中,那么SQL
Server只需要分配一浅锁开销。

    //// calc insert 10000 records consume time.
    var sw = Stopwatch.StartNew();

    //// Creates a database connection.
    using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString()))
    {
        conn.Open();
        int cnt = 0;
        SqlTransaction trans = conn.BeginTransaction();
        while (cnt++ < 10000)
        {
            using (var cmd = new SqlCommand("SP_Insert_jk_users", conn))
            {
                //// Parameterized SQL to defense injection attacks
                cmd.CommandType = CommandType.StoredProcedure;

                //// Uses transcation to batch insert data.
                //// To avoid lock and connection overhead.
                cmd.Transaction = trans;
                cmd.Parameters.Add("@user_login", userLogin);
                cmd.Parameters.Add("@user_pass", userPass);
                cmd.Parameters.Add("@user_nicename", userNicename);
                cmd.Parameters.Add("@user_email", userEmail);
                cmd.Parameters.Add("@user_status", userStatus);
                cmd.Parameters.Add("@display_name", displayName);
                cmd.Parameters.Add("@user_url", userUrl);
                cmd.Parameters.Add("@user_activation_key", userActivationKey);
                cmd.ExecuteNonQuery();
            }
        }

        //// If no exception, commit transcation.
        trans.Commit();
    }

    sw.Stop();
}

SQL Server 4

贪图4 数据写入时间

使用SqlBulkCopy

由此采用工作封装了写入操作,当我们重运行代码,发现数目写入的进度大大提高了,只待4.5109秒,由于一个业务只需要分配一次于锁资源,减少了分配锁与数据库联网的耗时。

理所当然,我们可为祭SqlBulkCopy实现大气数码的写入操作,首先我们创建数量实施,然后用SqlBulkCopy的WriteToServer()方法将数据行批量写副到表中,具体贯彻代码如下:

/// <summary>
/// Gets the data rows.
/// </summary>
/// <returns></returns>
DataRow[] GetDataRows(int rowCnt)
{
    //// Creates a custom table.
    var dt = new DataTable("jk_users");
    dt.Columns.Add(new DataColumn("user_login", typeof(System.String)));
    dt.Columns.Add(new DataColumn("user_pass", typeof(System.String)));
    dt.Columns.Add(new DataColumn("user_nicename", typeof(System.String)));
    dt.Columns.Add(new DataColumn("user_email", typeof(System.String)));
    dt.Columns.Add(new DataColumn("user_url", typeof(System.String)));
    dt.Columns.Add(new DataColumn("user_registered", typeof(System.DateTime)));
    dt.Columns.Add(new DataColumn("user_activation_key", typeof(System.String)));
    dt.Columns.Add(new DataColumn("user_status", typeof(System.Int32)));
    dt.Columns.Add(new DataColumn("display_name", typeof(System.String)));

    //// Initializes data row.
    var dr = dt.NewRow();
    dr["user_login"] = "JK_RUSH";
    dr["user_pass"] = "D*<1C2jK#-";
    dr["user_nicename"] = "JK";
    dr["user_email"] = "jkhuang@gamil.com";
    dr["user_status"] = 1;
    dr["display_name"] = "JK_RUSH";
    dr["user_url"] = "http://www.cnblogs.com/rush";
    dr["user_activation_key"] = "347894102386";
    dr["user_registered"] = DateTime.Now;

    //// Creates data row array.
    var dataRows = new DataRow[rowCnt];
    for (int i = 0; i < rowCnt; i++)
    {
        dataRows[i] = dr;
    }

    return dataRows;
}

前,我们定义了GetDataRows()方法用来创造数量实施,首先我们创建了一个自定义表,给该表添加相应的数据列,这里我们管数据列都命名吧相应为表中列名,当然,名字可以无等同,这时我们不怕来一个疑云了,那么数据库如何管打定义数据列和表中数据列对应起来也?其实,我们得调用ColumnMappings.Add方法成立从从定义数据列和表中数据列的相应关系,接下去,我们调用SqlBulkCopy的WriteToServer()方法将数据行写入表中。

//// Creates 10001 data rows. 
var dataRows = GetDataRows(10001);
var sw = Stopwatch.StartNew();

//// Creates a database connection.
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString()))
{
    conn.Open();
    using (var bulkCopy = new SqlBulkCopy(conn))
    {
        //// Maping the data columns.
        bulkCopy.ColumnMappings.Add("user_login", "user_login");
        bulkCopy.ColumnMappings.Add("user_pass", "user_pass");
        bulkCopy.ColumnMappings.Add("user_nicename", "user_nicename");
        bulkCopy.ColumnMappings.Add("user_email", "user_email");
        bulkCopy.ColumnMappings.Add("user_url", "user_url");
        bulkCopy.ColumnMappings.Add("user_registered", "user_registered");
        bulkCopy.ColumnMappings.Add("user_activation_key", "user_activation_key");
        bulkCopy.ColumnMappings.Add("user_status", "user_status");
        bulkCopy.ColumnMappings.Add("display_name", "display_name");
        bulkCopy.DestinationTableName = "dbo.jk_users";
        //// Insert data into datatable.
        bulkCopy.WriteToServer(dataRows);
    }
    sw.Stop();
}

SQL Server 5

 

 

 

祈求5 数据写入时间

面,我们通过业务和SqlBulkCopy实现数量批量状副数据库中,但实在,每次我们调用cmd.ExecuteNonQuery()方法都见面发生一个来来往往消息,从客户端应用程序到数据库被,所以我们纪念是不是是一样种艺术只有发送一潮信息就得写副的操作也?

使用说明参数

如,大家利用SQL Server 2008,它提供一个新的效益表变量(Table
Parameters)可以用整表数据汇集成一个参数传递给存储过程还是SQL语句。它的注意性能开销是以数据集中成参数(O(数据量))。

现行,我们修改前的代码,在SQL Server中定义我们的表变量,具体定义如下:

-- =============================================
-- Author:        JKhuang
-- Create date: 08/16/2012
-- Description:    Declares a user table paramter.
-- =============================================
CREATE TYPE jk_users_bulk_insert AS TABLE (
    user_login varchar(60),
    user_pass varchar(64),
    user_nicename varchar(50),
    user_email varchar(100),
    user_url varchar(100),
    user_activation_key varchar(60),
    user_status int,
    display_name varchar(250)
)

面,我们定义了一个表参数jk_users_bulk_insert,接着我们定义一个储存过程接受表参数jk_users_bulk_insert,具体定义如下:

-- =============================================
-- Author:        JKhuang
-- Create date: 08/16/2012
-- Description:    Creates a stored procedure, receive
-- a jk_users_bulk_insert argument.
-- =============================================
CREATE PROCEDURE sp_insert_jk_users 
@usersTable jk_users_bulk_insert READONLY 
AS

INSERT INTO jk_users (user_login, user_pass, user_nicename, user_email, user_url, 
user_activation_key, user_status, display_name, user_registered) 

SELECT user_login, user_pass, user_nicename, user_email, user_url, 
user_activation_key, user_status, display_name, GETDATE() 
FROM @usersTable

收受我们于客户端代码中,调用存储过程又用说明作为参数方式传送给存储过程。

var sw = Stopwatch.StartNew();
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN2"].ToString()))
{
    conn.Open();
    //// Invokes the stored procedure.
    using (var cmd = new SqlCommand("sp_insert_jk_users", conn))
    {
        cmd.CommandType = CommandType.StoredProcedure;

        //// Adding a "structured" parameter allows you to insert tons of data with low overhead
        var param = new SqlParameter("@userTable", SqlDbType.Structured) { Value = dt };
        cmd.Parameters.Add(param);
        cmd.ExecuteNonQuery();
    }
}

sw.Stop();

现,我们再度履行写副操作发现写副效率及SqlBulkCopy相当。

1.1.3总结

本文通过博客系统用户表设计之例子,介绍我们以统筹过程遭到爱犯的不当和代码的欠缺,例如:SQL注入、数据库资源自由等题材;进而使一些常用之代码优化技巧对代码进行优化,并且通过分析数据库写副的习性开销(连接时间、解析器、数据库连接、约束处理、VARCHAR和磁盘IO),我们以存储过程、数据库事务、SqlBulkCopy和阐明参数等措施降低数据库的开销。

参考

[1] http://beginner-sql-tutorial.com/sql-query-tuning.htm

[2] http://www.dzone.com/links/r/sql_optimization_tipsquestions.html

[3]
http://blackrabbitcoder.net/archive/2010/11/11/c.net-little-wonders—a-presentation.aspx

[4]
http://www.altdevblogaday.com/2012/05/16/sql-server-high-performance-inserts/

关于作者:

[作者]:JK_Rush从事.NET开发和热衷于开源高性能系统设计,通过博文交流和分享经验,欢迎转载,请保留原文地址,谢谢。 [出处]: http://www.cnblogs.com/rush/ [本文基于]: 署名-非商业性使用 3.0 许可协议发布,欢迎转载,演绎,但是必须保留本文的署名 JK_Rush (包含链接),且不得用于商业目的。如您有任何疑问或者授权方面的协商,请与我联系 。

 

相关文章