C# Redis使用Hashes=C ASP.NET Core 3.0和Docker时速度非常慢
我目前正在开发一个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个 这是我的部分积垢类: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的
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"),
});