批量将字典数据设置到Redis中

3

我正在使用StackExchange Redis DB,通过以下方式使用Batch插入Key-Value字典:

private static StackExchange.Redis.IDatabase _database;
public void SetAll<T>(Dictionary<string, T> data, int cacheTime)
{
    lock (_database)
    {
        TimeSpan expiration = new TimeSpan(0, cacheTime, 0);
        var list = new List<Task<bool>>();
        var batch = _database.CreateBatch();               
        foreach (var item in data)
        {
            string serializedObject = JsonConvert.SerializeObject(item.Value, Formatting.Indented,
        new JsonSerializerSettings { ContractResolver = new SerializeAllContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore });

            var task = batch.StringSetAsync(item.Key, serializedObject, expiration);
            list.Add(task);
            serializedObject = null;
        }
        batch.Execute();

        Task.WhenAll(list.ToArray());
    }
}

我的问题:设置350个词典项需要耗费7秒

我的疑问:这是将批量条目设置到Redis的正确方法吗?还是有更快的方法可以实现?任何帮助将不胜感激。谢谢。


这个链接有帮助吗:在Redis中批量创建键 - ServiceStack C# - Keyur PATEL
2
@KeyurPATEL,我“有根据的猜测”是关键问题在于序列化成本和带宽成本-这意味着实际上使用哪种工具并不重要(我说“有根据的猜测”,因为如果没有实际的复制,我不能确定,但是:我在序列化和Redis方面非常有经验,所以我敢打赌我的直觉是正确的) - Marc Gravell
1
我告诉你我想知道的是:总有效载荷大小,也就是说:如果你执行 long totalChars = 0; 然后在循环中 totalChars += item.Key.Length + serializedObject.Length + 25;,那么最终的 totalChars 是多少?显然这不完全等同于字节(UTF-8 是可变长度的),但这将是一种快速简便的了解你正在传输多少数据的方法;+25 是每个命令的传输开销,假设 *3\r\n$3\r\nSET\r\n$X\r\n...\r\n$Y\r\n...\r\n - Marc Gravell
@MarcGravell 让我来查一下。 - User3250
@MarcGravell 刚刚检查了 totalChars 大约是 3639111 - User3250
显示剩余3条评论
2个回答

8
“just”这个词是一个相对的概念,没有更多的上下文很难理解,特别是:这些负载有多大?
然而,为了帮助您进行调查,以下是一些要点:
- 如果不是为了自己的目的,就没有必要锁定IDatabase;SE.Redis在内部处理线程安全,并且旨在由竞争线程使用。 - 目前,您的时间包括所有序列化代码(JsonConvert.SerializeObject);如果您的对象很大,这将会增加,特别是。为了得到一个合理的测量结果,我强烈建议您分别计时序列化和redis时间。 - batch.Execute()方法使用管道API,并且在调用之间不等待响应,因此:您看到的时间不是延迟的累积效果;这只留下了本地CPU(用于序列化),网络带宽和服务器CPU;客户端库工具无法影响其中的任何内容。 - 有一个StringSet重载版本,接受一个KeyValuePair[];你可以选择使用它来代替batch,但唯一的区别是它是可变参数MSET而不是多个SET;无论哪种方式,你都会阻塞连接,使其他调用者在此期间无法访问(因为batch的目的是使命令连续)。 - 在这里,你实际上不需要使用CreateBatch,特别是因为你锁定了数据库(但我仍然建议你不需要这样做);CreateBatch的目的是使一系列命令连续,但我认为你在这里不需要这样做;你可以依次为每个命令使用_database.StringSetAsync,这也有一个优点,即你将在发送前一个命令时并行运行序列化(CPU绑定)和redis操作(IO绑定)——它将允许你重叠序列化而不需要任何工作(除了删除CreateBatch调用);这也意味着您不会垄断其他调用者的连接。
所以,我要做的第一件事就是删除一些代码。
private static StackExchange.Redis.IDatabase _database;
static JsonSerializerSettings _redisJsonSettings = new JsonSerializerSettings {
    ContractResolver = new SerializeAllContractResolver(),
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore };

public void SetAll<T>(Dictionary<string, T> data, int cacheTime)
{
    TimeSpan expiration = new TimeSpan(0, cacheTime, 0);
    var list = new List<Task<bool>>();
    foreach (var item in data)
    {
        string serializedObject = JsonConvert.SerializeObject(
            item.Value, Formatting.Indented, _redisJsonSettings);

        list.Add(_database.StringSetAsync(item.Key, serializedObject, expiration));
    }
    Task.WhenAll(list.ToArray());
}

第二件事就是将序列化的时间单独计时,与redis工作分开。
第三件事就是看看能否序列化到一个MemoryStream中,最好是可以重复使用的MemoryStream,以避免字符串分配和UTF-8编码。
using(var ms = new MemoryStream())
{
    foreach (var item in data)
    {
        ms.Position = 0;
        ms.SetLength(0); // erase existing data
        JsonConvert.SerializeObject(ms,
            item.Value, Formatting.Indented, _redisJsonSettings);

        list.Add(_database.StringSetAsync(item.Key, ms.ToArray(), expiration));
    }
}

在这个上下文中,对象很大,具有字符串属性中的大量信息和许多嵌套类。让我尝试你的建议。很快就会回来。谢谢。 - User3250
@User3250,太好了;在这种情况下,我认为将序列化成本与网络成本分开是更加重要的,这样你就知道你正在测量哪一个;一种非常便宜的方法是只需注释掉redis代码,这样你只需要将其序列化为字符串或流(但不做任何其他操作,除非可能调用GC.KeepAlive以防止JIT做任何聪明的事情)- 看看需要多长时间。那个时间就是序列化成本。 - Marc Gravell
JsonConvert.SerializeObject(ms,item.Value, Formatting.Indented, _redisJsonSettings);无法工作。找不到重载。我尝试使用以前的代码list.Add(_database.StringSetAsync(item.Key, serializedObject, expiration));效果非常好!时间缩短到了约100毫秒。 - User3250
@User3250 稍等,我会找到正确的Json.NET代码与流一起使用 - 每一点帮助都有用 :) 更新:糟糕!它只接受TextWriterJsonWriter... 唉,不值得深究了。 - Marc Gravell
是的,看起来 _database.StringSetAsync 起到了作用。真希望我能给你的回答点赞+25个 :) - User3250

5

这个回答有点离题,但根据讨论听起来主要成本是序列化:

在这种情况下,对象很大,具有巨大的字符串属性和许多嵌套类。

你可以做的一件事是不要存储JSON。JSON相对较大,并且作为文本进行序列化和反序列化的成本相对较高。除非你使用rejson,否则Redis只将数据视为不透明的二进制块,因此它不关心实际值是什么。因此,你可以使用更有效的格式。

我非常偏向于使用protobuf-net在我们的Redis存储中。protobuf-net针对以下方面进行了优化:

  • 小输出(紧凑的二进制格式,没有冗余信息)
  • 快速二进制处理(通过上下文IL发射等方式进行极致优化)
  • 良好的跨平台支持(它实现了Google的“protobuf”线路格式,在几乎所有可用平台上都可用)
  • 设计与现有C#代码很好地配合使用,而不仅仅是从.proto模式生成的全新类型

我建议使用protobuf-net而不是Google自己的C# protobuf库,因为最后一个弹道点意味着:你可以将其与现有数据一起使用。

为了说明原因,我将使用https://aloiskraus.wordpress.com/2017/04/23/the-definitive-serialization-performance-guide/中的这张图片:

serializer performance

特别注意protobuf-net的输出大小只有Json.NET的一半(减少带宽成本),序列化时间不到五分之一(降低本地CPU成本)。

你需要向你的模型添加一些属性以帮助protobuf-net(根据How to convert existing POCO classes in C# to google Protobuf standard POCO),但然后这将仅仅是:

using(var ms = new MemoryStream())
{
    foreach (var item in data)
    {
        ms.Position = 0;
        ms.SetLength(0); // erase existing data
        ProtoBuf.Serializer.Serialize(ms, item.Value);

        list.Add(_database.StringSetAsync(item.Key, ms.ToArray(), expiration));
    }
}

如您所见,对于您的Redis代码,所需的代码更改是最小的。显然,在读取数据时需要使用Deserialize<T>


如果您的数据是基于文本的,您也可以考虑通过GZipStreamDeflateStream运行序列化; 如果您的数据被文本所占据,它将非常压缩。


我一定会检查这个。谢谢。 - User3250
嗨,@Marc,我有一个旁问,你使用gzip吗?如果是这样,与“正常”序列化相比,你遇到了多大的性能损失?我发现在使用+json时会出现巨大的性能损失,其中序列化需要花费约3倍的时间。第二个问题,微软的Bond(二进制)序列化程序实际上在某些情况下甚至比protobuf更快,你试过吗?谢谢,m - MichaC
@michaC 我没有关于开销的确切数字;我们很乐意支付它来最小化IO(网络,每个键每次读取)和RAM(服务器,每个键);关于Bond - 我看过但没有使用过。有关它真正擅长的场景的任何信息吗?我正在积极地开发protobuf-net - 也许我可以在这些场景中“提高我的水平” :) - Marc Gravell
@MarcGravell 我创建了一个小的 Github 仓库 https://github.com/MichaCo/SerializationBenchmarks 如果你想看一下 / 继续交流 ;) 目前,只有一个非常简单的字符串列表基准测试,我可能会添加更多。 - MichaC
@MichaC 太棒了 - 我一定会看的。不过我会先专注于发布2.3.0版本。我会添加一个问题来提醒自己:完成 https://github.com/mgravell/protobuf-net/issues/266 - Marc Gravell

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