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

Batch set data from Dictionary into Redis

我正在使用 StackExchange Redis DB 使用 Batch 插入键值对字典,如下所示:

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 中的正确方法还是有更快的方法? 任何帮助表示赞赏。谢谢

"just" 是一个非常相对的术语,如果没有更多的上下文就没有真正意义,特别是:这些有效载荷有多大?

不过,澄清几点以帮助您调查:

  • 没有必要锁定 IDatabase 除非那纯粹是为了您自己的目的; SE.Redis 在内部处理线程安全,旨在供竞争线程使用
  • 目前,您的计时将包括所有序列化代码(JsonConvert.SerializeObject);这将加起来,尤其是 如果你的对象很大;为了获得合适的衡量标准,我强烈建议您分别对序列化和 Redis 时间进行计时
  • batch.Execute() 方法使用管道 API 并且不等待调用之间的响应,因此:您看到的时间 而不是 潜伏期的累积效应;只剩下本地 CPU(用于序列化)、网络带宽和服务器 CPU;客户端库工具不会影响任何这些东西
  • 有一个接受 KeyValuePair<RedisKey, RedisValue>[]StringSet 重载;你 可以 选择使用它而不是批处理,但这里唯一的区别是它是 varadic MSET 而不是 muliple SET;无论哪种方式,您都将在持续时间内阻止其他呼叫者的连接(因为批处理的目的是使命令连续)
  • 实际上 不需要在这里使用 CreateBatch尤其是 因为您正在锁定数据库(但是我仍然建议你不需要这样做); CreateBatch 的目的是使命令序列 顺序 ,但我看不出你在这里需要这个;您可以依次对每个命令使用 _database.StringSetAsync,这 的优点是您可以 运行 序列化 并行 之前发送的命令 - 它允许您重叠序列化(CPU 绑定)和 redis ops(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,最好是我可以重复使用的 - 避免 string 分配和 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));
    }
}

第二个答案有点离题,但根据讨论,主要成本似乎是序列化:

The object in this context is big with huge infos in string props and many nested classes.

你可以在这里做的一件事是不存储JSON。 JSON 相对较大,并且基于文本的序列化和反序列化处理成本相对较高。除非你使用 rejson,否则 redis 只是将你的数据视为一个不透明的 blob,所以它不关心实际值是什么。因此,您可以使用更高效的格式。

我有很大的偏见,但我们在我们的 redis 存储中使用了 protobuf-net。 protobuf-net 针对:

进行了优化
  • 小输出(没有冗余信息的密集二进制)
  • 快速二进制处理(通过上下文 IL emit 等进行了荒谬的优化)
  • 良好的跨平台支持(它实现了 Google 的 "protobuf" 有线格式,几乎在所有可用平台上都可用)
  • 旨在与现有的 C# 代码一起工作,而不仅仅是从 .proto 模式生成的全新类型

由于最后一点,我建议使用 protobuf-net 而不是 Google 自己的 C# protobuf 库,意思是:您可以将它与您已有的数据一起使用。

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

特别注意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序列化;如果你的数据以文本为主,它会很好地压缩。