• c#之Redis队列在邮件提醒中的应用


    场景

    有这样一个场景,一个邮件提醒的windows服务,获取所有开启邮件提醒的用户,循环获取这些用户的邮件,发送一条服务号消息。但问题来了,用户比较少的情况下,轮询一遍时间还能忍受,如果用户多了,那用户名称排序靠后的人,收到邮件提醒的消息,延迟时间就非常长了。

    准备

    c#之Redis实践list,hashtable

    c#之Redis队列

    方案

    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负责一直从队列中取数据就行,直到该线程停止。

  • 相关阅读:
    Sql之表的连接总结
    sql之独立子查询和相关子查询总结
    canvas 绘点图
    gulp插件
    jquery插件开发模板
    js中substring和substr的用法比较
    phpStudy 2016 更新下载,新版支持php7.0
    phpStudy for Linux (lnmp+lamp一键安装包)
    用 Function.apply() 的参数数组化来提高 JavaScript程序性能
    Js apply() call()使用详解
  • 原文地址:https://www.cnblogs.com/wolf-sun/p/5895795.html
Copyright © 2020-2023  润新知