c# - 检查状态生存时间是否未过期
问题描述
我有一个简单的 ASP.NET Core webapi 来管理一些设备。
public class Device : BaseModel
{
public string DeviceNumber { get; set; }
public int TypeId { get; set; }
public bool IsOnline { get; set; }
public int? StatusDuration { get; set; }
public bool IsDeleted { get; set; }
}
PostgreSQL 中的所有数据存储。
例如,当设备更改时IsOnline
,StatusDuration
设置为 30 分钟(不同设备可能不同)。
public async Task ChangeOnline(int id, bool isOnline, int statusDuration )
{
var device = await _dbContext.Devices.SingleOrDefaultAsync(x => x.Id == id);
if (device.IsOnline != isOnline)
{
device.IsOnline = isOnline;
device.StatusDuration = statusDuration;
device.UpdateDateTime = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
}
}
然后我需要执行一些后台作业来检查所有设备
(device.UpdateDateTime.AddMinutes(30) <= DateTime.UtcNow) && device.IsOnline
并设置IsOnline
为false
我试过的:
var dateTime = DateTime.UtcNow;
while(true)
{
var devices = await _dbContext.Devices.Where(x => x.IsOnline && x.UpdateDateTime.AddMinutes(x.StatusDuration) <= dateTime).ToListAsync;
foreach (var device in devices)
{
device.IsOnline = false;
device.UpdateDateTime = dateTime;
}
await _dbContext.SaveChangesAsync();
Thread.Sleep(5000);
}
但是这个每 5000 毫秒调用一次数据库并检查很多记录。有没有更好的方法可以在一段时间后禁用设备?也许我应该获取所有数据,然后在第二次迭代中只获取那些具有最近到期日期或类似日期的记录。我相信已经存在一些用于此类操作的算法。
解决方案
最有效的方法是实现如果不活动时间固定为 30 分钟,那么我要做的第一件事是更改您的查询,以便我们直接比较UpdateDateTime
而不是针对该字段执行函数:
var expiry = DateTime.UtcNow.AddMinutes(-30);
while(true)
{
var devices = await _dbContext.Devices.Where(x => x.IsOnline && x.UpdateDateTime < expiry).ToListAsync;
foreach (var device in devices)
{
device.IsOnline = false;
device.UpdateDateTime = dateTime;
}
await _dbContext.SaveChangesAsync();
Thread.Sleep(5000);
}
如果您必须使用,StatusDuration
那么您当前的查询应该足够高效,但是另一种选择是更改ChangeOnline
设置到期时间而不是持续时间的方法,现在我们正在将查询中最低效的方面转移到写入过程中,这将改善阅读,看到我们阅读的频率将远高于写作,这通常是最好的权衡。
public async Task ChangeOnline(int id, bool isOnline, int statusDuration )
{
var device = await _dbContext.Devices.SingleOrDefaultAsync(x => x.Id == id);
if (device.IsOnline != isOnline)
{
device.IsOnline = isOnline;
device.StatusDuration = statusDuration;
device.UpdateDateTime = DateTime.UtcNow;
device.ExpiryDateTime = DateTime.UtcNow.AddMinutes(statusDuration);
await _dbContext.SaveChangesAsync();
}
}
然后你只需要查找到期列,没有功能:
var now = DateTime.UtcNow;
while(true)
{
var devices = await _dbContext.Devices
.Where(x => x.IsOnline && x.ExpiryDateTime <= now)
.Take(batchSize)
.ToListAsync;
...
你还应该确保你的Devices
表上有一个索引IsOnline
和UpdateDateTime
(或者如果你这样做的话,ExpiryDateTime)
CREATE INDEX IX_DeviceExpiryCheck
ON Devices (IsOnline, UpdateDateTime);
这是对数据库中索引的非常有效的查询,因此我不提倡带回更多数据并将其缓存在本地,在许多用例中这并不是一个真正的选择,因为其他进程可能会调用您ChangeOnline
或以其他方式修改IsOnline
等等UpdateDateTime
无论如何,您的缓存结果集都需要定期刷新。
如果有大量设备(超过 100 个)可能会过期,那么更新语句可能会占用更多资源,并且如果一次进行太多更改,可能会中断您的管道。如果这成为问题,您可以批量查找,以下是 IMO 对您的问题最有效的解决方案:
var now = DateTime.UtcNow;
int recordCount = 0;
int batchSize = 50;
while(true)
{
do
{
var devices = await _dbContext.Devices
.Where(x => x.IsOnline && x.ExpiryDateTime <= now)
.Take(batchSize)
.ToListAsync;
foreach (var device in devices)
{
device.IsOnline = false;
device.UpdateDateTime = dateTime;
}
await _dbContext.SaveChangesAsync();
Thread.Sleep(1000); // reduce the DTUs on the db by spreading the updates out a bit
} while(recordCount == batchSize && recordCount > 0); // just in case you set batch size to zero ;)
Thread.Sleep(5000);
}
最后要考虑的是将签出频率更改为 1 或 5 分钟,而不是 5 秒。
这是一个可能对性能产生巨大影响的业务决策,您真的需要在 5 秒内知道设备是否在过去 X 分钟内没有活动吗?如果它在 30 分钟内没有激活,那么 00:30:00 和 00:30:05 或 00:31:00 之间有什么区别?
我是管理大量 IoT 设备的团队的一员,我们的模式与您的类似,用于监控我们所说的心跳。最后我们从数据库中完全去掉了 IsOnline 的概念,只使用了类似的字段
UpdateDateTime
,以减少对数据库的写入,IsOnline
因此将概念字段移动到业务逻辑层作为UpdateDateTime
. 这样的更改可能对您当前的业务逻辑和查询很重要,但它是另一个需要考虑的选项。
推荐阅读
- jquery - jQuery编辑内容
- ios - 如何使用函数调用正确更新 CGContext?
- c# - OnPaint 事件很慢并抛出“阴影”
- spring - Spring websocket/broker 故障转移
- javascript - 如何将自定义 js 文件包含/导入到 vue 项目中?
- javascript - 需要帮助从 JavaScript 中的字符串中提取数字
- python - 试图让 PyCharm 帮助我检查数组的维度
- c# - 如何使用 Listbox C# 将两列放在同一行
- javascript - 在 windows 命令行/ package.json 中设置 env
- scala - 如何根据原始数据帧中的总行数将数据帧拆分为两个数据帧