Orleans 提供了 Timers 和 Reminders两种机制。
Timers
计时器与 .NET 里提供的 System.Threading.Timer 类基本相同。此外,计时器在单独的线程上执行。如果有多个与之关联的计时器,在运行时会进行上下文切换去执行每个计时器。
用法:
要启动计时器,直接使用 RegisterTimer 方法就可以,该方法在 Grain 类中实现,方法参数说明:
Func<object, Task> asyncCallback // 计时器开始计时调用的函数
object state // asyncCallback 函数所需参数
TimeSpan dueTime // 启动到第一次执行间隔的时间量
TimeSpan period // 等行为下一次执行间隔的时间量
方法返回一个 IDisposable 句柄,通过 Dispose 方法来停止计时器。
另外,一旦 Grain 被停用或者 Silo 崩坏,计时器将停止触发。
接下来我们看下使用的代码:
public interface ITimerGrain : IGrainWithIntegerKey
{
Task Start();
Task Finish();
}
public class TimerGrain : Grain, ITimerGrain
{
private readonly ILogger logger;
private IDisposable? disposable;
public TimerGrain(ILogger<HelloGrain> logger)
{
this.logger = logger;
}
public Task Start()
{
disposable = RegisterTimer(o =>
{
logger.LogInformation($"time tick = {DateTime.Now}\n ");
return Task.CompletedTask;
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
public Task Finish()
{
if (disposable != null)
{
logger.LogInformation($"计时器停止--{DateTime.Now}\n");
disposable.Dispose();
disposable = null;
}
return Task.CompletedTask;
}
}
client 调用
var timerGrain = client.GetGrain<ITimerGrain>(1);
await timerGrain.Start();
//休息个10秒钟再来取消计时器
Thread.Sleep(10000);
await timerGrain.Finish();
计时器的用法比较简单,但我们也要注意如下事项:
-
启用收集 Grain 时,计时器不会将 Grain 激活的状态从空闲更改为使用中。不会因计时器在执行回调函数而推迟取消激活原本空闲的 Grain 。Grain 一旦被停用,那么定时器也将被丢弃。
-
从 asyncCallback 返回的 Task 被解析之前,不会执行下一次 asyncCallback 。 计时器永远不会重叠调用 asyncCallback。而且 asyncCallback 所需要的执行时间会影响到调用 asyncCallback 的频率。这与 System.Threading.Timer 一个很重要的不同点。
-
任何由 asyncCallback 返回的异常或错误的 Task 都会被记录,但不会阻止下一次 asyncCallback 进入队列。
-
asyncCallback 调用不会作为消息传递,因此它不会受消息交互场景的影响。
以上就是 Timers 的用法。接下来看下 Reminders 。
Reminders
Reminders 翻译过来为提醒,但我觉得这不太贴切,所以就保留英文。Reminders 与计时器有类似的地方,我们先看它的用法,最后再来说它们之间的区别。
Reminders 要依赖存储,所以在使用之前要指定存储方式。存储方式可以在 Silo 配置,创建 SiloHostBuilder 对象时调用 UseXReminderService 扩展方法进行配置,其中 X 是存储方式名称。例如:我使用 MySql 数据库,在配置之前先添加 NuGet 引用 Microsoft.Orleans.Reminders.AdoNet 和 MySql.Data ,具体的配置如下:
.UseAdoNetReminderService(options =>
{
options.ConnectionString = "Server=localhost;Port=3306;Database=yld;Uid=root;Pwd=mysql;";
options.Invariant = "MySql.Data.MySqlClient";
})
除了 SQL 数据库,还支持如下图:
每个具体的配置方案这里就不介绍,相信都会。另外,Orleans 还提供了一个开发环境下使用的内存存储方式,其配置比较简单直接使用 UseInMemoryReminderService 方法即可。
这里我就使用 MySql 来做示例。
先做些准备工作,创建一个数据,这里数据库名称就叫 yld 。然后到 Github 上下载 MySQL-Reminders.sql 的数据库脚本 。执行脚本,准备工作完成。 接下来按下面步骤启动一个 Reminders 。
1、定义一个 IReminderGrain 接口如下:
public interface IReminderGrain : IGrainWithIntegerKey
{
Task Execute();
Task Cancel();
}
2、定义一个 ReminderGrain 类,该类继承 Grain 和 IReminderGrain 、IRemindable 接口。
3、实现 IRemindable 接口里的 ReceiveReminder 方法。
4、在 Execute 方法调用 Grain 类的 RegisterOrUpdateReminder 方法,进行注册。该方法参数说明如下:
string reminderName // 设置名称,该名称必须唯一
TimeSpan dueTime // 启动到第一次执行间隔的时间量
TimeSpan period // 等行为下一次执行间隔的时间量
该方法返回一个 IGrainReminder 。
5、通过 Grain 类的 UnregisterReminder 方法可以取消 Reminders ,该方法接收一个 IGrainReminder 参数。IGrainReminder 可以通过 Grain 类的 GetReminder 方法获取,GetReminder 方法是通过名称来获取 IGrainReminder 。
完整代码如下:
public class ReminderGrain : Grain, IReminderGrain, IRemindable
{
private IGrainReminder? reminder;
private readonly ILogger logger;
public ReminderGrain(ILogger<HelloGrain> logger)
{
this.logger = logger;
}
public Task Cancel()
{
if (reminder != null)
{
logger.LogInformation($"结束 Reminider --{DateTime.Now}\n");
UnregisterReminder(reminder);
}
return Task.CompletedTask;
}
public async Task Execute()
{
reminder = await RegisterOrUpdateReminder("Reminder Demo", TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(60));
}
public Task ReceiveReminder(string reminderName, TickStatus status)
{
logger.LogInformation($"执行 Reminider --{DateTime.Now}\n");
return Task.CompletedTask;
}
}
client 调用
var reminderGrain = client.GetGrain<IReminderGrain>(1);
await reminderGrain.Execute();
Console.WriteLine("待待 120 秒 ...");
Thread.Sleep(120000);
Console.WriteLine("Reminder 停止--" + DateTime.Now);
await reminderGrain.Cancel();
Reminders 启动时会在数据库表 orleansreminderstable 中新增一条记录。暂停 Reminder 时会把该条记录删除。
完整代码,放在 github 上。
Timers 与 Reminders 区别
-
Reminders 是持久的,因为它通过存储,保证在部分或全部集群重启时可以继承触发,除非明确取消
-
Reminders 不会特定的时间去执行特定的任务,这样会错过时间执行任务,只能执行下一次任务。
-
Reminders 与 Grain 是相互关联,无需单独激活。
-
如果 Reminders 还在执行中,而没有 Grain 可以关联,则会自动创建一个 Grain 。如果 Grain 状态变为空闲且被停用,则将会在下一次执行时激活该 Grain。
-
Reminders 可以通过消息传递,并受制于其它 grain 方法的交互场景。
-
Reminders 不是一个高频计时器,最小单位为 1 分钟。它的周期以分钟,小时或天为单位。不能以秒为单位会报错。
如何选择
-
grain 停用或 silo 崩掉,计时器会停止作业从而影响任务的执行,而 Reminders 不会。如果对任务执行比较重要,要求比较高,集群重启不影响任务执行,那就选 Reminders 。否则,就选择 Timers 。
-
计时器的作业频率快,以秒或分钟为单元。而 Reminders 执行一些频率比较慢的任务,例如:以分钟、小时或天为单位。
-
计时器可以在调用 grain 方法启动或在 Grain 类的 OnActivateAsync 方法启动。
Timers 和 Reminders 的 dueTime 参数尽量不设置为 0 。
最后,祝大家学习愉快!