C# Redis使用Hashes=C ASP.NET Core 3.0和Docker时速度非常慢

C# Redis使用Hashes=C ASP.NET Core 3.0和Docker时速度非常慢,c#,docker,redis,C#,Docker,Redis,我目前正在开发一个web api,它在存储和发送数据时必须快速高效。首先,我尝试了实体框架核心,然后是ADO.NET,这大大提高了速度 为了提高速度,我尝试设置Redis。然而,与ADO.NET相比,它的速度有多慢,这让我感到震惊。具体来说,ADO.NET 541ms和Redis高达2.5s,可通过postman检索具有5个属性的32个对象 以下是我为优化Redis所做的一些工作: 使我所有的HashGet异步 关闭了持久性存储,尽管它已关闭 将“保存在配置中”设置为 创建了IDatabase的

我目前正在开发一个web api,它在存储和发送数据时必须快速高效。首先,我尝试了实体框架核心,然后是ADO.NET,这大大提高了速度

为了提高速度,我尝试设置Redis。然而,与ADO.NET相比,它的速度有多慢,这让我感到震惊。具体来说,ADO.NET 541ms和Redis高达2.5s,可通过postman检索具有5个属性的32个对象

以下是我为优化Redis所做的一些工作:

使我所有的HashGet异步 关闭了持久性存储,尽管它已关闭 将“保存在配置中”设置为 创建了IDatabase的一个全局实例,因此只使用了一个连接 在启动时创建了所有哈希。请参阅RedisConnectionHelper.Instance.Hash 我的实际哈希RedisConnectionHelper.Instance.hash表示每行数据,从device1:input1到device32:input12,总共384个

这是我的部分积垢类:

namespace StateAPI.RedisContext
{
    public class RedisCRUD : IRedisCRUD
    {
        // ^^^ store and re-use this!!!
        IDatabase db = RedisConnectionHelper.Instance.ConnectionMultiplexer.GetDatabase();

        public async Task<IEnumerable<StateRedis>> AllStatesAsync()
        {
            List<StateRedis> returnedDevices = new List<StateRedis>();

            foreach (string h in RedisConnectionHelper.Instance.Hashes)
            {
                StateRedis state = new StateRedis
                {
                    Id = await db.HashGetAsync(h, "id"),
                    InputState = await db.HashGetAsync(h, "state"),
                    OnPhrase = await db.HashGetAsync(h, "onphrase"),
                    OffPhrase = await db.HashGetAsync(h,"offphrase"),
                    When = await db.HashGetAsync(h, "when")
                };

                returnedDevices.Add(state);
            }

            return returnedDevices;
        }
    }
}
请注意,这是Docker的Redis容器,在运行Docker的linux服务器上内部运行。SQL Server也在同一个Docker服务器上运行,通常,该服务器可以很好地运行我们所有的容器。请注意,我在这里读到,当某人的web项目与Redis位于同一位置时,他们的问题就消失了

计时时间似乎在1秒到2.5秒之间波动,因此明显存在一个重大瓶颈

更新

我的Redis连接助手:

namespace StateAPI.Helpers
{
    public class RedisConnectionHelper
    {
        private static RedisConnectionHelper _instance = null;

        public ConnectionMultiplexer ConnectionMultiplexer { get; set; }

        public List<string> Hashes { get; set;}

        public static RedisConnectionHelper Instance
        {
            get
            {
                if (_instance == null) _instance = new RedisConnectionHelper();
                return _instance;
            }
            protected set
            {
                _instance = value;
            }
        }

        public RedisConnectionHelper()
        {
            // ^^^ store and re-use this!!!
            ConnectionMultiplexer = ConnectionMultiplexer.Connect("dell-docker"); // Note that ConnectionMultiplexer implements IDisposable and can be disposed when no longer required

            // create hashes for each device and input e.g. device1:input12
            Hashes = new List<string>();

            int deviceCount = 1;
            int inputCount = 1;

            int numOfInputs = 12;
            int numOfDevices = 32;

            for (int i = 0; i < (numOfDevices * numOfInputs); i++)
            {
                if (i % numOfInputs == 0 && i != 0)
                {
                    inputCount = 1;
                    deviceCount = deviceCount + 1;
                }

                string deviceHash = $"device{deviceCount}:input{inputCount}";

                inputCount = inputCount + 1;

                Hashes.Add(deviceHash);
            }
        }
    }
}
问题 您发布的代码对哈希集合中的Redis 384项进行1920次往返,每次请求5个,并按顺序进行:

foreach (string h in RedisConnectionHelper.Instance.Hashes)
{
    StateRedis state = new StateRedis
    {
        Id = await db.HashGetAsync(h, "id"),              // Execution stops until Redis returns
        InputState = await db.HashGetAsync(h, "state"),   // Execution stops until Redis returns
        OnPhrase = await db.HashGetAsync(h, "onphrase"),  // Execution stops until Redis returns
        OffPhrase = await db.HashGetAsync(h,"offphrase"), // Execution stops until Redis returns
        When = await db.HashGetAsync(h, "when")           // Execution stops until Redis returns
    };

    returnedDevices.Add(state);
}
对于每个散列,这段代码发出5个请求,并等待每个请求返回,然后再发出下一个请求。即使到Redis的延迟只有1ms,也就是说,等待从每个响应接收第一个字节就需要1920ms

在这里使用async/await无助于解决此问题。尽管线程被解除阻塞以服务其他传入请求,但等待意味着每次对db.HashGetAsync的调用直到前一次调用完成后才开始

这类似于生成1920个SQL查询,如下所示:

SELECT id        FROM data WHERE deviceID = 1 AND inputID = 1
SELECT state     FROM data WHERE deviceID = 1 AND inputID = 1
SELECT onphrase  FROM data WHERE deviceID = 1 AND inputID = 1
SELECT offphrase FROM data WHERE deviceID = 1 AND inputID = 1
SELECT when      FROM data WHERE deviceID = 1 AND inputID = 1

SELECT id        FROM data WHERE deviceID = 1 AND inputID = 2
SELECT state     FROM data WHERE deviceID = 1 AND inputID = 2
SELECT onphrase  FROM data WHERE deviceID = 1 AND inputID = 2
SELECT offphrase FROM data WHERE deviceID = 1 AND inputID = 2
SELECT when      FROM data WHERE deviceID = 1 AND inputID = 2
...

SELECT id        FROM data WHERE deviceID = 32 AND inputID = 12
SELECT state     FROM data WHERE deviceID = 32 AND inputID = 12
SELECT onphrase  FROM data WHERE deviceID = 32 AND inputID = 12
SELECT offphrase FROM data WHERE deviceID = 32 AND inputID = 12
SELECT when      FROM data WHERE deviceID = 32 AND inputID = 12
快速改进 将请求数减少5倍的快速方法是检索一个请求中所需的所有哈希值:

foreach (string h in RedisConnectionHelper.Instance.Hashes)
{
    var keys = await db.HashGetAsync(h, new RedisValue[]{ "id", "state", "onphrase", "offphrase", "when" })
    StateRedis state = new StateRedis
    {
        Id = keys[0],
        InputState = keys[1],
        OnPhrase = keys[2],
        OffPhrase = keys[3],
        When = keys[4],
    };

    returnedDevices.Add(state);
}
此代码将对每个哈希进行1个请求,或384个请求。这可能仍然比一个操作需要更多的请求,但可能使Redis实现与SQL实现相当

可能更好的解决方案 流水线请求 Redis可以非常快速地响应请求,但现在代码正在等待前一个响应,然后再发送下一个请求。相反,您可以:提前发送所有邮件,然后接收回复

public async Task<IEnumerable<StateRedis>> AllStatesAsync()
{
    List<StateRedis> returnedDevices = new List<StateRedis>();

    // Start every request without awaiting the responses
    List<Task<StateRedis>> stateTasks =
        RedisConnectionHelper.Instance.Hashes
        .Select(hashKey => GetStateRedisAsync(hashKey))
        .ToList();

    // Wait for the responses
    StateRedis[] states = await Task.WhenAll(stateTasks);
    return states;

    async Task<StateRedis> GetStateRedisAsync(RedisKey key)
    {
        var keys = await db.HashGetAsync(h, new RedisValue[]{ "id", "state", "onphrase", "offphrase", "when" })
        return new StateRedis
        {
            Id = keys[0],
            InputState = keys[1],
            OnPhrase = keys[2],
            OffPhrase = keys[3],
            When = keys[4],
        };
    }
}
使用此格式,您可以一次查询所有字符串:

var values = db.StringGet(new RedisKey[] { "device1:input1", "device1:input2", ... });
foreach (RedisValue value in values)
{
    var stateRedis = JsonConvert.DeserializeObject<StateRedis>(value);

    // stateRedis == new StateRedis { id = 1, state = "foobar", ... }
}

使用这种结构,您仍然需要发出32个请求来获取所有设备的数据,但是如果散列中的一个项是一个大的二进制blob,您可以避免不必要地从Redis进行传输。

创建了一个IDatabase全局实例,因此只使用了一个连接。文档中说,该连接用于存储和重用ConnectionMultiplexer,而不是它返回的IDatabase。散列用于加速查找,而不是迭代。迭代意味着更慢,并且您的代码对每个项目发出一个请求,即384次往返。您的ORM/ADO.NET代码是什么样子的?如果它选择了所有内容,Redis中的等效结构将是一个包含所有384项的列表。另一方面,384项根本不是数据,因此索引数据库表可以很容易地缓存在服务器的内存中,可能在单个核心的CPU缓存中缓存很长一段时间。您可以使用脚本一次性获取所需的所有密钥。将限制连接并且速度会更快。@Jameshorpe我已经更新了我的帖子,你能确认我正确地存储和重用了ConnectionMultiplexer吗@Panagiotis,是的ADO.NET它将使用SqlDataReader和存储过程一次选择所有内容@LeBigCat将读取链接。尽管如此,我还是认为即使没有最有效的代码,Redis也会闪电式地运行SQL。更新:我决定在本地运行Redis容器,而不是与Linux服务器通信,并且在没有任何代码调整的情况下,我已经将时间缩短到大约200毫秒。我想知道Redis是否需要在同一台服务器上运行;然而,从Linux服务器对相同数据的API调用只需要35毫秒,所以我想我还没有走出困境。这是一个多么好的答案,我没有想到!好吧,我刚刚很快尝试了你的第一个建议,我已经把它从大约200毫秒降到了大约50毫秒。我仍然很困惑,为什么通过我们的Linux服务器与Redis通信时速度非常慢,大约2500秒。不管怎样,我假设当所讨论的API与localhost在同一台服务器上运行时不会出现问题
Redis现在似乎快多了。是的,散列是这里的初始建议,所以它们不必是。下一步我将尝试管道,然后是字符串,看看我如何进行。我猜这是延迟。有一个db.Ping方法可以告诉您从Redis接收单个响应需要多长时间,这应该是每个Redis请求的开销。这可能有助于您排除故障:如果平均延迟接近1ms,那么问题可能是请求的数量太多。您可以将Redis的延迟与针对SQL执行SELECT 1的延迟进行比较,以查看Redis的开销是否具有可比性。谢谢,我将db.Ping放在从列表中获取哈希并存储该哈希的方法的开头,我得到的最多是00:00:00.0029417,然后它减少了我在Postman中执行的更多PUT请求。我注意到的一件事是,当我启动应用程序时,第一次获取、放置等的速度似乎是最慢的,然后我发出的请求越多,时间越短。请注意,我刚刚ping了我的本地Redis容器,也许我应该将它指向服务器上的容器以查看结果。
public async Task<IEnumerable<StateRedis>> AllStatesAsync()
{
    List<StateRedis> returnedDevices = new List<StateRedis>();

    // Start every request without awaiting the responses
    List<Task<StateRedis>> stateTasks =
        RedisConnectionHelper.Instance.Hashes
        .Select(hashKey => GetStateRedisAsync(hashKey))
        .ToList();

    // Wait for the responses
    StateRedis[] states = await Task.WhenAll(stateTasks);
    return states;

    async Task<StateRedis> GetStateRedisAsync(RedisKey key)
    {
        var keys = await db.HashGetAsync(h, new RedisValue[]{ "id", "state", "onphrase", "offphrase", "when" })
        return new StateRedis
        {
            Id = keys[0],
            InputState = keys[1],
            OnPhrase = keys[2],
            OffPhrase = keys[3],
            When = keys[4],
        };
    }
}
db.StringSet("device1:input1",   JsonConvert.SerializeObject(new StateRedis { id = 1, state = "foobar", ... }))
db.StringSet("device1:input2",   JsonConvert.SerializeObject(new StateRedis { id = 2, state = "foobar", ... }))
...
db.StringSet("device32:input12", JsonConvert.SerializeObject(new StateRedis { id = 384, state = "foobar", ... }))
var values = db.StringGet(new RedisKey[] { "device1:input1", "device1:input2", ... });
foreach (RedisValue value in values)
{
    var stateRedis = JsonConvert.DeserializeObject<StateRedis>(value);

    // stateRedis == new StateRedis { id = 1, state = "foobar", ... }
}
db.HashSet("device1", new [] {
    new HashEntry("input1:id", 1),
    new HashEntry("input1:state", "foobar"),
    ...
    new HashEntry("input12:id", 12),
    new HashEntry("input12:state", "foobar"),
});