使用Json.net将大型数据列表流式传输为JSON格式

8
使用MVC模型,我想编写一个JsonResult,将Json字符串流式传输到客户端,而不是一次性将所有数据转换为Json字符串再将其流式传输回客户端。
我有一些操作需要发送非常大的(超过300,000条记录)作为Json传输,我认为基本的JsonResult实现不可扩展。
我正在使用Json.net,我想知道是否有一种方法可以在转换时将Json字符串的块流式传输。
//Current implementation:
response.Write(Newtonsoft.Json.JsonConvert.SerializeObject(Data, formatting));
response.End();

//I know I can use the JsonSerializer instead
Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Serialize(textWriter, Data);

然而,我不确定如何将块写入textWriter并写入响应,并调用响应.Flush()直到所有300,000条记录都转换为Json。

这样做是否可能?

2个回答

19

假设您的最终输出是一个JSON数组,并且每个“块”在该数组中是一个项目,您可以尝试使用以下JsonStreamingResult类。它使用JsonTextWriter将JSON写入输出流,并使用JObject作为一种手段,以便在将其写入编写器之前单独序列化每个项目。您可以向JsonStreamingResult传递一个IEnumerable实现,该实现可以单独从数据源中读取项目,以便您不必一次性将它们全部存储在内存中。我没有进行过广泛的测试,但它应该能让您朝正确的方向前进。

public class JsonStreamingResult : ActionResult
{
    private IEnumerable itemsToSerialize;

    public JsonStreamingResult(IEnumerable itemsToSerialize)
    {
        this.itemsToSerialize = itemsToSerialize;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";
        response.ContentEncoding = Encoding.UTF8;

        JsonSerializer serializer = new JsonSerializer();

        using (StreamWriter sw = new StreamWriter(response.OutputStream))
        using (JsonTextWriter writer = new JsonTextWriter(sw))
        {
            writer.WriteStartArray();
            foreach (object item in itemsToSerialize)
            {
                JObject obj = JObject.FromObject(item, serializer);
                obj.WriteTo(writer);
                writer.Flush();
            }
            writer.WriteEndArray();
        }
    }
}

解决方案有效,避免了内存溢出异常,这很棒。但是,我认为如果批量记录一起刷新而不是一个接一个地刷新,它会更加优化。不确定最佳数量是多少! - sam360
是啊,我也在想这个问题。您可以在 JsonStreamingResult 中轻松添加一个计数器,使其等待从可枚举对象中读取一定数量的记录后再刷新。如果不同情况下该数字不同,您可以将其作为参数,以便为每个不同的用途进行调整。此外,在 IEnumerable 方面,您还可以实现一种查询数据源的分批机制,以提高效率。但是要进行大量的测量和测试,以确定哪种方式最好。 - Brian Rogers
另一个想法,虽然可能不太可能实现,是测量缓冲区大小并在每64KB或类似的大小时刷新。不确定我们是否可以检查JsonTextWriter中数据的大小。 - sam360
2
如果你想做类似的事情,可以尝试使用 BufferedStream 包装 OutputStream。然而,这个问答似乎表明,在.NET中,大多数流已经非常好地优化了缓冲区。如果是这种情况,也许最好根本不要调用 Flush,只需在内部缓冲区满时让流自行处理即可。不确定,你需要进行测试。 - Brian Rogers
1
一些基准测试表明,最有效的方法是使用serializer.Serialize(writer, data); 并一次性将所有数据传递给它,正如上面的评论所指出的那样,Stream本身处理缓冲区的工作非常出色,你的代码不需要进行一个巨大的循环 :) - sam360
@BrianRogers 你在实际应用中是如何使用的?能否在这里发布一些代码? - jkyadav

0

将其交给.NET并等待缓冲区填满的问题还有其他问题。

例如: 如果这样做,一些json内容可能会被截断,导致前端解析问题。

到目前为止,最好的方法是在每次迭代中刷新批处理,如果使用批处理,则在每个单独项目上刷新它,如果这是您的设计目的。

目前我使用SSE将数据推送到浏览器,并使用分隔符消息“on message end”来指示浏览器可以关闭连接,我知道SSE用于连续流,但我们也可以使用它来帮助分块和批处理响应。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接