1,需求背景:
内存溢出的问题最初调试时,发现是生成Excel时用StringBuilder变量缓存数据,当数据过大时,导致内存溢出(解决);之后再次测试,发现由于取数据时涉及到多表查询,之前的逻辑是从各表依次取出数据,都中和至一个DataSet中,然后生成文件,发现在中和的过程中,也可能会发生内存溢出,所以修改为每查寻一个表,往文件中写入一次数据(解决);再次测试,发现当单个表中数据过大时(20W行*265列),查询的时候也会直接发生内存溢出(在服务器上已经测试)。最终经过与XX和XX协商之后,决定采用每次取一定量的数据(可能是1W、2W、5W,以测试速度最快的为准),涉及到跨多个表,多次取数据多次写数据的方式来测底解决内存溢出问题。经过最后结果实际验证,此方法不会出现内存溢出问题。
考虑到下载历史数据几乎遍布我们每一个项目,但是之前的处理方式,几乎全部都是一次查询,一次生成文件,这样迟早要导致像上述几种问题,最终内存溢出,而且对于大数据量,采用多线程分批导出、分批写数据的方式,也可以在一定程度上缩短时间,所以将该Bug的解决过程与大家分享。
2,错误现象:
3,详细设计实现过程
1>.设计思路图
上图是解决内存溢出问题的具体设计思路。可以看到其中主要有两块逻辑,代表了两个线程,左边的线程专门用于分页提取数据并加载至缓存,右边的线程专门用于从缓存提取数据写入文件中。当两个线程都运行完毕时,代表文件生成完毕。然后再进行压缩,将生成的压缩文件的路径返回给用户进行下载。
2>. 难点分析
1,线程阻塞与文件生成完毕
由于查询数据线程和写入文件线程是同时在运行的,生成一个文件可能需要多次读写,当写入线程去缓存取数据时,此时缓存里面可能有数据可能没有数据,这都不能做为线程结束的标志,那么究竟什么时候才能判断文件已经生成完毕呢?
只有当查询线程结束并且缓存里面没有数据时,才能知道该文件需要的全部数据已经写入了该文件中,才能断定文件生成完毕,然后终止两个线程进行下一步操作。
2,在顺序的多表中分页读取数据
虽然该功能过程比较繁琐,但因为该功能在分页查询历史数据时已经完成,再次可以直接拿来调用即可。在最终完成该功能之后的测试中发现这块功能还是不够准确,所以进行了小范围的修改,如果大家有兴趣可以去研究一下,有兴趣给我留言求源码!
3>. 核心源代码
1,开启多线程
首先开启查询数据线程和写入数据线程,并在开启两个线程之后进行阻塞,一直等文件生成完毕才能解除阻塞继续向下执行压缩等步骤。源代码如下:
1. /// <summary>
2. /// 生成Excel文件逻辑结构
3. /// </summary>
4. ///<returns></returns>
5. publicstring
6. {
7. //查询数据线程启动
8. vardueryDataThread = new
9. {IsBackground =true, Priority =ThreadPriority.Highest, Name = "QueryData"};
10. dueryDataThread.Start();
11. //写入数据至文件线程启动
12. varwriteDataThread = new
13. {IsBackground=true, Priority = ThreadPriority.Highest, Name= "WriteData"};
14. writeDataThread.Start();
15. manual.WaitOne();//阻塞线程
16. //压缩文件,返回路径
17. returnstring.IsNullOrEmpty(fullpath)? "" :newPrintExcelStreamWrite().RarFile(fullpath);
18. }
2,查询数据至缓存
循环查询出数据加载到缓存队列,用于写入文件。源代码如下:
19. /// <summary>
20. /// 查询数据至缓存
21. /// </summary>
22. void
23. {
24. ………………..
25. //查询到在开始时间和结束时间之间的表集合
26. var tables =TableAllocationQueue.Create().GetTableAllocations(dbseq, condition.BeginTime,condition.EndlessTime);
27. if (tables ==null) thrownewException("该终端的起始时间内没有有效的数据表");
28. //记录下每个表中的有效数据的行数
29. var listcount = newList<int>();
30. //组合查询每个表中数据行数SQL语句
31. var cmdQueryRowCountString =QueryRowCountString(condition);
32. foreach (TableAllocationEntity entityin
33. {
34. ………………..//查询每个表中数据行数
35. if (ds !=null)listcount.Add(int.Parse(ds.Tables[0].Rows[0]["COUNT"].ToString()));
36. }
37. //得到有效数据的总行数,用于分页
38. int
39. if (rows <= 0) thrownewException("该终端的起始时间内有效数据的行数为0");
40. //如果查询到数据才生成文件
41. fullpath= newPrintExcelStreamWrite(condition.Vin).CreateFullPath();
42. int
43. //根据总的数据行数,得出需要多少次才能取完数据
44. int.TryParse((rows / PageSize + 1).ToString(),out
45. if
46. {
47. //每次取数据的行数
48. condition.PageSize= PageSize;
49. //开始循环取数据
50. for (int
51. {
52. //表明为分页的页数
53. condition.PageIndex= i;
54. //分页查询数据
55. vards = GetPageData(listcount, dbseq, condition, tables);
56. if (ds !=null&& ds.Tables.Count > 0 && ds.Tables[0].Rows.Count > 0)
57. {
58. lock
59. {
60. queue.Enqueue(ds);//将数据放入队列中
61. }
62. }
63. }
64. }
65. }
66. ………………..
67. finally
68. {
69. queryFlag= true; //标识查询线程结束
70. }
71. }
3, 从缓存取数写入文件
循环从缓存队列依次取出缓存数据写入文件,当查询结束并且缓存中没有数据时,表明数据已全部写入文件中,生成文件过程结束,此时需要结束线程,解除阻塞。源代码如下图:
72. /// <summary>
73. ///从缓存取数写入文件
74. ///<summary>
75. void
76. {
77. //通过查询条件获得需要查询的数据列,并通过MD5值解析出对应的通道名称
78. ………………..
79. bool flag =true;
80. //开始循环取数
81. while
82. {
83. ………………..
84. begintime= DateTime.Now;
85. DataSetds;
86. //如果缓存中有数据,循环取出队列头上的数据并写入文件中
87. while (queue !=null&& queue.Count > 0)
88. {
89. lock
90. {
91. ds= queue.Dequeue();
92. }
93. newPrintExcelStreamWrite(condition.Vin).CreateExcel(fullpath, ds, columnLst);
94. }
95. //当查询线程结束并且缓存队列中没有数据,表明文件已生成完毕
96. if (queryFlag && (queue ==null
97. {
98. manual.Set();//解除阻塞
99. flag= false;//停止循环取数,结束该线程
100. }
101. …………………………….
102. }
103. }