.Text中SqlParameter引起的Bug

   这篇文章中讨论的Bug是最近博客园频繁出现的两个异常:
1、"ArgumentException The SqlParameter with ParameterName '@EntryID' is already contained by another SqlParameterCollection."
2、"ArgumentException The SqlParameter with ParameterName
'@ItemCount' is already contained by another SqlParameterCollection."
    这个Bug我在.Text中的Bug 文章中已经讨论过,但当时并没有找出问题的真正原因,文章中的解决方法也没有解决问题。最后,在韩磊的指点下才消除了这个Bug。在这篇文章中, 我谈谈自己的一些心得。
    当出现上述两个异常时, 我首先想到的是找出异常抛出的位置。根据异常的内容,异常应该发生在数库访问代码中, 也就是SQLHelper.cs, .Text中所有的数据库访问都是通过SqlHelper。但在SqlHelper中并没有对异常进行捕获, 只有一处try...catch语句, 所做也只是在catch中关闭SqlConnection。为了发现异常在哪抛出的,需要增加捕获异常的代码,根据异常内容,可以判断出是在执行ExecuteReader过程中出现的异常。我就在ExecuteReader(SqlConnection connection, SqlTransaction transaction, CommandType commandType, string commandText, SqlParameter[] commandParameters, SqlConnectionOwnership connectionOwnership)中增加了try...catch代码,在catch中通过Logger.LogManager.CreateExceptionLog(e,"ExecuteReader Exception");将异常写入日志。在这里我犯一个错误:.Text中的日志是存在数据库中,CreateExceptionLog是要向数据库写入日志信息,但在catch中, 数据库访问已经出现了异常,再进行数据库写入操作,只会继续抛出异常。我这样寻找异常发生的位置显然是徒劳无获的,而且增加了新的异常,更不利于问题的解决。
    对于这两个异常,我自然而然认为问题出在SqlHelper中,所以我要在SqlHelper捕获到异常发生的位置。“.Text中的Bug ”文章中想通过在finally中cmd.Parameters.Clear();来解决问题,可是异常仍然存在。所以我决定首先要捕获发生异常的位置,我发现捕获异常代码的问题后,改成了将.Text的日志写入xml文件,这样在发生数据库操作异常,也能将异常写入日志。经过这样的更改,我终于找到了异常发生的位置,异常发生在SqlHelper.AttachParameters中,在循环执行command.Parameters.Add(p);时产生了异常。
    我开始仔细分析command.Parameters.Add(p);,实际就是SqlParameter.Add(SqlParameter value),用Reflector查看了一下其中的代码,没什么收获。这时我开始怀疑是多线程并发执行command.Parameters.Add(p)引起的问题。可command并不是共享资源,在每次执行ExecuteReader时,command都是一个新的实例(SqlCommand cmd = new SqlCommand();).不应该存在同步问题。
    我百思不得其解,于是与韩磊交流了这个问题,开始他也没想到解决的方法。后来,突然他问我是不是只有
“@EntryID”与“@ItemCount”会出现错误,其他的存储过程参数没有出现?根据日志,我的回答是“是”。然后,他告诉问题出在.Text的SqlDataProvider中,他以前遇到过并且解决了这个问题,只不过一时忘记了。
    问题出在SqlDataProvider中的两个私有静态成员DefaultEntryQueryParameter、DefaultEntryParameters,类型都是SqlParameter[]。它们在SqlDataProviderr 的构造函数中被初始化:
    DefaultEntryQueryParameters = BuildDefaultEntryQueryParameters();
    DefaultEntryParameters = BuildDefaultEntryParameters();
解决方法就是去掉构造函数中的初如化,在每处调用DefaultEntryQueryParameters或DefaultEntryParameters的地方,重新初始化它们,也就是使它们指向新的SqlParameter[]实例。更改代码最简单的方法就是将这两个私有成员改成属性:
public static
SqlParameter[] DefaultEntryParameters
       
{
           
get

           
{
               
return
BuildDefaultEntryParameters();
            }

        }
public static
SqlParameter[] DefaultEntryQueryParameters
       
{
           
get

           
{
               
return
BuildDefaultEntryQueryParameters();
            }

        }

下面,我来分析一下原因,不对之处请大家指正。
既然异常是在执行command.Parameters.Add(p);产生的,那我们要首先分析一下这里为什么会抛出异常?
用Reflector要查看一下SqlParameterCollection.Add的代码:

public SqlParameter Add(SqlParameter value)
{
this
.OnSchemaChanging();
this
.AddWithoutEvents(value);
return
value;

}


继续看看AddWithoutEvents的代码:

private void AddWithoutEvents(SqlParameter value)
{
this.Validate(-1
, value);
value.Parent
= this
;
this
.ArrayList().Add(value);

}


这里的value.Parent = this
;应该引起我们的注意,参数value的Parent属性在SqlParameterCollection.Add
中被
改变,这就使SqlParameter value与SqlParameterCollection关联起来,一个SqlParameter value只能
同时属于一个SqlParameterCollection。那我们再看看Validate(-1
, value):

internal void Validate(int index, SqlParameter value)
{
if (value == null
)
{
throw ADP.ParameterNull("value", this, this
.ItemType);

}

if (value.Parent != null)
{
if (this !=
value.Parent)
{
throw ADP.ParametersIsNotParent(this.ItemType, value.ParameterName, this
);

}

if (index != this.IndexOf(value))
{
throw ADP.ParametersIsParent(this.ItemType, value.ParameterName, this
);

}


}

string text1 = value.ParameterName;
if (!
ADP.IsEmpty(text1))
{
return
;

}

index
= 1;
do

{
text1
= string.Concat("Parameter"
, index.ToString());
index
= (index + 1
);

}

while ((-1 != this.IndexOf(text1)));
value.ParameterName
=
text1;

}

  从上面的代码就可以看出异常是如何产生的,如果value被另外一个SqlParameterCollection使用(this != value.Parent),
就会引发异常。

那为什么出现SqlParameterCollection使用同一个SqlParameter的情况?

  罪魁祸首就是两个私有静态成员DefaultEntryQueryParameter、DefaultEntryParameters,私有静态成员被类的所有实例共享。在SqlDataProvider的不同实例的生命周期中, 都共享这两个静态成员。当SqlDataProvider的多个实例同时执行command.Parameters.Add(p)操作时,如果都用到DefaultEntryQueryParameter或DefaultEntryParameters,就会引发异常"...is already contained by another SqlParameterCollection."
  解决这个问题的方法除了前面的每次调用DefaultEntryQueryParameter或
DefaultEntryParameters,重新创建SqlParameter[],也可以将DefaultEntryQueryParameter与DefaultEntryParameters变成非静态私有成员,但这种在\方法在多线程的情况下,也会出现同样的问题。最安全的方法就是每次使用SqlParameter,都重新创建SqlParameter的实例。 
  这个bug一直存在.Text中,那为什么现在才发现?而且有很多.Text的网站为什么没有发现这个Bug?因为这个Bug只会出
现在ExecuteReader中,所以即使发生异常,对系统没什么影响,只要重新刷新一下就行了。而且这个异常只会出现在SqlDataProvider的多个实例同时执行command.Parameters.Add(p)操作时,同时发生的概率与网站的访问量有关。以前博客园很少出现这个异常,最近因为博客园访问量变大,同时执行command.Parameters.Add(p)的概率变高了,所以异常出现的次数也变多了。

从这个Bug中,我们应该吸取两个教训:
1、慎用私有静态成员。
2、安全地使用SqlParameter,每次使用,每次新建。

非常感谢韩磊在解决这个问题中给予指点。
posted @ 2004-06-30 09:06  dudu  阅读(4850)  评论(7编辑  收藏  举报