如何高效地执行超时操作
最近为一家公司开发集成微信的客服系统。粉丝通过微信与客服联系,客服通过微信,手机app,或移动页面即时回复粉丝咨询内容。其中有一个需求是粉丝在一段时间内(比如10分钟)没有任何回复操作就视为咨询结束(即会话过期),断开会话并提示粉丝已经断开咨询。
这是一个很常规的需求。通过在消息表中记录每个消息的时间,然后每隔一段时间统计出超时的信息,然后逐条执行会话过期操作。
这个方式,耗时,耗力(要查询所有消息记录),超时的时间也不是很准(取决于检查操作的时间间隔)。一旦数据量大了,过期查询操作都可能超过执行间隔时间了。
有一个优化的方案是把过期查询独立出来,使用一个新表或直接使用一个字典记录每个会话(Key)和最后接收时间(LastTime)。然后定期执行过期检查(遍历所有会话记录,找出最后接收时间超出10分钟的记录),找出记录执行过期操作。
当然这个方案时间需要把定期检查的时间间隔设置成很小,才能保证过期操作的及时性。这里有人会提出另一种优化方案:直接把时钟绑定到会话中。这些方案理论上都是可行的,但在会话量特别大时(实际如果在微信公众号上推新活动时,同时进来咨询的粉丝可能达到上万人之多)
这样会直接把服务器拖死。
于是,我想到了使用消息中间件,把这个超时处理完全从系统中独立出来。由于原先我们就在使用Redis作为缓存服务,得知它还有一个Key过期通知(需要额外开启,看来这个功能会影响性能)。于是我们很快就把会话过期系统设计开发出来。
在经过一段时间的运行后,我们发现系统中会有少量的会话过期没有准时执行。在分析原因时,发现使用Redis做过期通知一不稳定二增加了现有系统的复杂。
所以我还是决定自己动手写个基础的框架 - 左轮手枪。
为什么给这个框架取名为左轮手枪而不是机枪呢?机枪多厉害,左轮手枪是不是有点老掉牙,性能不怎么样啊?(这个做技术的嘛,起名字当然不能太高调)
主要是因为:超时操作,更像左轮手枪是不断重复(转轮)的。我们可以把每一件事看作是一颗待发射的子弹。它们被合理地安排在左轮上。而左轮手枪只是有规则地触发下一个弹膛中的子弹而已。
所以我首先定义了子弹,它只有编号和动作:
public interface IBullet
{
string Id { get; }
Task Action { get; }
}
然后定义左轮,根据不同的业务左轮中可以放置的最大子弹量可以调整。
private readonly Dictionary<string, IBullet>[] wheel;
最后封装在Revolver中,使用一个Timer有规则地扣扳机。
Revolver()
{
this.wheel = new Dictionary<string, IBullet>[maxSeconds];
for (int i = 0; i < wheel.Length; i++)
{
this.wheel[i] = new Dictionary<string, IBullet>();
}
this.timer = new Timer(1000);
this.timer.Elapsed += Timer_Elapsed;
this.timer.Start();
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
var slot = (int)(DateTime.UtcNow - baseTime).TotalSeconds % 3600;
foreach (var key in this.wheel[slot].Keys)
{
lock (lockObject)
{
map.Remove(key);
}
this.wheel[slot][key].Action.Start();
}
this.wheel[slot] = new Dictionary<string, IBullet>();
}
如果有事件需要超时通知,只需自定义一个子弹:
public class MyBullet : IBullet
{
public Task Action
{
get
{
return Task.Run(() => { Console.WriteLine(this.Id + " Run @" + DateTime.Now); });
}
}
public string Id { get; set; }
}
然后把子弹上膛即可:
Revolver.Instance.SetTimeOut(new MyBullet() { Id = "消息1" }, 10);
Revolver.Instance.SetTimeOut(new MyBullet() { Id = "消息2" }, 20);