场景
有这样一个场景,一个邮件提醒的windows服务,获取所有开启邮件提醒的用户,循环获取这些用户的邮件,发送一条服务号消息。但问题来了,用户比较少的情况下,轮询一遍时间还能忍受,如果用户多了,那用户名称排序靠后的人,收到邮件提醒的消息,延迟时间就非常长了。
准备
方案
1、生产者线程一获取所有开启邮件提醒的用户。
2、根据配置来决定使用多少个队列,以及每个队列的容量。
3、线程一,获取未满的队列,将当前用户入队。如果所有的队列已满,则挂起2s,然后重新获取未满的队列,用户入队。
4、根据配置开启消费者线程,每个线程独立处理逻辑。如果获取的用户为空或者当前队列为空,挂起2s。否则通过EWS服务拉取该用户的邮件,并提醒。
5、如果在获取用户邮件的过程中出错,则将该用户重新入当前队列,等待下次拉取。
测试
队列
测试代码
/// <summary> /// 消息队列管理 /// </summary> public class MyRedisQueueBus : IDisposable { /// <summary> /// 线程个数 /// </summary> private int _threadCount; /// <summary> /// 每个线程中itcode的容量 /// </summary> private int _threadCapacity; /// <summary> /// 线程 /// </summary> private Thread[] _threads; /// <summary> /// 生产者线程 /// </summary> private Thread _producerThread; /// <summary> /// 挂起时间 /// </summary> private const int WAITSECONDE = 2000; /// <summary> /// 队列名称前缀 /// </summary> private string _queuePrefix; /// <summary> /// 构造函数 /// </summary> /// <param name="threadCount">线程个数</param> /// <param name="threadCapacity">每个线程处理的队列容量</param> /// <param name="queuePrefix">每个线程处理的队列容量</param> public MyRedisQueueBus(int threadCount, int threadCapacity, string queuePrefix) { this._threadCapacity = threadCapacity; this._threadCount = threadCount; this._queuePrefix = queuePrefix + "_{0}"; } /// <summary> /// 开启生产者 /// </summary> public void StartProducer() { _producerThread = new Thread(() => { IRedisClientFactory factory = RedisClientFactory.Instance; EmailAlertsData emailAlertsData = new EmailAlertsData(); //白名单 string[] userIdsWhiteArray = TaskGloableParameter.WhiteList.Split(new char[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries); //入队 using (IRedisClient client = factory.CreateRedisClient(WebConfig.RedisServer, WebConfig.RedisPort)) { client.Password = WebConfig.RedisPwd; client.Db = WebConfig.RedisServerDb; while (true) { //获取所有开启邮件提醒的用户 List<EmailAlerts> lstEmails = emailAlertsData.GetAllStartAlerts(SyncState.ALL, userIdsWhiteArray); foreach (var item in lstEmails) { int queueIndex = -1; string queueName = string.Format(this._queuePrefix, queueIndex); for (int i = 0; i < _threadCount; i++) { queueName = string.Format(this._queuePrefix, i); //如果当前队列没有填满,则直接跳出,使用该队列进行入队 if (client.GetListCount(queueName) < _threadCapacity) { queueIndex = i; break; } } //如果所有队列都已经满了,则挂起2s等待消费者消耗一部分数据,然后重新开始 if (queueIndex == -1) { Thread.SpinWait(WAITSECONDE); //重新获取队列 for (int i = 0; i < _threadCount; i++) { queueName = string.Format(this._queuePrefix, i); //如果当前队列没有填满,则直接跳出,使用该队列进行入队 if (client.GetListCount(queueName) < _threadCapacity) { queueIndex = i; break; } } } else { //入队 client.EnqueueItemOnList(queueName, JsonConvert.SerializeObject(new MyQueueItem { UserId = item.itcode, SyncState = item.Email_SyncState })); } } } } }); _producerThread.Start(); } /// <summary> /// 开启消费者 /// </summary> public void StartCustomer() { _threads = new Thread[_threadCount]; for (int i = 0; i < _threads.Length; i++) { _threads[i] = new Thread(CustomerRun); _threads[i].Start(i); } } private void CustomerRun(object obj) { int threadIndex = Convert.ToInt32(obj); string queueName = string.Format(this._queuePrefix, threadIndex); IRedisClientFactory factory = RedisClientFactory.Instance; using (IRedisClient client = factory.CreateRedisClient(WebConfig.RedisServer, WebConfig.RedisPort)) { while (true) { client.Password = WebConfig.RedisPwd; client.Db = WebConfig.RedisServerDb; if (client.GetListCount(queueName) > 0) { string resultJson = client.DequeueItemFromList(queueName); //如果获取的结果为空,则挂起2s if (string.IsNullOrEmpty(resultJson)) { Thread.SpinWait(WAITSECONDE); } else { try { //耗时业务处理 MyQueueItem item = JsonConvert.DeserializeObject<MyQueueItem>(resultJson); Console.WriteLine("Threadid:{0},User:{1}", Thread.CurrentThread.ManagedThreadId.ToString(), item.UserId); } catch (Exception ex) { //如果出错,重新入队 client.EnqueueItemOnList(queueName, resultJson); } } } else { //当前队列为空,挂起2s Thread.SpinWait(WAITSECONDE); } } } } public void Dispose() { //释放资源时,销毁线程 if (this._threads != null) { for (int i = 0; i < this._threads.Length; i++) { this._threads[i].Abort(); } } GC.Collect(); } }
Main方法调用
static void Main(string[] args) { MyRedisQueueBus bus = new MyRedisQueueBus(10, 10, "mail_reminder_queue"); bus.StartProducer(); Thread.SpinWait(2000); bus.StartCustomer(); Console.Read(); }
总结
通过配置的方式,确定开启的队列数和线程数,如果用户增加可以增加线程数,或者添加机器的方式解决。这样,可以解决排名靠后的用户,通过随机分发队列,有机会提前获取邮件提醒,可以缩短邮件提醒的延迟时间。当然,这种方案并不太完美,目前也只能想到这里了。这里把这个思路写出来,也是希望获取一个更好的解决方案。
上面的代码只是测试用的代码,后来发现将创建IRedisClient写在循环内,很容易出问题,频繁创建client,也以为这频繁打开关闭,如果释放不及时,那么会产生很多的redis连接,造成redis服务器负担。如果放在循环外边,这个client负责一直从队列中取数据就行,直到该线程停止。