优化你的DiscuzNT3.0,让它跑起来,4asp.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是如何登场的,敬请期待下回分解。