优化你的DiscuzNT3.0,让它跑起来(4)asp.net 缓存和死锁

 注:本文仅针对 DiscuzNT3.0, sqlserver 2000版本,其他版本请勿对号入座.

 经过前面的几次优化之后我们的论坛终于稳定了一段时间,大概半年之后我们的论坛迎来了每天大约50万的pv,这时候论坛有开始出现了问题。症状是这样的:

管理员发现,网站经常会打不开, 但是也不报错,好像永远一直在打开,直到浏览器认为它打不开了,这样的症状每天会出现几次,而且越来越频繁。每次发生这样的情况过后一般iis的事件查看器都会asp.net有死锁提示,于是我知道,我终于遇上传说中的死锁了,每次有死锁迹象的时候我都跟踪了一下sqlserver,发现数据库是正常的,那看来就是asp.net这边的问题了。

可是DiscuzNT这么大的一个论坛,里面包含了十几个项目,项目如此之多,代码量如此之大,到底哪里出了问题呢,一下子还真不好定位。还好微软给我们提供了两个很不错的工具,windbg 和 IIS Diagnostics,winddbg是用来调试内存的工具,而IIS Diagnostics则是抓取内存的好工具,我也正是借助这两个工具才快速定位到了问题,不过很遗憾的是我抓取的dump文件由于时间太久,竟然找不到了,所以现在暂时无法一展它们的风采。(不过后续会介绍windbg的用法,因为它真的帮了我大忙)

那到底是哪里引发的死锁呢,废话不多说,看看下面的代码就知道了,Discuz.Cache.DNTCache.cs 类文件

  1     /// <summary>

 2           ///  构造函数
 3           ///   </summary>
 4           private  DNTCache()
 5          {
 6               if (MemCachedConfigs.GetConfig()  !=   null   &&  MemCachedConfigs.GetConfig().ApplyMemCached)
 7                  applyMemCached  =   true ;
 8 
 9               if  (applyMemCached)
10                  cs  =   new  MemCachedStrategy();
11               else
12              {
13                  cs  =   new  DefaultCacheStrategy();
14 
15                  objectXmlMap  =  rootXml.CreateElement( " Cache " );
16                   // 建立内部XML文档.
17                  rootXml.AppendChild(objectXmlMap);
18 
19                   // LogVisitor clv = new CacheLogVisitor();
20                   // cs.Accept(clv);
21 
22                  cacheConfigTimer.AutoReset  =   true ;
23                  cacheConfigTimer.Enabled  =   true ;
24                  cacheConfigTimer.Elapsed  +=   new  System.Timers.ElapsedEventHandler( Timer_Elapsed ); // 重点看下这个方法
25                  cacheConfigTimer.Start();
26              }
27          }

 

看下这个方法 Timer_Elapsed 

1       private   static   void  Timer_Elapsed( object  sender, System.Timers.ElapsedEventArgs e)
2          {
3               if  ( ! applyMemCached)
4              {
5                   // 检查并移除相应的缓存项
6                  instance  =   CachesFileMonitor.CheckAndRemoveCache (instance); // 这个方法里持有一个锁
7             }

8         } 

 

看看这个方法 CachesFileMonitor.CheckAndRemoveCache 

  1     /// <summary>

 2           ///  检查和移除缓存
 3           ///   </summary>
 4           ///   <param name="instance"></param>
 5           ///   <returns></returns>
 6           public   static  DNTCache CheckAndRemoveCache(DNTCache instance) //
 7          {
 8               // 当程序运行中cache.config发生变化时则对缓存对象做删除的操作
 9              cachefilenewchange  =  System.IO.File.GetLastWriteTime(path);
10               if  (cachefileoldchange  !=  cachefilenewchange)
11              {                
12                   lock  (cachelockHelper)
13                  {
14                       if  (cachefileoldchange  !=  cachefilenewchange)
15                      {
16                           // 当有要清除的项时
17                          DataSet dsSrc  =   new  DataSet();
18                          dsSrc.ReadXml(path);
19                           foreach  (DataRow dr  in  dsSrc.Tables[ 0 ].Rows)
20                          {
21                               if  (dr[ " xpath " ].ToString().Trim()  !=   "" )
22                              {
23                                  DateTime removedatetime  =  DateTime.Now;
24                                   try
25                                  {
26                                      removedatetime  =  Convert.ToDateTime(dr[ " removedatetime " ].ToString().Trim());
27                                  }
28                                   catch
29                                  {
30                                      ;
31                                  }
32 
33                                   if  (removedatetime  >  cachefilenewchange.AddSeconds( - 2 ))
34                                  {
35                                       string  xpath  =  dr[ " xpath " ].ToString().Trim();
36                                       instance.RemoveObject (xpath,  false );  //  这个方法里持有第二个锁
37                                  }
38                              }
39                          }
40 
41                          cachefileoldchange  =  cachefilenewchange;
42 
43                          dsSrc.Dispose();
44                      }
45                  }
46              }
47               return  instance;
48          }

 

看看 

RemoveObject 方法:

 

 1       ///   <summary>
 2           ///  通过指定的路径删除缓存中的对象
 3           ///   </summary>
 4           ///   <param name="xpath"> 分级对象的路径 </param>
 5           ///   <param name="writeconfig"> 是否写入文件 </param>
 6           public   virtual   void  RemoveObject( string  xpath,  bool  writeconfig)
 7          {
 8               lock  (lockHelper)
 9              {
10                   try
11                  {
12                       if  (applyMemCached)
13                      {
14                           // 移除相应的缓存项
15                          cs.RemoveObject(xpath);
16                      }
17                       else
18                      {
19                           if  (writeconfig)
20                          {
21                               CachesFileMonitor.UpdateCacheItem (xpath); // 这里再次持有锁
22                          }
23 
24                          XmlNode result  =  objectXmlMap.SelectSingleNode(PrepareXpath(xpath));
25                           // 检查路径是否指向一个组或一个被缓存的实例元素
26                           if  (result.HasChildNodes)
27                          {
28                               // 删除所有对象和子结点的信息
29                              XmlNodeList objects  =  result.SelectNodes( " *[@objectId] " );
30                               string  objectId  =   "" ;
31                               foreach  (XmlNode node  in  objects)
32                              {
33                                  objectId  =  node.Attributes[ " objectId " ].Value;
34                                  node.ParentNode.RemoveChild(node);
35                                   // 删除对象
36                                  cs.RemoveObject(objectId);
37                              }
38                          }
39                           else
40                          {
41                               // 删除元素结点和相关的对象
42                               string  objectId  =  result.Attributes[ " objectId " ].Value;
43                              result.ParentNode.RemoveChild(result);
44                              cs.RemoveObject(objectId);
45                          }
46                      }
47                      
48                  }
49                   catch // 如出错误表明当前路径不存在
50                  {}
51 
52             }

53         } 

 

再来看看方法UpdateCacheItem:

 1       ///   <summary>
 2           ///  更新或插入相应的缓存路径
 3           ///   </summary>
 4           ///   <param name="xpath"></param>
 5           public   static   void  UpdateCacheItem( string  xpath)
 6          {
 7              DataTable dt  =   new  DataTable( " cachetableremove " );
 8              dt.Columns.Add( " xpath " , System.Type.GetType( " System.String " ));
 9              dt.Columns.Add( " removedatetime " , System.Type.GetType( " System.DateTime " ));
10 
11               // 当有要清除的项时
12              DataSet dsSrc  =   new  DataSet();
13               lock (cachelockHelper)
14              {
15                  dsSrc.ReadXml(path);
16 
17                   bool  nohasone  =   true ;
18                   foreach  (DataRow dr  in  dsSrc.Tables[ 0 ].Rows)
19                  {
20                       if  (dr[ " xpath " ].ToString().Trim()  ==  xpath)
21                      {
22                          dr[ " removedatetime " =  DateTime.Now.ToString();
23                          nohasone  =   false ;
24                           break ;
25                      }
26                  }
27 
28                   if  (nohasone)
29                  {
30                      DataRow dr  =  dsSrc.Tables[ 0 ].NewRow();
31                      dr[ " xpath " =  xpath;
32                      dr[ " removedatetime " =  DateTime.Now.ToString();
33                      dsSrc.Tables[ 0 ].Rows.Add(dr);
34                  }
35 
36                  dsSrc.WriteXml(path);
37                  dsSrc.Dispose();
38             }

39         }

 

通过上面的代码的红字体部分我们可以看到,如果DNTCache 启动它的定时器,它将会顺序持有如下锁

cachelockHelper —— 》 CachesFileMonitor.CheckAndRemoveCache() 持有

    |

    |

lockHelper  ——》 instance.RemoveObject()持有

 

    |

    |

cachelockHelper  ——》 CachesFileMonitor.UpdateCacheItem() 持有

 

如果刚好有一种情况持有所的顺序跟上面相反,比如持有顺序 lockHelper —— cachelockHelper —— lockHelper  ,而且这两种情况同时发生了,那死锁就这样产生了,那有没有这样的情况?有!

我们来看看 Discuz.Cache.DNTCache.cs 的 GetCacheService():

 

 1       ///   <summary>
 2           ///  单体模式返回当前类的实例
 3           ///   </summary>
 4           ///   <returns></returns>
 5           public   static  DNTCache GetCacheService()
 6          {
 7               if  (instance  ==   null )
 8              {
 9                   lock (lockHelper)
10                  {
11                       if  (instance  ==   null )
12                      {
13                          instance  =  applyMemCached  ?   new  DNTCache() :  CachesFileMonitor.CheckAndRemoveCache( new  DNTCache());
14                      }
15                  }
16              }
17 
18               return instance;

19         } 

 

看上面的 lock (lockHelper), 是不是很眼熟啊,对了,他刚好是上面第一种持有锁情况里面出现的第二个锁,只要这个 

Discuz.Cache.DNTCache.
GetCacheService() 和 CachesFileMonitor.CheckAndRemoveCache() 同时被启动,那死锁就产生了,而
Discuz.Cache.DNTCache.
GetCacheService()是返回当前缓存的实例,可以说他时时刻刻都在被调用,你可以尝试搜索一下
Discuz.Cache.DNTCache.
GetCacheService(),你会发现他无处不在,当 
Discuz.Cache.DNTCache.
GetCacheService()Discuz.Cache.DNTCache.Timer_Elapsed() 同时发生,死锁也就产生了。

 

 

既然问题找到了,那该如何解决呢,我看了一下,这个

Discuz.Cache.DNTCache里面用到的lock作用就是为了保证唯一性,但是我发现若不是唯一好像也没什么影响,于是我把lock注释了,试运行一段时间之后,发现并没有什么影响,于是一直沿用至今。

 

 

本篇是本系列里针对DiscuzNT的c#代码做出优化的第一篇文章,比较遗憾的是第一大功臣windbg未能华丽登场,不过它以后还有机会。欲知windbg是如何登场的,敬请期待下回分解。



 

    

 

你可能感兴趣的:(asp.net)