13°

使用内存映射文件MMF实现大数据量导出时的内存优化

前言

     导出功能几乎是所有应用系统必不可少功能,今天我们来谈一谈,如何使用内存映射文件MMF进行内存优化,本文重点介绍使用方法,相关原理可以参考文末的连接

实现

     我们以单次导出一个excel举例(csv同理),excel包含1~n个sheet,在每个sheet中存储的按行和列的坐标在单元格存储具体数据,如果我们要使用MMF,第一个要考虑的就是如何将整个excel合理的存储到MMF中。这里我们引入MMF两个对象:

MemoryMappedFile          --表示内存映射文件
MemoryMappedViewAccessor  --表示随机访问的内存映射文件视图

使用MemoryMappedFile.CreateNew(string mapName, long capacity)可以得到一个指定名称和指定大小的内存映射文件,以下简称mmf
* 这里需要注意的是capacity为long类型,以字节为单位,通过计算可知文件大小上限为1G
使用mmf.CreateViewAccessor(long offset, long size)可以得到一个从指定位置开始的指定大小空间的访问器,以下简称accessor
* 这里同样需要注意的是size的大小,如果加上offset超过文件大小,会报System.UnauthorizedAccessException: Access to the path is denied.

考虑到数据体积和管理成本,这里使用mmf存储sheet,使用accessor存储一行数据
* 这里有个需要注意的是mmf不能存储引用类型,包括字符串...,折衷先将string转为char[]然后使用WriteArray方法存储,考虑到取数据的时候同样需要使用char[]数组,而数组必须指定长度,我们将数组长度和具体数据都存起来,这样取数据时候的索引也可以计算出来了

 

 

 这面是具体的实现代码:

        //添加外部引用防止被自动GC
        public List<MemoryMappedFile> mmfs = new List<MemoryMappedFile>();
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;summary&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> 写入
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/summary&gt;</span>
    <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> WriteMMF()
    {
        </span><span style="color: #0000ff;">for</span>(<span style="color: #0000ff;">var</span> f = <span style="color: #800080;">1</span>; f &lt;= <span style="color: #800080;">3</span>; f ++<span style="color: #000000;">)
        {
            </span><span style="color: #008000;">//</span><span style="color: #008000;">每一个File相当于一个excel的一个sheet(一个患者一行)</span>
            <span style="color: #0000ff;">var</span> mmf = MemoryMappedFile.CreateNew($<span style="color: #800000;">"</span><span style="color: #800000;">mmftest{f}</span><span style="color: #800000;">"</span>, <span style="color: #800080;">1024</span> * <span style="color: #800080;">1024 * 1024</span>); <span style="color: #008000;">//</span><span style="color: #008000;">文件大小最大为1G</span>

            <span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">var</span> row = <span style="color: #800080;">0</span>; row &lt; <span style="color: #800080;">10</span>; row++<span style="color: #000000;">)
            {
                </span><span style="color: #008000;">//</span><span style="color: #008000;">每一个ViewAccessor相当于excel的一行</span>
                <span style="color: #0000ff;">var</span> accessor = mmf.CreateViewAccessor(row * <span style="color: #800080;">1024</span> * <span style="color: #800080;">1024</span>, <span style="color: #800080;">1024</span> * <span style="color: #800080;">1024</span>); <span style="color: #008000;">//</span><span style="color: #008000;">通过具体数据量计算空间,offset位移为每个size的长度,size为1M</span>
                <span style="color: #0000ff;">var</span> index = <span style="color: #800080;">0</span><span style="color: #000000;">;
                </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">var</span> i = <span style="color: #800080;">0</span>; i &lt; <span style="color: #800080;">16384</span>; i++<span style="color: #000000;">)
                {
                    </span><span style="color: #008000;">//</span><span style="color: #008000;">相当于一行的每一个cell</span>
                    <span style="color: #0000ff;">var</span> buffer = ASCIIEncoding.UTF8.GetBytes($<span style="color: #800000;">"</span><span style="color: #800000;">测试第{row}行第{i}个单元格~!</span><span style="color: #800000;">"</span><span style="color: #000000;">);
                    </span><span style="color: #0000ff;">var</span> length =<span style="color: #000000;"> buffer.Length;
                    accessor.Write(index, length);
                    accessor.WriteArray(index </span>+ <span style="color: #800080;">4</span>, buffer, <span style="color: #800080;">0</span><span style="color: #000000;">, length);
                    index </span>+= (length + <span style="color: #800080;">4</span><span style="color: #000000;">);
                }
            }
            mmfs.Add(mmf);
        }
    }

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;summary&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> 读取
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/summary&gt;</span>
    <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> ReadMMF()
    {
        </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">var</span> f = <span style="color: #800080;">1</span>; f &lt;= <span style="color: #800080;">3</span>; f++<span style="color: #000000;">)
        {
            </span><span style="color: #0000ff;">using</span> <span style="color: #0000ff;">var</span> mmf = MemoryMappedFile.OpenExisting($<span style="color: #800000;">"</span><span style="color: #800000;">mmftest{f}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
            </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">var</span> row = <span style="color: #800080;">0</span>; row &lt; <span style="color: #800080;">10</span>; row++<span style="color: #000000;">)
            {
                </span><span style="color: #0000ff;">using</span> <span style="color: #0000ff;">var</span> accessor = mmf.CreateViewAccessor(row * <span style="color: #800080;">1024</span> * <span style="color: #800080;">1024</span>, <span style="color: #800080;">1024</span> * <span style="color: #800080;">1024</span>); <span style="color: #008000;">//</span><span style="color: #008000;">通过写数据同时做的记录控制大小</span>
                <span style="color: #0000ff;">var</span> index = <span style="color: #800080;">0</span><span style="color: #000000;">;
                </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">var</span> i = <span style="color: #800080;">0</span>; i &lt; <span style="color: #800080;">16384</span>; i++<span style="color: #000000;">)
                {
                    </span><span style="color: #0000ff;">var</span> size =<span style="color: #000000;"> accessor.ReadInt32(index);
                    </span><span style="color: #0000ff;">var</span> buffer = <span style="color: #0000ff;">new</span> <span style="color: #0000ff;">byte</span><span style="color: #000000;">[size];
                    accessor.ReadArray(index </span>+ <span style="color: #800080;">4</span>, buffer, <span style="color: #800080;">0</span><span style="color: #000000;">, size);
                    </span><span style="color: #0000ff;">var</span> result =<span style="color: #000000;"> ASCIIEncoding.UTF8.GetString(buffer);
                    Console.WriteLine(result);
                    index </span>+= (size + <span style="color: #800080;">4</span><span style="color: #000000;">);
                }
            }
        }

        mmfs </span>= <span style="color: #0000ff;">null</span><span style="color: #000000;">;
    }</span></pre> 

          * 这里有个需要注意的点是,如果不在外部引用mmf,如果创建多个mmf,只有最新一个能通过OpenExisting方法打开,其他的都报System.IO.FileNotFoundException,猜测是资源被释放掉了

          * 还有个需要注意的点是,一定要计算好数据体积,不要超过mmf上限,使用accessor的时候也要注意,数据量实在太大可以考虑将一个sheet拆成多个mmf,或者将一行数据拆成多个accessor

    这样就可以实现从数据库获然后处理再存储到载体的流程,整个过程中内存使用控制在一个比较低的水平,当然,这是使用时间换空间,相应的导出时间会延长

    顺便说一下,原本考虑后续使用epplus进行excel生成,后来发现npoi也和poi一样有SXSSFWorkbook对象,可以流式读取数据,配合内存映射文件可以实现整个导出过程

 

相关连接参考:

https://docs.microsoft.com/en-us/dotnet/api/system.io.memorymappedfiles?view=netframework-4.8 

https://www.cnblogs.com/flyant/p/4443187.html

https://www.bygeek.cn/2018/05/24/understand-memory-mapped-file/

https://stackoverflow.com/questions/10806518/write-string-data-to-memorymappedfile

本文转载自博客园,原文链接:https://www.cnblogs.com/y-yp/p/12191258.html

全部评论: 0

    我有话说: