1,需求背景:

       内存溢出的问题最初调试时,发现是生成Excel时用StringBuilder变量缓存数据,当数据过大时,导致内存溢出(解决);之后再次测试,发现由于取数据时涉及到多表查询,之前的逻辑是从各表依次取出数据,都中和至一个DataSet中,然后生成文件,发现在中和的过程中,也可能会发生内存溢出,所以修改为每查寻一个表,往文件中写入一次数据(解决);再次测试,发现当单个表中数据过大时(20W行*265列),查询的时候也会直接发生内存溢出(在服务器上已经测试)。最终经过与XX和XX协商之后,决定采用每次取一定量的数据(可能是1W、2W、5W,以测试速度最快的为准),涉及到跨多个表,多次取数据多次写数据的方式来测底解决内存溢出问题。经过最后结果实际验证,此方法不会出现内存溢出问题。

          考虑到下载历史数据几乎遍布我们每一个项目,但是之前的处理方式,几乎全部都是一次查询,一次生成文件,这样迟早要导致像上述几种问题,最终内存溢出,而且对于大数据量,采用多线程分批导出、分批写数据的方式,也可以在一定程度上缩短时间,所以将该Bug的解决过程与大家分享。

2,错误现象

多线程导出Java_多线程

3,详细设计实现过程

1>.设计思路图

 

多线程导出Java_内存溢出_02

上图是解决内存溢出问题的具体设计思路。可以看到其中主要有两块逻辑,代表了两个线程,左边的线程专门用于分页提取数据并加载至缓存,右边的线程专门用于从缓存提取数据写入文件中。当两个线程都运行完毕时,代表文件生成完毕。然后再进行压缩,将生成的压缩文件的路径返回给用户进行下载。

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.        }