ASP.NET 核心应用程序中每个用户一个长 运行 进程
One long running process per user in an ASP.NET Core Application
我有一个异步函数,可以执行一些长时间的 运行 和 CPU 密集型任务。因为它是异步的,所以它不会阻止 UI.
在正常情况下,我对它的运行方式没有任何问题。该过程在单击按钮时触发,但它可以在 API 调用时触发。
我发现如果我快速连续多次点击按钮,整个网站就会开始响应slowly.This这是一种滥用类型,但它会对性能产生不利影响。
我想在内存中实现一个用户调用函数的列表,这样每个用户都可以拥有一个长 运行 函数 运行 的实例,但它不会接受来自该用户的任何进一步请求。
谁知道实现这个的一些聪明的方法或者以前做过类似的事情。我正在考虑类似线程安全集合的东西,你可以向其中添加任务,但是如果你尝试为某个用户/claimsprincipal 添加一个已经 运行 的任务,那么它将无法添加它。
您的解决方案存在一些问题:
- 如果任务是 CPU 密集的,它仍然可以支撑 UI,即使它是
async
。一旦所有 CPU 都忙,您将看到性能影响。
- 长 运行 任务在网络服务器上不是一个好主意。 Web 服务器旨在处理大量短请求。长请求没有得到有效处理,它们也可能在完成之前被回收。
- 您的实施使拒绝服务攻击变得容易。您基本上已经通过多次单击按钮创建了一个。
您应该将这些类型的任务卸载到更适合处理此类工作的应用程序服务器上。这使您的网络服务器可以自由地做它最擅长的事情:处理网络请求。
使用最适合您需求的 Web 和应用程序服务器之间的通信形式:WCF、REST API 或 MSMQ 或 RabbitMQ 等消息队列框架。
如果不深入探讨这样做是否适合您的特定业务情况和架构,我认为您的问题基本上可以归结为:如何确保用户只能调用特定流程一次一个。
您可以使用线程安全集合为每个用户保存一个标志,指示他们当前是否 运行 这样的进程。在您的异步代码中使用 try{}finally{} 块来管理该值。在 try 块中将其设置为 true,并在 finally 中将其设置为 false。剩下的 运行 长的工作是在你将它设置为 true 之后,在你执行 finally 之前。概述如下:
try{
//if thread safe value for this user is already true then exit.
//set thread safe collection value for this user to true
//Do work
}finally{
//set thread safe collection value for this user to false;
}
如果是我,我会在集合中放入一个对象,该对象有一个值,即布尔值。这样,当我检查它的值或设置它的值时,我可以使用 lock
关键字锁定该用户的对象,这样我就可以确保在我检查它和当我设置它时。
希望对您有所帮助。
最后我在 DI 上注册了一个 Service 作为 Singleton。
public class SingleInstanceRegisterService: IDisposable
{
private ConcurrentDictionary<SingleInstance,SingleInstance> dict = new ConcurrentDictionary<SingleInstance, SingleInstance>();
AutoResetEvent arv;
Timer timer;
public SingleInstanceRegisterService()
{
arv = new AutoResetEvent(false);
timer = new Timer(this.Cleanup, arv, 0, 60000);
}
public bool TryAdd(SingleInstance i)
{
return dict.TryAdd(i, i);
}
public bool ContainsKey(SingleInstance i)
{
return dict.ContainsKey(i);
}
public bool TryRemove(SingleInstance i)
{
SingleInstance x;
return dict.TryRemove(i, out x);
}
public void Cleanup(Object stateInfo)
{
AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
if (dict != null)
{
foreach(SingleInstance i in dict.Keys)
{
if (i.Expiry < DateTimeOffset.Now)
{
TryRemove(i);
}
}
}
autoEvent.Set();
}
public void Dispose()
{
timer?.Dispose();
timer = null;
}
}
SingleInstance 定义为:
public class SingleInstance: IEquatable<SingleInstance>
{
public string UserSubject;
public string FullMethodName;
public DateTimeOffset Expiry;
public SingleInstance(User u, MethodInfo m, DateTimeOffset exp)
{
UserSubject = u.UserSubject.ToString();
FullMethodName = m.DeclaringType.FullName + "." + m.Name;
Expiry = exp;
}
public bool Equals(SingleInstance other)
{
return (other != null &&
other.FullMethodName == this.FullMethodName &&
other.UserSubject == this.UserSubject);
}
public override bool Equals(object other)
{
return this.Equals(other as SingleInstance);
}
public override int GetHashCode()
{
return (UserSubject.GetHashCode() + FullMethodName.GetHashCode());
}
}
要使用它,我们执行以下操作。
_single 是我注入的依赖 SingleInstanceRegisterService。
var thisInstance = new SingleInstance(user, method, DateTimeOffset.UtcNow.AddMinutes(1));
if (_single.TryAdd(thisInstance))
{
// tell the database to kick off some long running process
DoLongRunningDatabaseProcess();
// remove lock on running process
_single.TryRemove(thisInstance);
}
如果 TryAdd 无法添加它,那么它必须已经是 运行。请注意,有一个计时器会自动清除已过期的进程锁。
我有一个异步函数,可以执行一些长时间的 运行 和 CPU 密集型任务。因为它是异步的,所以它不会阻止 UI.
在正常情况下,我对它的运行方式没有任何问题。该过程在单击按钮时触发,但它可以在 API 调用时触发。
我发现如果我快速连续多次点击按钮,整个网站就会开始响应slowly.This这是一种滥用类型,但它会对性能产生不利影响。
我想在内存中实现一个用户调用函数的列表,这样每个用户都可以拥有一个长 运行 函数 运行 的实例,但它不会接受来自该用户的任何进一步请求。
谁知道实现这个的一些聪明的方法或者以前做过类似的事情。我正在考虑类似线程安全集合的东西,你可以向其中添加任务,但是如果你尝试为某个用户/claimsprincipal 添加一个已经 运行 的任务,那么它将无法添加它。
您的解决方案存在一些问题:
- 如果任务是 CPU 密集的,它仍然可以支撑 UI,即使它是
async
。一旦所有 CPU 都忙,您将看到性能影响。 - 长 运行 任务在网络服务器上不是一个好主意。 Web 服务器旨在处理大量短请求。长请求没有得到有效处理,它们也可能在完成之前被回收。
- 您的实施使拒绝服务攻击变得容易。您基本上已经通过多次单击按钮创建了一个。
您应该将这些类型的任务卸载到更适合处理此类工作的应用程序服务器上。这使您的网络服务器可以自由地做它最擅长的事情:处理网络请求。
使用最适合您需求的 Web 和应用程序服务器之间的通信形式:WCF、REST API 或 MSMQ 或 RabbitMQ 等消息队列框架。
如果不深入探讨这样做是否适合您的特定业务情况和架构,我认为您的问题基本上可以归结为:如何确保用户只能调用特定流程一次一个。
您可以使用线程安全集合为每个用户保存一个标志,指示他们当前是否 运行 这样的进程。在您的异步代码中使用 try{}finally{} 块来管理该值。在 try 块中将其设置为 true,并在 finally 中将其设置为 false。剩下的 运行 长的工作是在你将它设置为 true 之后,在你执行 finally 之前。概述如下:
try{
//if thread safe value for this user is already true then exit.
//set thread safe collection value for this user to true
//Do work
}finally{
//set thread safe collection value for this user to false;
}
如果是我,我会在集合中放入一个对象,该对象有一个值,即布尔值。这样,当我检查它的值或设置它的值时,我可以使用 lock
关键字锁定该用户的对象,这样我就可以确保在我检查它和当我设置它时。
希望对您有所帮助。
最后我在 DI 上注册了一个 Service 作为 Singleton。
public class SingleInstanceRegisterService: IDisposable
{
private ConcurrentDictionary<SingleInstance,SingleInstance> dict = new ConcurrentDictionary<SingleInstance, SingleInstance>();
AutoResetEvent arv;
Timer timer;
public SingleInstanceRegisterService()
{
arv = new AutoResetEvent(false);
timer = new Timer(this.Cleanup, arv, 0, 60000);
}
public bool TryAdd(SingleInstance i)
{
return dict.TryAdd(i, i);
}
public bool ContainsKey(SingleInstance i)
{
return dict.ContainsKey(i);
}
public bool TryRemove(SingleInstance i)
{
SingleInstance x;
return dict.TryRemove(i, out x);
}
public void Cleanup(Object stateInfo)
{
AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
if (dict != null)
{
foreach(SingleInstance i in dict.Keys)
{
if (i.Expiry < DateTimeOffset.Now)
{
TryRemove(i);
}
}
}
autoEvent.Set();
}
public void Dispose()
{
timer?.Dispose();
timer = null;
}
}
SingleInstance 定义为:
public class SingleInstance: IEquatable<SingleInstance>
{
public string UserSubject;
public string FullMethodName;
public DateTimeOffset Expiry;
public SingleInstance(User u, MethodInfo m, DateTimeOffset exp)
{
UserSubject = u.UserSubject.ToString();
FullMethodName = m.DeclaringType.FullName + "." + m.Name;
Expiry = exp;
}
public bool Equals(SingleInstance other)
{
return (other != null &&
other.FullMethodName == this.FullMethodName &&
other.UserSubject == this.UserSubject);
}
public override bool Equals(object other)
{
return this.Equals(other as SingleInstance);
}
public override int GetHashCode()
{
return (UserSubject.GetHashCode() + FullMethodName.GetHashCode());
}
}
要使用它,我们执行以下操作。 _single 是我注入的依赖 SingleInstanceRegisterService。
var thisInstance = new SingleInstance(user, method, DateTimeOffset.UtcNow.AddMinutes(1));
if (_single.TryAdd(thisInstance))
{
// tell the database to kick off some long running process
DoLongRunningDatabaseProcess();
// remove lock on running process
_single.TryRemove(thisInstance);
}
如果 TryAdd 无法添加它,那么它必须已经是 运行。请注意,有一个计时器会自动清除已过期的进程锁。