数据库死锁,优化你的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 ///
2 /// 构造函数 3 ///
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 ///
2 /// 检查和移除缓存 3 ///
4 ///
5 /// 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 /// 2 /// 通过指定的路径删除缓存中的对象 3 /// 4 /// 分级对象的路径
5 /// 是否写入文件
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 /// 2 /// 更新或插入相应的缓存路径 3 /// 4 ///
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 /// 2 /// 单体模式返回当前类的实例 3 /// 4 /// 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是如何登场的,敬请期待下回分解。


Tags:  死锁产生的条件 什么是死锁 死锁的必要条件 db2死锁 数据库死锁

延伸阅读

最新评论

发表评论