zoukankan      html  css  js  c++  java
  • .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,每次使用,每次新建。

    非常感谢韩磊在解决这个问题中给予指点。
  • 相关阅读:
    CF 319C
    日常---区域赛临近
    poj 3728 The merchant 倍增lca求dp
    zoj 3742 Delivery 好题
    zoj 3717 Balloon 2-sat
    CF 163E. e-Government ac自动机+fail树+树状数组
    CF 335B
    hdu 4739 状压DP
    hdu 4738 桥
    Hibernate中的继承映射
  • 原文地址:https://www.cnblogs.com/dudu/p/19628.html
Copyright © 2011-2022 走看看